From 9b0b4e3fc022c134747e22d0d0aaa5c8cef8bba3 Mon Sep 17 00:00:00 2001 From: Emilio Almansi Date: Tue, 2 Jan 2018 01:37:16 -0300 Subject: [PATCH] Add support for Bitpay and CashAddr formats. --- gulpfile.js | 11 ++- package-lock.json | 8 ++ package.json | 1 + src/address.js | 74 ++++++++++++++++--- test/address.js | 185 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 10 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 5b42373..a1f50d8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -44,10 +44,19 @@ gulp.task( gulp.task( 'test:mocha', shell.task([ - 'npx nyc --reporter=html --reporter=text npx mocha', + `npx nyc --reporter=html --reporter=text npx mocha ${getTaskArgs()}`, ]) ); +function getTaskArgs() { + if (process.argv.length < 4) { + return ''; + } + const args = process.argv.splice(3); + const argsWithQuotes = args.map(a => a.indexOf(' ') !== -1 ? `"${a}"` : a); + return argsWithQuotes.join(' '); +} + gulp.task( 'test:karma', ['build:tests'], diff --git a/package-lock.json b/package-lock.json index a4f0eb8..540d37a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1689,6 +1689,14 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "cashaddrjs": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.1.4.tgz", + "integrity": "sha512-JWBY7IUwOX0h+s5F0xheVTD58qUa1SE1AUFasWUFNLxy/7N9B+LpLzFYb4UCMd/Fy5inRJGY36LJ9FjeuwTBLg==", + "requires": { + "big-integer": "1.6.26" + } + }, "catharsis": { "version": "0.8.9", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", diff --git a/package.json b/package.json index a60b8ae..d824945 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "bn.js": "=2.0.4", "bs58": "=2.0.0", "buffer-compare": "=1.0.0", + "cashaddrjs": "^0.1.4", "elliptic": "=3.0.3", "inherits": "=2.0.1", "lodash": "^4.17.4" diff --git a/src/address.js b/src/address.js index b017bc7..bd0355c 100644 --- a/src/address.js +++ b/src/address.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var $ = require('./util/preconditions'); +var cashaddr = require('cashaddrjs'); var errors = require('./errors'); var Base58Check = require('./encoding/base58check'); var Networks = require('./networks'); @@ -9,6 +10,9 @@ var Hash = require('./crypto/hash'); var JSUtil = require('./util/js'); var PublicKey = require('./publickey'); +const BITPAY_P2PKH_VERSION_BYTE = 28; +const BITPAY_P2SH_VERSION_BYTE = 40; + /** * Instantiate an address from an address String or Buffer, a public key or script hash Buffer, * or an instance of {@link PublicKey} or {@link Script}. @@ -104,7 +108,7 @@ Address.prototype._classifyArguments = function(data, network, type) { } else if (data instanceof Script) { return Address._transformScript(data, network); } else if (typeof(data) === 'string') { - return Address._transformString(data, network, type); + return Address._transformString(data, network, type, Address.DefaultFormat); } else if (_.isObject(data)) { return Address._transformObject(data); } else { @@ -112,6 +116,15 @@ Address.prototype._classifyArguments = function(data, network, type) { } }; +/** @static */ +Address.LegacyFormat = 'legacy'; +/** @static */ +Address.BitpayFormat = 'bitpay'; +/** @static */ +Address.CashAddrFormat = 'cashaddr'; +/** @static */ +Address.DefaultFormat = Address.LegacyFormat; + /** @static */ Address.PayToPublicKeyHash = 'pubkeyhash'; /** @static */ @@ -271,14 +284,34 @@ Address.createMultisig = function(publicKeys, threshold, network) { * @returns {Object} An object with keys: hashBuffer, network and type * @private */ -Address._transformString = function(data, network, type) { +Address._transformString = function(data, network, type, format) { if (typeof(data) !== 'string') { throw new TypeError('data parameter supplied is not a string.'); } data = data.trim(); - var addressBuffer = Base58Check.decode(data); - var info = Address._transformBuffer(addressBuffer, network, type); - return info; + if (format === Address.LegacyFormat) { + return Address._transformBuffer(Base58Check.decode(data), network, type); + } + else if (format === Address.BitpayFormat) { + var addressBuffer = Base58Check.decode(data); + if (format === Address.BitpayFormat) { + if (addressBuffer[0] === BITPAY_P2PKH_VERSION_BYTE) { + addressBuffer[0] = 0; + } + else if (addressBuffer[0] === BITPAY_P2SH_VERSION_BYTE) { + addressBuffer[0] = 5; + } + } + return Address._transformBuffer(addressBuffer, network, type); + } + else if (format === Address.CashAddrFormat) { + var networkObject = Networks.get(network); + var version = new Buffer([networkObject[type]]); + var hashBuffer = new Buffer(cashaddr.decode(data).hash); + var addressBuffer = Buffer.concat([version, hashBuffer]); + return Address._transformBuffer(addressBuffer, network, type); + } + throw new TypeError('Unrecognized address format.'); }; /** @@ -375,8 +408,9 @@ Address.fromBuffer = function(buffer, network, type) { * @param {string=} type - The type of address: 'script' or 'pubkey' * @returns {Address} A new valid and frozen instance of an Address */ -Address.fromString = function(str, network, type) { - var info = Address._transformString(str, network, type); +Address.fromString = function(str, network, type, format) { + format = format || Address.DefaultFormat; + var info = Address._transformString(str, network, type, format); return new Address(info.hashBuffer, info.network, info.type); }; @@ -480,8 +514,30 @@ Address.prototype.toObject = Address.prototype.toJSON = function toObject() { * * @returns {string} Bitcoin address */ -Address.prototype.toString = function() { - return Base58Check.encode(this.toBuffer()); +Address.prototype.toString = function(format) { + format = format || Address.DefaultFormat; + if (format === Address.LegacyFormat) { + return Base58Check.encode(this.toBuffer()); + } + else if (format === Address.BitpayFormat) { + var buffer = this.toBuffer(); + if (this.network.toString() === 'livenet') { + if (this.type === Address.PayToPublicKeyHash) { + buffer[0] = BITPAY_P2PKH_VERSION_BYTE; + } + else if (this.type === Address.PayToScriptHash) { + buffer[0] = BITPAY_P2SH_VERSION_BYTE; + } + } + return Base58Check.encode(buffer); + } + else if (format === Address.CashAddrFormat) { + var prefix = this.network.toString() === 'livenet' ? 'bitcoincash' : 'bchtest'; + var type = this.type === Address.PayToPublicKeyHash ? 'P2PKH' : 'P2SH'; + var hash = [...this.hashBuffer]; + return cashaddr.encode(prefix, type, hash); + } + throw new TypeError('Unrecognized address format.'); }; /** diff --git a/test/address.js b/test/address.js index 48e54ed..c542852 100644 --- a/test/address.js +++ b/test/address.js @@ -116,6 +116,66 @@ describe('Address', function() { 'mtX8nPZZdJ8d3QNLRJ1oJTiEi26Sj6LQXS' ]; + const PKHLivenetBitpay = [ + 'CMPeBN1BZDzaqU5DF66X5QykLcS1voucT9', + 'CRZoT4EafXoYLNJm3bPpTjK3h4q1FSxet4', + 'CTHVPhghRAmiLHajoKYTGRyiU8RomQmAfZ', + 'CaSvYEmgxVRYiAauWzW1XP4SHkyTiS78yy', + 'CaSvYEmgxVRYiAauWzW1XP4SHkyTiS78yy', + ]; + + const P2SHLivenetBitpay = [ + 'H8rnMErHmZWKpp8H3beDwL8BsSEwzDFSjJ', + 'H8kzbJ9Mw46WdAxC8SAFadHn1oNqp6jEsu', + 'HCGvZEM8pNyAFBfRrz9Eo4N4eGJPuFahd9', + 'HVZezVtqnDwoTZTZ997fZUUGZMetDFUDLf', + 'HVZezVtqnDwoTZTZ997fZUUGZMetDFUDLf', + ]; + + const PKHTestnetBitpay = [ + 'n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X', + 'n45x3R2w2jaSC62BMa9MeJCd3TXxgvDEmm', + 'mursDVxqNQmmwWHACpM9VHwVVSfTddGsEM', + 'mtX8nPZZdJ8d3QNLRJ1oJTiEi26Sj6LQXS', + ]; + + const P2SHTestnetBitpay = [ + '2N7FuwuUuoTBrDFdrAZ9KxBmtqMLxce9i1C', + '2NEWDzHWwY5ZZp8CQWbB7ouNMLqCia6YRda', + '2MxgPqX1iThW3oZVk9KoFcE5M4JpiETssVN', + '2NB72XtkjpnATMggui83aEtPawyyKvnbX2o', + ]; + + const PKHLivenetCashAddr = [ + 'bitcoincash:qqmq4ua630cqumzt29ml2jmy8gesega95cjctx4j02', + 'bitcoincash:qp3awknl3dz8ezu3rmapff3phnzz95kansf0r3rs4x', + 'bitcoincash:qpmtrcpj2fgwfn7f5fxxg7xrx08nayzquuv62srvq4', + 'bitcoincash:qrz5xl0qmwxj8q7k57cfgn0avfuf86sr85x2056k40', + 'bitcoincash:qrz5xl0qmwxj8q7k57cfgn0avfuf86sr85x2056k40', + ]; + + const P2SHLivenetCashAddr = [ + 'bitcoincash:pqv60krfqv3k3lglrcnwtee6ftgwgaykpccr8hujjz', + 'bitcoincash:pqvglydfxx28ahwhgvkkuc2rsl3jkfz8py95pldjhu', + 'bitcoincash:pqljzrnjwlyfnsap2hxpey85zpktmhhvdcjse2m6lm', + 'bitcoincash:pr7v23sd6m3yslrawkcevd39mg8g7nzew5zmfd0lrt', + 'bitcoincash:pr7v23sd6m3yslrawkcevd39mg8g7nzew5zmfd0lrt', + ]; + + const PKHTestnetCashAddr = [ + 'bchtest:qr3pswmv0t332gwaedmuhqcp59gswsu2ysdn664dvs', + 'bchtest:qrmeqjy9l3me7tchp3szxuu692a9uhwu4ugltjlulm', + 'bchtest:qzw4t46ref4lm73d5l4ht3nzse987ee2tsv9zydr5v', + 'bchtest:qz82yclajj49kq3cnqk5khs9h2qx5drfruglvwmnac', + ]; + + const P2SHTestnetCashAddr = [ + 'bchtest:pzvmx80heyrg69ypkkt90rwmknfmmy96av8f02lrrf', + 'bchtest:pr5npcvrffxjx3czwuu4r438en5zlw6a9cm22jfau6', + 'bchtest:pqaek07h55x57zx35kc0vtmyf7n3zkhz7vxlq6zven', + 'bchtest:prp72h7we64y8y0d92t80a9y6d82e5pp5qafr2whk4', + ]; + describe('validation', function() { it('getValidationError detects network mismatchs', function() { @@ -558,4 +618,129 @@ describe('Address', function() { }); }); + describe('Address formats', function() { + + it('should throw an error if given an invalid format', function() { + (function() { + new Address(PKHLivenet[0]).toString('some invalid format'); + }).should.throw('Unrecognized address format.'); + }); + + it('should successfully convert address into Bitpay format', function() { + for (const i in PKHLivenet) { + const output = new Address(PKHLivenet[i]).toString(Address.BitpayFormat); + output.should.equal(PKHLivenetBitpay[i]); + } + for (const i in P2SHLivenet) { + const output = new Address(P2SHLivenet[i]).toString(Address.BitpayFormat); + output.should.equal(P2SHLivenetBitpay[i]); + } + for (const i in PKHTestnet) { + const output = new Address(PKHTestnet[i]).toString(Address.BitpayFormat); + output.should.equal(PKHTestnetBitpay[i]); + } + for (const i in P2SHTestnet) { + const output = new Address(P2SHTestnet[i]).toString(Address.BitpayFormat); + output.should.equal(P2SHTestnetBitpay[i]); + } + }); + + it('should successfully decode address in Bitpay format', function() { + for (const i in PKHLivenetBitpay) { + const address = Address.fromString( + PKHLivenetBitpay[i], + 'livenet' , + Address.PayToPublicKeyHash, + Address.BitpayFormat + ); + address.toString().should.equal(PKHLivenet[i].trim()); + } + for (const i in P2SHLivenetBitpay) { + const address = Address.fromString( + P2SHLivenetBitpay[i], + 'livenet' , + Address.PayToScriptHash, + Address.BitpayFormat + ); + address.toString().should.equal(P2SHLivenet[i].trim()); + } + for (const i in PKHTestnetBitpay) { + const address = Address.fromString( + PKHTestnetBitpay[i], + 'testnet' , + Address.PayToPublicKeyHash, + Address.BitpayFormat + ); + address.toString().should.equal(PKHTestnet[i].trim()); + } + for (const i in P2SHTestnetBitpay) { + const address = Address.fromString( + P2SHTestnetBitpay[i], + 'testnet' , + Address.PayToScriptHash, + Address.BitpayFormat + ); + address.toString().should.equal(P2SHTestnet[i].trim()); + } + }); + + it('should successfully convert address into CashAddr format', function() { + for (const i in PKHLivenet) { + const output = new Address(PKHLivenet[i]).toString(Address.CashAddrFormat); + output.should.equal(PKHLivenetCashAddr[i]); + } + for (const i in P2SHLivenet) { + const output = new Address(P2SHLivenet[i]).toString(Address.CashAddrFormat); + output.should.equal(P2SHLivenetCashAddr[i]); + } + for (const i in PKHTestnet) { + const output = new Address(PKHTestnet[i]).toString(Address.CashAddrFormat); + output.should.equal(PKHTestnetCashAddr[i]); + } + for (const i in P2SHTestnet) { + const output = new Address(P2SHTestnet[i]).toString(Address.CashAddrFormat); + output.should.equal(P2SHTestnetCashAddr[i]); + } + }); + + it('should successfully decode address in CashAddr format', function() { + for (const i in PKHLivenetCashAddr) { + const address = Address.fromString( + PKHLivenetCashAddr[i], + 'livenet' , + Address.PayToPublicKeyHash, + Address.CashAddrFormat + ); + address.toString().should.equal(PKHLivenet[i].trim()); + } + for (const i in P2SHLivenetCashAddr) { + const address = Address.fromString( + P2SHLivenetCashAddr[i], + 'livenet' , + Address.PayToScriptHash, + Address.CashAddrFormat + ); + address.toString().should.equal(P2SHLivenet[i].trim()); + } + for (const i in PKHTestnetCashAddr) { + const address = Address.fromString( + PKHTestnetCashAddr[i], + 'testnet' , + Address.PayToPublicKeyHash, + Address.CashAddrFormat + ); + address.toString().should.equal(PKHTestnet[i].trim()); + } + for (const i in P2SHTestnetCashAddr) { + const address = Address.fromString( + P2SHTestnetCashAddr[i], + 'testnet' , + Address.PayToScriptHash, + Address.CashAddrFormat + ); + address.toString().should.equal(P2SHTestnet[i].trim()); + } + }); + }); + });