diff --git a/lib/address.js b/lib/address.js index 39b993d..51bf621 100644 --- a/lib/address.js +++ b/lib/address.js @@ -4,117 +4,341 @@ var base58check = require('./encoding/base58check'); var networks = require('./networks'); var Hash = require('./crypto/hash'); -function Address(buf) { - if (!(this instanceof Address)) - return new Address(buf); - if (Buffer.isBuffer(buf)) { - this.fromBuffer(buf); - } else if (typeof buf === 'string') { - var str = buf; - this.fromString(str); - } else if (buf) { - var obj = buf; - this.set(obj); +/** + * + * Bitcore Address + * + * Instantiate an address from an address String or Buffer, a public key or script hash Buffer, + * or an instance of Pubkey or Script. + * + * @example + * + * var address = new Address(keypair.pubkey, 'testnet').toString(); + * + * @param {String} data - The encoded data in various formats + * @param {String} [network] - The network: 'mainnet' or 'testnet' + * @param {String} [type] - The type of address: 'script' or 'pubkey' + * @returns {Address} A new valid and frozen instance of an Address + */ +function Address(data, network, type) { + + if (!data) { + throw Error('Please include address data'); } -} -Address.prototype.set = function(obj) { - this.hashbuf = obj.hashbuf || this.hashbuf || null; - this.networkstr = obj.networkstr || this.networkstr || 'mainnet'; - this.typestr = obj.typestr || this.typestr || 'pubkeyhash'; - return this; -}; + if (network && (network !== 'mainnet' && network !== 'testnet')) { + throw new Error('Network must be "mainnet" or "testnet"'); + } -Address.prototype.fromBuffer = function(buf) { - if (buf.length !== 1 + 20) - throw new Error('Address buffers must be exactly 21 bytes'); - var version = buf[0]; - if (version === networks['mainnet']['pubkeyhash']) { - this.networkstr = 'mainnet'; - this.typestr = 'pubkeyhash'; - } else if (version === networks['mainnet']['scripthash']) { - this.networkstr = 'mainnet'; - this.typestr = 'scripthash'; - } else if (version === networks['testnet']['pubkeyhash']) { - this.networkstr = 'testnet'; - this.typestr = 'pubkeyhash'; - } else if (version === networks['testnet']['scripthash']) { - this.networkstr = 'testnet'; - this.typestr = 'scripthash'; + if (type && (type !== 'pubkeyhash' && type !== 'scripthash')) { + throw new Error('Type must be "pubkeyhash" or "scripthash"'); + } + + var info; + + // transform and validate input data + if (data instanceof Buffer && data.length === 20) { + info = Address._transformHash(data); + } else if (data instanceof Buffer && data.length === 21) { + info = Address._transformBuffer(data, network, type); + } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Pubkey')) { + info = Address._transformPubkey(data); + } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) { + info = Address._transformScript(data); + } else if (typeof(data) === 'string') { + info = Address._transformString(data, network, type); } else { - this.networkstr = 'unknown'; - this.typestr = 'unknown'; + throw new Error('Unrecognized data format'); } - this.hashbuf = buf.slice(1); + // set defaults is not set + info.network = info.network || network || 'mainnet'; + info.type = info.type || type || 'pubkeyhash'; + + // set the validated values + this.hashBuffer = info.hashBuffer; + this.network = info.network; + this.type = info.type; return this; -}; -Address.prototype.fromHashbuf = function(hashbuf, networkstr, typestr) { - if (hashbuf.length !== 20) - throw new Error('hashbuf must be exactly 20 bytes'); - this.hashbuf = hashbuf; - this.networkstr = networkstr || 'mainnet'; - this.typestr = typestr || 'pubkeyhash'; - return this; -}; - -Address.prototype.fromPubkey = function(pubkey, networkstr) { - this.hashbuf = Hash.sha256ripemd160(pubkey.toBuffer()); - this.networkstr = networkstr || 'mainnet'; - this.typestr = 'pubkeyhash'; - return this; -}; - -Address.prototype.fromScript = function(script, networkstr) { - this.hashbuf = Hash.sha256ripemd160(script.toBuffer()); - this.networkstr = networkstr || 'mainnet'; - this.typestr = 'scripthash'; - return this; -}; - -Address.prototype.fromString = function(str) { - var buf = base58check.decode(str); - return this.fromBuffer(buf); } -Address.isValid = function(addrstr) { - try { - var address = new Address().fromString(addrstr); - } catch (e) { - return false; +/** + * + * Internal function to transform a hash buffer + * + * @param {Buffer} hash - An instance of a hash Buffer + * @returns {Object} An object with keys: hashBuffer + */ +Address._transformHash = function(hash){ + var info = {}; + if (!hash instanceof Buffer) { + throw new Error('Address supplied is not a buffer'); } - return address.isValid(); + if (hash.length !== 20) { + throw new Error('Address hashbuffers must be exactly a 20 bytes'); + } + info.hashBuffer = hash; + return info; +} + +/** + * + * Internal function to transform a bitcoin address buffer + * + * @param {Buffer} buffer - An instance of a hex encoded address Buffer + * @param {String} [network] - The network: 'mainnet' or 'testnet' + * @param {String} [type] - The type: 'pubkeyhash' or 'scripthash' + * @returns {Object} An object with keys: hashBuffer, network and type + */ +Address._transformBuffer = function(buffer, network, type){ + var info = {}; + if (!buffer instanceof Buffer) { + throw new Error('Address supplied is not a buffer'); + } + if (buffer.length !== 1 + 20) { + throw new Error('Address buffers must be exactly 21 bytes'); + } + + var bufNetwork = false; + var bufType = false; + + switch(buffer[0]){ // the version byte + case networks.mainnet.pubkeyhash: + bufNetwork = 'mainnet'; + bufType = 'pubkeyhash'; + break; + + case networks.mainnet.scripthash: + bufNetwork = 'mainnet'; + bufType = 'scripthash'; + break; + + case networks.testnet.pubkeyhash: + bufNetwork = 'testnet'; + bufType = 'pubkeyhash'; + break; + + case networks.testnet.scripthash: + bufNetwork = 'testnet'; + bufType = 'scripthash'; + break; + } + + if (!bufNetwork || (network && network !== bufNetwork)) { + throw new Error('Address has mismatched network type'); + } + + if (!bufType || ( type && type !== bufType )) { + throw new Error('Address has mismatched type'); + } + + info.hashBuffer = buffer.slice(1); + info.network = bufNetwork; + info.type = bufType; + return info; +} + +/** + * + * Internal function to transform a Pubkey + * + * @param {Pubkey} pubkey - An instance of Pubkey + * @returns {Object} An object with keys: hashBuffer, type + */ +Address._transformPubkey = function(pubkey){ + var info = {}; + if (!pubkey.constructor || (pubkey.constructor.name && pubkey.constructor.name !== 'Pubkey')) { + throw new Error('Address must be an instance of Pubkey'); + } + info.hashBuffer = Hash.sha256ripemd160(pubkey.toBuffer()); + info.type = 'pubkeyhash'; + return info; +} + +/** + * + * Internal function to transform a Script + * + * @param {Script} script - An instance of Script + * @returns {Object} An object with keys: hashBuffer, type + */ +Address._transformScript = function(script){ + var info = {}; + if (!script.constructor || (script.constructor.name && script.constructor.name !== 'Script')) { + throw new Error('Address must be an instance of Script'); + } + info.hashBuffer = Hash.sha256ripemd160(script.toBuffer()); + info.type = 'scripthash'; + return info; +} + +/** + * + * Internal function to transform a bitcoin address string + * + * @param {String} data - An instance of Pubkey + * @param {String} [network] - The network: 'mainnet' or 'testnet' + * @param {String} [type] - The type: 'pubkeyhash' or 'scripthash' + * @returns {Object} An object with keys: hashBuffer, network and type + */ +Address._transformString = function(data, network, type){ + if( typeof(data) !== 'string' ) { + throw Error('Address supplied is not a string'); + } + var addressBuffer = base58check.decode(data); + var info = Address._transformBuffer(addressBuffer, network, type); + return info; +} + +/** + * + * Instantiate an address from a Pubkey instance + * + * @param {String} data - An instance of Pubkey + * @param {String} network - The network: 'mainnet' or 'testnet' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.fromPubkey = function(data, network){ + var info = Address._transformPubkey(data); + return new Address(info.hashBuffer, network, info.type); }; -Address.prototype.isValid = function() { +/** + * + * Instantiate an address from a ripemd160 public key hash + * + * @param {Buffer} hash - An instance of buffer of the hash + * @param {String} network - The network: 'mainnet' or 'testnet' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.fromPubkeyHash = function(hash, network) { + var info = Address._transformHash(hash); + return new Address(info.hashBuffer, network, 'pubkeyhash'); +}; + +/** + * + * Instantiate an address from a ripemd160 script hash + * + * @param {Buffer} hash - An instance of buffer of the hash + * @param {String} network - The network: 'mainnet' or 'testnet' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.fromScriptHash = function(hash, network) { + var info = Address._transformHash(hash); + return new Address(info.hashBuffer, network, 'scripthash'); +}; + +/** + * + * Instantiate an address from a Script + * + * @param {Script} script - An instance of Script + * @param {String} network - The network: 'mainnet' or 'testnet' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.fromScript = function(script, network) { + var info = Address._transformScript(script); + return new Address(info.hashBuffer, network, info.type); +}; + +/** + * + * Instantiate an address from a buffer of the address + * + * @param {Buffer} buffer - An instance of buffer of the address + * @param {String} [network] - The network: 'mainnet' or 'testnet' + * @param {String} [type] - The type of address: 'script' or 'pubkey' + * @returns {Address} A new valid and frozen instance of an Address + */ +Address.fromBuffer = function(buffer, network, type) { + var info = Address._transformBuffer(buffer, network, type); + return new Address(info.hashBuffer, info.network, info.type); +}; + +/** + * + * Instantiate an address from an address string + * + * @param {String} str - An string of the bitcoin address + * @param {String} [network] - The network: 'mainnet' or 'testnet' + * @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); + return new Address(info.hashBuffer, info.network, info.type); +}; + +/** + * + * Will return a validation error if exists + * + * @example + * + * var error = Address.getValidationError('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'testnet'); + * // a network mismatch error + * + * @param {String} data - The encoded data + * @param {String} network - The network: 'mainnet' 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 { - this.validate(); + new Address(data, network, type); + } catch (e) { + error = e; + } + return error; +}; + +/** + * + * Will return a boolean if an address is valid + * + * @example + * + * var valid = Address.isValid('15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', 'mainnet'); + * // true + * + * @param {String} data - The encoded data + * @param {String} network - The network: 'mainnet' or 'testnet' + * @param {String} type - The type of address: 'script' or 'pubkey' + * @returns {null|Error} The corresponding error message + */ +Address.isValid = function(data, network, type) { + var error = Address.getValidationError(data, network, type); + if (error) { + return false; + } else { return true; - } catch (e) { - return false; } }; +/** + * + * Will return a buffer representation of the address + * + * @returns {Buffer} Bitcoin address buffer + */ Address.prototype.toBuffer = function() { - var version = new Buffer([networks[this.networkstr][this.typestr]]); - var buf = Buffer.concat([version, this.hashbuf]); + var version = new Buffer([networks[this.network][this.type]]); + var buf = Buffer.concat([version, this.hashBuffer]); return buf; }; +/** + * + * Will return a the string representation of the address + * + * @returns {String} Bitcoin address + */ Address.prototype.toString = function() { return base58check.encode(this.toBuffer()); }; -Address.prototype.validate = function() { - if (!Buffer.isBuffer(this.hashbuf) || this.hashbuf.length !== 20) - throw new Error('hash must be a buffer of 20 bytes'); - if (this.networkstr !== 'mainnet' && this.networkstr !== 'testnet') - throw new Error('networkstr must be "mainnet" or "testnet"'); - if (this.typestr !== 'pubkeyhash' && this.typestr !== 'scripthash') - throw new Error('typestr must be "pubkeyhash" or "scripthash"'); - return this; -}; - module.exports = Address; diff --git a/test/address.js b/test/address.js index 7eecc10..66b3324 100644 --- a/test/address.js +++ b/test/address.js @@ -8,149 +8,267 @@ var Address = bitcore.Address; var Script = bitcore.Script; describe('Address', function() { + var pubkeyhash = new Buffer('3c3fa3d4adcaf8f52d5b1843975e122548269937', 'hex'); var buf = Buffer.concat([new Buffer([0]), pubkeyhash]); var str = '16VZnHwRhwrExfeHFHGjwrgEMq8VcYPs9r'; + var strTest = 'n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X'; - it('should create a new address object', function() { - var address = new Address(); - should.exist(address); - address = new Address(buf); - should.exist(address); - address = new Address(str); - should.exist(address); + it('should throw an error because of missing data', function() { + (function() { + var a = new Address(); + }).should.throw('Please include address data'); }); - describe('@isValid', function() { + it('should throw an error because of bad network param', function() { + (function(){ + var a = new Address(validAddresses[0], 'main', 'pubkeyhash'); + }).should.throw('Network must be "mainnet" or "testnet"'); + }); - it('should validate this valid address string', function() { - Address.isValid(str).should.equal(true); + it('should throw an error because of bad type param', function() { + (function() { + var a = new Address(validAddresses[0], 'mainnet', 'pubkey'); + }).should.throw('Type must be "pubkeyhash" or "scripthash"'); + }); + + + // mainnet valid + var validAddresses = [ + '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2', + '1A6ut1tWnUq1SEQLMr4ttDh24wcbJ5o9TT', + '1BpbpfLdY7oBS9gK7aDXgvMgr1DPvNhEB2', + '1Jz2yCRd5ST1p2gUqFB5wsSQfdm3jaFfg7' + ]; + + // mainnet p2sh + var validp2shAddresses = [ + '342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey', + '33vt8ViH5jsr115AGkW6cEmEz9MpvJSwDk', + '37Sp6Rv3y4kVd1nQ1JV5pfqXccHNyZm1x3', + '3QjYXhTkvuj8qPaXHTTWb5wjXhdsLAAWVy' + ]; + + // testnet p2sh + var testValidp2shAddresses = [ + '2N7FuwuUuoTBrDFdrAZ9KxBmtqMLxce9i1C', + '2NEWDzHWwY5ZZp8CQWbB7ouNMLqCia6YRda', + '2MxgPqX1iThW3oZVk9KoFcE5M4JpiETssVN', + '2NB72XtkjpnATMggui83aEtPawyyKvnbX2o' + ]; + + //mainnet bad checksums + var badChecksums = [ + '15vkcKf7gB23wLAnZLmbVuMiiVDc3nq4a2', + '1A6ut1tWnUq1SEQLMr4ttDh24wcbj4w2TT', + '1BpbpfLdY7oBS9gK7aDXgvMgr1DpvNH3B2', + '1Jz2yCRd5ST1p2gUqFB5wsSQfdmEJaffg7' + ]; + + //mainnet non-base58 + var nonBase58 = [ + '15vkcKf7g#23wLAnZLmb$uMiiVDc3nq4a2', + '1A601ttWnUq1SEQLMr4ttDh24wcbj4w2TT', + '1BpbpfLdY7oBS9gK7aIXgvMgr1DpvNH3B2', + '1Jz2yCRdOST1p2gUqFB5wsSQfdmEJaffg7' + ]; + + //testnet valid + var testValidAddresses = [ + 'n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X', + 'n45x3R2w2jaSC62BMa9MeJCd3TXxgvDEmm', + 'mursDVxqNQmmwWHACpM9VHwVVSfTddGsEM', + 'mtX8nPZZdJ8d3QNLRJ1oJTiEi26Sj6LQXS' + ]; + + describe('validation', function() { + + it('should describe this mainnet address as an invalid testnet address', function() { + var error = Address.getValidationError('37BahqRsFrAd3qLiNNwLNV3AWMRD7itxTo', 'testnet'); + should.exist(error); }); - it('should invalidate this valid address string', function() { - Address.isValid(str.substr(1)).should.equal(false); + it('should validate addresses', function() { + for(var i=0;i