diff --git a/src/js/services/bitcoin-uri.service.js b/src/js/services/bitcoin-uri.service.js index a7566812d..f6f91ddce 100644 --- a/src/js/services/bitcoin-uri.service.js +++ b/src/js/services/bitcoin-uri.service.js @@ -1,6 +1,7 @@ 'use strict'; -// https://en.bitcoin.it/wiki/BIP_0072 +// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +// https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki (function(){ @@ -30,9 +31,91 @@ } + function bitpayAddrOnMainnet(address) { + var Address = bch.Address; + var BitpayFormat = Address.BitpayFormat; + + var mainnet = bch.Networks.mainnet; + + var result = null; + if (address[0] == 'C') { + try { + result = Address.fromString(address, mainnet, 'pubkeyhash', BitpayFormat); + } catch (e) {}; + + } else if (address[0] == 'H') { + try { + result = Address.fromString(address, mainnet, 'scripthash', BitpayFormat); + } catch (e) {}; + + } + return result; + } + + + function cashAddrOnMainnet(address) { + var Address = bch.Address; + var CashAddrFormat = Address.CashAddrFormat; + + var mainnet = bch.Networks.mainnet; + + var prefixed = 'bitcoincash:' + address; + var result = null; + if (address[0] == 'q') { + try { + result = Address.fromString(prefixed, mainnet, 'pubkeyhash', CashAddrFormat); + } catch (e) {}; + + } else if (address[0] == 'p') { + try { + result = Address.fromString(prefixed, mainnet, 'scripthash', CashAddrFormat); + } catch (e) {}; + + } + return result; + } + + function cashAddrOnTestnet(address) { + var Address = bch.Address; + var CashAddrFormat = Address.CashAddrFormat; + + var testnet = bch.Networks.testnet; + + var prefixed = 'bchtest:' + address; + var result = null; + if (address[0] == 'q') { + try { + result = Address.fromString(prefixed, testnet, 'pubkeyhash', CashAddrFormat); + } catch (e) {}; + + } else if (address[0] == 'p') { + try { + result = Address.fromString(prefixed, testnet, 'scripthash', CashAddrFormat); + } catch (e) {}; + + } + return result; + } function isValidCashAddr(address, network) { + var a = address.replace('bitcoincash:', ''); + var result = {}; + if (a[0] == '1') { + result = Address.fromString(a, 'livenet', 'pubkeyhash'); + } else if (a[0] == '3') { + result = Address.fromString(a, 'livenet', 'scripthash'); + } else if (a[0] == 'C') { + result = Address.fromString(a, 'livenet', 'pubkeyhash', BitpayFormat); + } else if (a[0] == 'H') { + result = Address.fromString(a, 'livenet', 'scripthash', BitpayFormat); + } else if (a[0] == 'q') { + result = Address.fromString(address, 'livenet', 'pubkeyhash', CashAddrFormat); + } else if (a[0] == 'p') { + result = Address.fromString(address, 'livenet', 'scripthash', CashAddrFormat); + } else { + return null; + } var isValid = false; @@ -144,55 +227,61 @@ var address = questionMarkSplit[1]; var params = questionMarkSplit[2]; - var paramsSplit = params.split('&'); - var others; - var req; - paramsSplit.forEach(function onParam(param){ - var valueSplit = param.split('='); - if (valueSplit.length !== 2) { - return parsed; - } + if (params.length > 0) { + var paramsSplit = params.split('&'); + var others; + var req; + var paramCount = paramsSplit.length; + for(var i = 0; i < paramCount; i++) { + var param = paramsSplit[i]; + var valueSplit = param.split('='); + if (valueSplit.length !== 2) { + return parsed; + } - var key = valueSplit[0]; - var value = valueSplit[1]; - switch(key) { - case 'amount': - if (parseFloat(value)) { - parsed.amount = value; - } else { - return parsed; - } - break; + var key = valueSplit[0]; + var value = valueSplit[1]; + var decodedValue = decodeURIComponent(value); + switch(key) { + case 'amount': + var amount = parseFloat(decodedValue); + if (amount) { // Checking for NaN, or no numbers at all etc. + parsed.amount = decodedValue; + } else { + return parsed; + } + break; - case 'label': - parsed.label = value; - break; + case 'label': + parsed.label = decodedValue; + break; - case 'message': - parsed.message = value; - break; + case 'message': + parsed.message = decodedValue; + break; - case 'r': - // Could use a more comprehesive regex to test URL validity, but then how would we know - // which part of the validation it failed? - if (value.startsWith('https://')) { - parsed.url = value; - } else { - return parsed; - } - break; + case 'r': + // Could use a more comprehesive regex to test URL validity, but then how would we know + // which part of the validation it failed? + if (decodedValue.startsWith('https://')) { + parsed.url = decodedValue; + } else { + return parsed; + } + break; - default: - if (key.startsWith('req-')) { - req = req || {}; - req[key] = value; - } else { - others = others || {}; - others[key] = value; - } - } + default: + if (key.startsWith('req-')) { + req = req || {}; + req[key] = decodedValue; + } else { + others = others || {}; + others[key] = decodedValue; + } + } - }); + }; + } parsed.others = others; parsed.req = req; @@ -207,43 +296,43 @@ //var legacyRe = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/; //var legacyTestnetRe = /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/; + var bitpayAddrMainnet = bitpayAddrOnMainnet(address); + var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase); + var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase); - if (bitcore.Address.isValid(address, 'livenet')) { + if (parsed.testnet && cashAddrTestnet) { + parsed.address = addressLowerCase; + parsed.coin = 'bch'; + parsed.legacyAddress = cashAddrTestnet.toString(); + + } else if (cashAddrMainnet) { + parsed.address = addressLowerCase; + parsed.coin = 'bch'; + parsed.legacyAddress = cashAddrMainnet.toString(); + parsed.testnet = false; + + } else if (bitcore.Address.isValid(address, 'livenet') && parsed.coin !== 'bch') { parsed.address = address; parsed.legacyAddress = address; parsed.testnet = false; - } else if (bitcore.Address.isValid(address, 'testnet')) { + } else if (bitcore.Address.isValid(address, 'testnet') && parsed.coin !== 'bch') { parsed.address = address; parsed.legacyAddress = address; parsed.testnet = true; - // bitcoinCaashJs.Address.isValid() assumes legacy address for string data, so does not work with cashaddr. - // } else if (isValidCashAddr(addressLowerCase, 'livenet')) { - } else if (cashAddrRe.test(address) && parsed.testnet) { - var cashAddr = 'bchtest:' + addressLowerCase; - parsed.address = cashAddr; + } else if (bitpayAddrMainnet) { + parsed.address = address; parsed.coin = 'bch'; - // TODO: Get legacy address - - - } else if (cashAddrRe.test(address)) { - var cashAddr = 'bitcoincash:' + addressLowerCase; - parsed.address = cashAddr; - parsed.coin = 'bch'; - - var bchAddresses = bitcoinCashJsService.readAddress(cashAddr); - parsed.legacyAddress = bchAddresses['legacy']; - + parsed.legacyAddress = bitpayAddrMainnet.toString(); parsed.testnet = false; - - } + } } - // TODO: Check for a private key here too + // TODO: Check for a private key here too, including WIF format, etc. // If has no address, must have Url. diff --git a/src/js/services/bitcoin-uri.service.spec.js b/src/js/services/bitcoin-uri.service.spec.js index 8ac2608a3..d72ce8b20 100644 --- a/src/js/services/bitcoin-uri.service.spec.js +++ b/src/js/services/bitcoin-uri.service.spec.js @@ -12,6 +12,17 @@ fdescribe('bitcoinUriService', function() { }); + it('Bitcoin BIP72', function() { + + var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + + expect(parsed.isValid).toBe(true); + expect(parsed.address).toBeUndefined() + expect(parsed.coin).toBe('btc'); + expect(parsed.legacyAddress).toBeUndefined(); + expect(parsed.testnet).toBeUndefined(); + expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + }); it('Bitcoin Cash BIP72', function() { @@ -25,38 +36,93 @@ fdescribe('bitcoinUriService', function() { expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu'); }); - it('Bitcoin BIP72', function() { + it('Bitcoin Cash prefix with legacy address', function() { - var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + var parsed = bitcoinUriService.parse('bitcoincash:1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j'); + + expect(parsed.isValid).toBe(false); + }); + + it('Bitcoin Cash uri with extended params', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc?unknown=something&mystery=Melton%20probang&req-one=ichi&req-beta=Ni%20san'); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBeUndefined() + expect(parsed.address).toBe('qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc'); + expect(parsed.coin).toBe('bch'); + expect(parsed.legacyAddress).toBe('1KrJRNApaAKRvHL5kDtL69nwmAJ31apAnu'); + expect(parsed.others.mystery).toBe('Melton probang'); + expect(parsed.others.unknown).toBe('something'); + expect(parsed.req['req-beta']).toBe('Ni san'); + expect(parsed.req['req-one']).toBe('ichi'); + expect(parsed.testnet).toBe(false); + }); + + it('Bitcoin Cash uri with invalid amount', function() { + + var parsed = bitcoinUriService.parse('bitcoincash:qq0knhwj4d5zy3kdph24w6etq58vwzua6sm7lhcmuk?amount=three'); + + expect(parsed.isValid).toBe(false); + }); + + + it('Bitcoin testnet address', function() { + + var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + + expect(parsed.isValid).toBe(true); + expect(parsed.address).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + expect(parsed.coin).toBeUndefined(); + expect(parsed.legacyAddress).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + expect(parsed.testnet).toBe(true); + }); + + it('Bitcoin testnet address', function() { + + var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + + expect(parsed.isValid).toBe(true); + expect(parsed.address).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + expect(parsed.coin).toBeUndefined(); + expect(parsed.legacyAddress).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + expect(parsed.testnet).toBe(true); + }); + + it('Bitcoin uri', function() { + + var parsed = bitcoinUriService.parse('bitcoin:15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN'); + + expect(parsed.isValid).toBe(true); + expect(parsed.address).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN'); expect(parsed.coin).toBe('btc'); - expect(parsed.legacyAddress).toBeUndefined(); - expect(parsed.testnet).toBeUndefined(); - expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr'); + expect(parsed.legacyAddress).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN'); + expect(parsed.testnet).toBe(false); }); - it('Bitcoin testnet address', function() { + it('Bitcoin uri with encoded label', function() { - var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + var parsed = bitcoinUriService.parse('bitcoin:1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT?label=Mr.%20Smith'); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); - expect(parsed.coin).toBeUndefined(); - expect(parsed.legacyAddress).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); - expect(parsed.testnet).toBe(true); + expect(parsed.address).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT'); + expect(parsed.coin).toBe('btc'); + expect(parsed.label).toBe('Mr. Smith'); + expect(parsed.legacyAddress).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT'); + expect(parsed.testnet).toBe(false); }); - it('Bitcoin testnet address', function() { + it('Bitcoin uri with params', function() { - var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); + var parsed = bitcoinUriService.parse('bitcoin:12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu?amount=20.3&label=Luke-Jr&message=Donation%20for%20project%20xyz'); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); - expect(parsed.coin).toBeUndefined(); - expect(parsed.legacyAddress).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4'); - expect(parsed.testnet).toBe(true); + expect(parsed.amount).toBe('20.3'); + expect(parsed.address).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu'); + expect(parsed.coin).toBe('btc'); + expect(parsed.label).toBe('Luke-Jr'); + expect(parsed.legacyAddress).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu'); + expect(parsed.message).toBe('Donation for project xyz'); + expect(parsed.testnet).toBe(false); }); it('legacy address', function() { @@ -75,7 +141,7 @@ fdescribe('bitcoinUriService', function() { var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt'); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBe('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt'); + expect(parsed.address).toBe('qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt'); expect(parsed.coin).toBe('bch'); expect(parsed.legacyAddress).toBe('mqk5vE278ytt6LUZqd97wi8c3FHsSYREX4'); expect(parsed.testnet).toBe(true); @@ -84,9 +150,10 @@ fdescribe('bitcoinUriService', function() { it('cashAddr with prefix', function() { var parsed = bitcoinUriService.parse('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92'); + console.log('parsed:', JSON.stringify(parsed)); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBe('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92'); + expect(parsed.address).toBe('qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92'); expect(parsed.coin).toBe('bch'); expect(parsed.legacyAddress).toBe('1JXsK3HSFqoMnwh4Mevf5bTgqPcgNWX7ic'); expect(parsed.testnet).toBe(false); @@ -97,12 +164,26 @@ fdescribe('bitcoinUriService', function() { var parsed = bitcoinUriService.parse('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq'); expect(parsed.isValid).toBe(true); - expect(parsed.address).toBe('bitcoincash:qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq'); + expect(parsed.address).toBe('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq'); expect(parsed.coin).toBe('bch'); expect(parsed.legacyAddress).toBe('15fm3EwqgBYcxkndALBfforueps5yWKReJ'); expect(parsed.testnet).toBe(false); }); + + it('Bitpay without prefix', function() { + + var parsed = bitcoinUriService.parse('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz'); + + expect(parsed.isValid).toBe(true); + expect(parsed.address).toBe('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz'); + expect(parsed.coin).toBe('bch'); + expect(parsed.legacyAddress).toBe('13LYEsnPqogE2SqJ325u1ZfiWxSWEo6uyo'); + expect(parsed.testnet).toBe(false); + }); + + + // Invalid addresses from https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md it('invalid cashAddr style 1', function() { var parsed = bitcoinUriService.parse('prefix:x64nx6hz');