diff --git a/lib/address.js b/lib/address.js index 88c8616..b7e3ec5 100644 --- a/lib/address.js +++ b/lib/address.js @@ -17,6 +17,8 @@ var JSUtil = require('./util/js'); * An address has two key properties: `network` and `type`. The type is either * `Address.PayToPublicKeyHash` (value is the `'pubkeyhash'` string) * or `Address.PayToScriptHash` (the string `'scripthash'`). The network is an instance of {@link Network}. + * You can quickly check whether an address is of a given kind by using the methods + * `isPayToPublicKeyHash` and `isPayToScriptHash` * * @example * ```javascript @@ -40,6 +42,8 @@ var JSUtil = require('./util/js'); * @constructor */ function Address(data, network, type) { + /* jshint maxcomplexity: 12 */ + /* jshint maxstatements: 20 */ if (!(this instanceof Address)) { return new Address(data, network, type); @@ -49,6 +53,11 @@ function Address(data, network, type) { return Address.createMultisig(data, network, type); } + if (data instanceof Address) { + // Immutable instance + return data; + } + if (!data) { throw new TypeError('First argument is required, please include address data.'); } @@ -61,24 +70,7 @@ function Address(data, network, type) { throw new TypeError('Third argument must be "pubkeyhash" or "scripthash".'); } - var info; - - // transform and validate input data - if ((data instanceof Buffer || data instanceof Uint8Array) && data.length === 20) { - info = Address._transformHash(data); - } else if ((data instanceof Buffer || data instanceof Uint8Array) && data.length === 21) { - info = Address._transformBuffer(data, network, type); - } else if (data.constructor && (data.constructor.name && data.constructor.name === 'PublicKey')) { - info = Address._transformPublicKey(data); - } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) { - info = Address._transformScript(data, network); - } else if (data instanceof Address) { - return data; - } else if (typeof(data) === 'string') { - info = Address._transformString(data, network, type); - } else { - throw new TypeError('First argument is an unrecognized data format.'); - } + var info = this._classifyArguments(data, network, type); // set defaults if not set info.network = info.network || Networks.get(network) || Networks.defaultNetwork; @@ -102,17 +94,37 @@ function Address(data, network, type) { return this; } +/** + * Internal function used to split different kinds of arguments of the constructor + * @param {*} data - The encoded data in various formats + * @param {Network|String|number} [network] - The network: 'livenet' or 'testnet' + * @param {String} [type] - The type of address: 'script' or 'pubkey' + * @returns {Object} An "info" object with "type", "network", and "hashBuffer" + */ +Address.prototype._classifyArguments = function(data, network, type) { + /* jshint maxcomplexity: 10 */ + // transform and validate input data + if ((data instanceof Buffer || data instanceof Uint8Array) && data.length === 20) { + return Address._transformHash(data); + } else if ((data instanceof Buffer || data instanceof Uint8Array) && data.length === 21) { + return Address._transformBuffer(data, network, type); + } else if (data.constructor && (data.constructor.name && data.constructor.name === 'PublicKey')) { + return Address._transformPublicKey(data); + } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) { + return Address._transformScript(data, network); + } else if (typeof(data) === 'string') { + return Address._transformString(data, network, type); + } else { + throw new TypeError('First argument is an unrecognized data format.'); + } +}; + /** @static */ Address.PayToPublicKeyHash = 'pubkeyhash'; -/** - * @static - * @value 'scripthash' - */ +/** @static */ Address.PayToScriptHash = 'scripthash'; /** - * Internal function to transform a hash buffer - * * @param {Buffer} hash - An instance of a hash Buffer * @returns {Object} An object with keys: hashBuffer * @private @@ -130,7 +142,7 @@ Address._transformHash = function(hash){ }; /** - * Internal function to discover the network and type + * Internal function to discover the network and type based on the first data byte * * @param {Buffer} buffer - An instance of a hex encoded address Buffer * @returns {Object} An object with keys: network and type @@ -138,24 +150,21 @@ Address._transformHash = function(hash){ */ Address._classifyFromVersion = function(buffer){ var version = {}; - switch(buffer[0]){ // the version byte + version.network = Networks.get(buffer[0]); + switch (buffer[0]) { // the version byte case Networks.livenet.pubkeyhash: - version.network = Networks.livenet; version.type = Address.PayToPublicKeyHash; break; case Networks.livenet.scripthash: - version.network = Networks.livenet; version.type = Address.PayToScriptHash; break; case Networks.testnet.pubkeyhash: - version.network = Networks.testnet; version.type = Address.PayToPublicKeyHash; break; case Networks.testnet.scripthash: - version.network = Networks.testnet; version.type = Address.PayToScriptHash; break; } @@ -172,6 +181,7 @@ Address._classifyFromVersion = function(buffer){ * @private */ Address._transformBuffer = function(buffer, network, type){ + /* jshint maxcomplexity: 9 */ var info = {}; if (!(buffer instanceof Buffer) && !(buffer instanceof Uint8Array)) { throw new TypeError('Address supplied is not a buffer.'); @@ -198,7 +208,7 @@ Address._transformBuffer = function(buffer, network, type){ }; /** - * Internal function to transform a PublicKey + * Internal function to transform a {@link PublicKey} * * @param {PublicKey} pubkey - An instance of PublicKey * @returns {Object} An object with keys: hashBuffer, type @@ -215,7 +225,7 @@ Address._transformPublicKey = function(pubkey){ }; /** - * Internal function to transform a Script + * Internal function to transform a {@link Script} into a `info` object. * * @param {Script} script - An instance of Script * @returns {Object} An object with keys: hashBuffer, type @@ -235,9 +245,13 @@ Address._transformScript = function(script, network){ /** * Creates a P2SH address from a set of public keys and a threshold. * - * @param {Array} publicKeys - * @param {number} threshold - * @param {Network} network + * The addresses will be sorted lexicographically, as that is the trend in bitcoin. + * To create an address from unsorted public keys, use the {@link Script#buildMultisigOut} + * interface. + * + * @param {Array} publicKeys - a set of public keys to create an address + * @param {number} threshold - the number of signatures needed to release the funds + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @return {Address} */ Address.createMultisig = function(publicKeys, threshold, network) { @@ -248,8 +262,8 @@ Address.createMultisig = function(publicKeys, threshold, network) { /** * Internal function to transform a bitcoin address string * - * @param {String} data - An instance of PublicKey - * @param {String} [network] - The network: 'livenet' or 'testnet' + * @param {String} data + * @param {String|Network} [network] - either a Network instance, 'livenet', or 'testnet' * @param {String} [type] - The type: 'pubkeyhash' or 'scripthash' * @returns {Object} An object with keys: hashBuffer, network and type * @private @@ -266,8 +280,8 @@ Address._transformString = function(data, network, type){ /** * Instantiate an address from a PublicKey instance * - * @param {PublicKey} data - An instance of PublicKey - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {PublicKey} data + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @returns {Address} A new valid and frozen instance of an Address */ Address.fromPublicKey = function(data, network){ @@ -280,7 +294,7 @@ Address.fromPublicKey = function(data, network){ * Instantiate an address from a ripemd160 public key hash * * @param {Buffer} hash - An instance of buffer of the hash - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @returns {Address} A new valid and frozen instance of an Address */ Address.fromPublicKeyHash = function(hash, network) { @@ -292,7 +306,7 @@ Address.fromPublicKeyHash = function(hash, network) { * Instantiate an address from a ripemd160 script hash * * @param {Buffer} hash - An instance of buffer of the hash - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @returns {Address} A new valid and frozen instance of an Address */ Address.fromScriptHash = function(hash, network) { @@ -304,7 +318,7 @@ Address.fromScriptHash = function(hash, network) { * Instantiate an address from a Script * * @param {Script} script - An instance of Script - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @returns {Address} A new valid and frozen instance of an Address */ Address.fromScript = function(script, network) { @@ -316,7 +330,7 @@ Address.fromScript = function(script, network) { * Instantiate an address from a buffer of the address * * @param {Buffer} buffer - An instance of buffer of the address - * @param {String} [network] - The network: 'livenet' or 'testnet' + * @param {String|Network} [network] - either a Network instance, 'livenet', or 'testnet' * @param {String} [type] - The type of address: 'script' or 'pubkey' * @returns {Address} A new valid and frozen instance of an Address */ @@ -329,7 +343,7 @@ Address.fromBuffer = function(buffer, network, type) { * Instantiate an address from an address string * * @param {String} str - An string of the bitcoin address - * @param {String} [network] - The network: 'livenet' or 'testnet' + * @param {String|Network} [network] - either a Network instance, 'livenet', or 'testnet' * @param {String} [type] - The type of address: 'script' or 'pubkey' * @returns {Address} A new valid and frozen instance of an Address */ @@ -361,19 +375,19 @@ Address.fromJSON = function fromJSON(json) { * * @example * ```javascript - * - * var error = Address.getValidationError('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'testnet'); * // a network mismatch error + * var error = Address.getValidationError('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'testnet'); * ``` * * @param {String} data - The encoded data - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @param {String} type - The type of address: 'script' or 'pubkey' * @returns {null|Error} The corresponding error message */ Address.getValidationError = function(data, network, type) { var error; try { + /* jshint nonew: false */ new Address(data, network, type); } catch (e) { error = e; @@ -386,13 +400,11 @@ Address.getValidationError = function(data, network, type) { * * @example * ```javascript - * - * var valid = Address.isValid('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'livenet'); - * // true + * assert(Address.isValid('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'livenet')); * ``` * * @param {String} data - The encoded data - * @param {String} network - The network: 'livenet' or 'testnet' + * @param {String|Network} network - either a Network instance, 'livenet', or 'testnet' * @param {String} type - The type of address: 'script' or 'pubkey' * @returns {boolean} The corresponding error message */ @@ -428,7 +440,7 @@ Address.prototype.toBuffer = function() { }; /** - * @returns {Object} An object of the address + * @returns {Object} A plain object with the address information */ Address.prototype.toObject = function toObject() { return { @@ -439,7 +451,7 @@ Address.prototype.toObject = function toObject() { }; /** - * @returns {Object} An object of the address + * @returns {String} A JSON representation of a plain object with the address information */ Address.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); diff --git a/test/address.js b/test/address.js index 7b6696c..0e8a1fb 100644 --- a/test/address.js +++ b/test/address.js @@ -21,7 +21,7 @@ describe('Address', function() { var buf = Buffer.concat([new Buffer([0]), pubkeyhash]); var str = '16VZnHwRhwrExfeHFHGjwrgEMq8VcYPs9r'; - it('should throw an error because of missing data', function() { + it('can\'t build without data', function() { (function() { return new Address(); }).should.throw('First argument is required, please include address data.'); @@ -116,57 +116,57 @@ describe('Address', function() { describe('validation', function() { - it('should describe this livenet address as an invalid testnet address', function() { + it('getValidationError detects network mismatchs', function() { var error = Address.getValidationError('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'testnet'); should.exist(error); }); - it('should should return a true boolean', function(){ + it('isValid returns true on a valid address', function(){ var valid = Address.isValid('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'livenet'); valid.should.equal(true); }); - it('should should return a false boolean', function(){ + it('isValid returns false on network mismatch', function(){ var valid = Address.isValid('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'testnet'); valid.should.equal(false); }); - it('should validate addresses', function() { + it('validates correctly the P2PKH test vector', function() { for (var i = 0; i < PKHLivenet.length; i++) { var error = Address.getValidationError(PKHLivenet[i]); should.not.exist(error); } }); - it('should validate p2sh addresses', function() { + it('validates correctly the P2SH test vector', function() { for (var i = 0; i < P2SHLivenet.length; i++) { var error = Address.getValidationError(P2SHLivenet[i]); should.not.exist(error); } }); - it('should validate testnet p2sh addresses', function() { + it('validates correctly the P2SH testnet test vector', function() { for (var i = 0; i < P2SHTestnet.length; i++) { var error = Address.getValidationError(P2SHTestnet[i], 'testnet'); should.not.exist(error); } }); - it('should not validate addresses with params', function() { + it('rejects correctly the P2PKH livenet test vector with "testnet" parameter', function() { for (var i = 0; i < PKHLivenet.length; i++) { var error = Address.getValidationError(PKHLivenet[i], 'testnet'); should.exist(error); } }); - it('should validate addresses with params', function() { + it('validates correctly the P2PKH livenet test vector with "livenet" parameter', function() { for (var i = 0; i < PKHLivenet.length; i++) { var error = Address.getValidationError(PKHLivenet[i], 'livenet'); should.not.exist(error); } }); - it('should not validate because of an invalid checksum', function() { + it('should not validate if checksum is invalid', function() { for (var i = 0; i < badChecksums.length; i++) { var error = Address.getValidationError(badChecksums[i], 'livenet', 'pubkeyhash'); should.exist(error); @@ -174,16 +174,21 @@ describe('Address', function() { } }); - it('should not validate because of mismatched network', function() { - for (var i = 0; i < PKHLivenet.length; i++) { - var error = Address.getValidationError(PKHLivenet[i], 'testnet', 'pubkeyhash'); + it('should not validate on a network mismatch', function() { + var error, i; + for (i = 0; i < PKHLivenet.length; i++) { + error = Address.getValidationError(PKHLivenet[i], 'testnet', 'pubkeyhash'); + should.exist(error); + error.message.should.equal('Address has mismatched network type.'); + } + for (i = 0; i < PKHTestnet.length; i++) { + error = Address.getValidationError(PKHTestnet[i], 'livenet', 'pubkeyhash'); should.exist(error); error.message.should.equal('Address has mismatched network type.'); } - }); - it('should not validate because of a mismatched type', function() { + it('should not validate on a type mismatch', function() { for (var i = 0; i < PKHLivenet.length; i++) { var error = Address.getValidationError(PKHLivenet[i], 'livenet', 'scripthash'); should.exist(error); @@ -191,7 +196,7 @@ describe('Address', function() { } }); - it('should not validate because of non-base58 characters', function() { + it('should not validate on non-base58 characters', function() { for (var i = 0; i < nonBase58.length; i++) { var error = Address.getValidationError(nonBase58[i], 'livenet', 'pubkeyhash'); should.exist(error); @@ -199,28 +204,13 @@ describe('Address', function() { } }); - it('should not validate addresses', function() { - for (var i = 0; i < badChecksums.length; i++) { - var error = Address.getValidationError(badChecksums[i]); - should.exist(error); - } - }); - - it('should validate testnet addresses', function() { + it('testnet addresses are validated correctly', function() { for (var i = 0; i < PKHTestnet.length; i++) { var error = Address.getValidationError(PKHTestnet[i], 'testnet'); should.not.exist(error); } }); - it('should not validate testnet addresses because of mismatched network', function() { - for (var i = 0; i < PKHTestnet.length; i++) { - var error = Address.getValidationError(PKHTestnet[i], 'livenet', 'pubkeyhash'); - should.exist(error); - error.message.should.equal('Address has mismatched network type.'); - } - }); - }); describe('encodings', function() { @@ -228,7 +218,7 @@ describe('Address', function() { it('should make an address from a buffer', function() { Address.fromBuffer(buf).toString().should.equal(str); new Address(buf).toString().should.equal(str); - Address(buf).toString().should.equal(str); + new Address(buf).toString().should.equal(str); }); it('should make an address from a string', function() { @@ -316,13 +306,14 @@ describe('Address', function() { }); it('should make this address from a compressed pubkey', function() { - var pubkey = PublicKey.fromDER(new Buffer('0285e9737a74c30a873f74df05124f2aa6f53042c2fc0a130d6cbd7d16b944b004', 'hex')); + var pubkey = new PublicKey('0285e9737a74c30a873f74df05124f2aa6f53042c2fc0a130d6cbd7d16b944b004'); var address = Address.fromPublicKey(pubkey); address.toString().should.equal('19gH5uhqY6DKrtkU66PsZPUZdzTd11Y7ke'); }); it('should make this address from an uncompressed pubkey', function() { - var pubkey = PublicKey.fromDER(new Buffer('0485e9737a74c30a873f74df05124f2aa6f53042c2fc0a130d6cbd7d16b944b004833fef26c8be4c4823754869ff4e46755b85d851077771c220e2610496a29d98', 'hex')); + var pubkey = new PublicKey('0485e9737a74c30a873f74df05124f2aa6f53042c2fc0a130d6cbd7d16b944b00' + + '4833fef26c8be4c4823754869ff4e46755b85d851077771c220e2610496a29d98'); var a = Address.fromPublicKey(pubkey, 'livenet'); a.toString().should.equal('16JXnhxjJUhxfyx4y6H4sFcxrgt8kQ8ewX'); var b = new Address(pubkey, 'livenet', 'pubkeyhash'); @@ -379,7 +370,7 @@ describe('Address', function() { describe('#toBuffer', function() { - it('should output this known hash', function() { + it('3c3fa3d4adcaf8f52d5b1843975e122548269937 corresponds to hash 16VZnHwRhwrExfeHFHGjwrgEMq8VcYPs9r', function() { var address = new Address(str); address.toBuffer().slice(1).toString('hex').should.equal(pubkeyhash.toString('hex')); }); @@ -388,37 +379,37 @@ describe('Address', function() { describe('#json', function() { - it('should output/input a JSON object', function() { - var address = Address.fromJSON(new Address(str).toJSON()); - address.toString().should.equal(str); - }); - - it('should output/input a JSON string', function() { + it('roundtrip to-from-to', function() { var json = new Address(str).toJSON(); var address = Address.fromJSON(json); address.toString().should.equal(str); }); + it('checks that the string parameter is valid JSON', function() { + expect(function() { + return Address.fromJSON('ยน'); + }).to.throw(); + }); }); describe('#toString', function() { - it('should output a livenet pubkeyhash address', function() { + it('livenet pubkeyhash address', function() { var address = new Address(str); address.toString().should.equal(str); }); - it('should output a scripthash address', function() { + it('scripthash address', function() { var address = new Address(P2SHLivenet[0]); address.toString().should.equal(P2SHLivenet[0]); }); - it('should output a testnet scripthash address', function() { + it('testnet scripthash address', function() { var address = new Address(P2SHTestnet[0]); address.toString().should.equal(P2SHTestnet[0]); }); - it('should output a testnet pubkeyhash address', function() { + it('testnet pubkeyhash address', function() { var address = new Address(PKHTestnet[0]); address.toString().should.equal(PKHTestnet[0]); });