diff --git a/lib/bcoin.js b/lib/bcoin.js index 18e83527..b3301b1c 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -27,6 +27,8 @@ bcoin.tx = require('./bcoin/tx'); bcoin.txPool = require('./bcoin/tx-pool'); bcoin.block = require('./bcoin/block'); bcoin.chain = require('./bcoin/chain'); +bcoin.keypair = require('./bcoin/keypair'); +bcoin.address = require('./bcoin/address'); bcoin.wallet = require('./bcoin/wallet'); bcoin.peer = require('./bcoin/peer'); bcoin.pool = require('./bcoin/pool'); diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js new file mode 100644 index 00000000..35d31771 --- /dev/null +++ b/lib/bcoin/address.js @@ -0,0 +1,431 @@ +/** + * address.js - address object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var hash = require('hash.js'); +var bn = require('bn.js'); +var inherits = require('inherits'); +var EventEmitter = require('events').EventEmitter; +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * Address + */ + +function Address(options) { + if (!(this instanceof Address)) + return new Address(options); + + EventEmitter.call(this); + + if (!options) + options = {}; + + this.options = options; + this.storage = options.storage; + this.label = options.label || ''; + this.change = !!options.change; + + this.key = bcoin.keypair({ + priv: options.priv, + pub: options.pub, + hd: options.hd, + key: options.key, + personalization: options.personalization, + entropy: options.entropy, + compressed: options.compressed + }); + + // Compatability + if (options.multisig) { + if (options.multisig.type) + options.type = options.multisig.type; + if (options.multisig.keys) + options.keys = options.multisig.keys; + if (options.multisig.m) + options.m = options.multisig.m; + if (options.multisig.n) + options.n = options.multisig.n; + } + + this.type = options.type || 'pubkeyhash'; + this.subtype = options.subtype; + this.keys = []; + this.m = options.m || 1; + this.n = options.n || 1; + this.redeem = null; + + if (this.n > 1) { + if (this.type !== 'multisig') + this.type = 'scripthash'; + if (this.type === 'scripthash') + this.subtype = 'multisig'; + } + + if (network.prefixes[this.type] == null) + throw new Error('Unknown prefix: ' + this.type); + + this.nmax = this.type === 'scripthash' + ? (this.key.compressed ? 15 : 7) + : 3; + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (this.n < 1 || this.n > this.nmax) + throw new Error('n ranges between 1 and ' + this.nmax); + + this.addKey(this.getPublicKey()); + + (options.keys || []).forEach(function(key) { + this.addKey(key); + }, this); + + if (options.redeem) + this.setRedeem(options.redeem); + + this.prefix = 'bt/address/' + this.getKeyAddress() + '/'; +} + +inherits(Address, EventEmitter); + +Address.prototype.setRedeem = function setRedeem(redeem) { + var old = this.getScriptAddress(); + + if (!utils.isBytes(redeem)) + redeem = bcoin.script.encode(redeem); + + this.type = 'scripthash'; + this.subtype = null; + this.redeem = redeem; + this.emit('scriptaddress', old, this.getScriptAddress()); +}; + +Address.prototype.addKey = function addKey(key) { + var old = this.getScriptAddress(); + + key = utils.toBuffer(key); + + var has = this.keys.some(function(k) { + return utils.isEqual(k, key); + }); + + if (has) + return; + + this.keys.push(key); + + this.keys = utils.sortKeys(this.keys); + this.emit('scriptaddress', old, this.getScriptAddress()); +}; + +Address.prototype.removeKey = function removeKey(key) { + var old = this.getScriptAddress(); + + key = utils.toBuffer(key); + + var index = this.keys.map(function(key, i) { + return utils.isEqual(key, pub) ? i : null; + }).filter(function(i) { + return i !== null; + })[0]; + + if (index == null) + return; + + this.keys.splice(index, 1); + + this.keys = utils.sortKeys(this.keys); + this.emit('scriptaddress', old, this.getScriptAddress()); +}; + +Address.prototype.getPrivateKey = function getPrivateKey(enc) { + return this.key.getPrivate(enc); +}; + +Address.toSecret = function toSecret(priv, compressed) { + return bcoin.keypair.toSecret(priv, compressed); +}; + +Address.fromSecret = function fromSecret(priv) { + return bcoin.keypair.fromSecret(priv); +}; + +Address.prototype.getScript = function getScript() { + if (this.type !== 'scripthash') + return; + + if (this.redeem) + return this.redeem.slice(); + + if (this.subtype === 'pubkey') + return bcoin.script.encode([this.getPublicKey(), 'checksig']); + + if (this.subtype === 'pubkeyhash' || this.keys.length < this.n) { + return bcoin.script.encode([ + 'dup', + 'hash160', + this.getKeyHash(), + 'equalverify', + 'checksig' + ]); + } + + return bcoin.script.encode( + bcoin.script.createMultisig(this.keys, this.m, this.n) + ); +}; + +Address.prototype.getScriptHash = function getScriptHash() { + if (this.type !== 'scripthash') + return; + + return utils.ripesha(this.getScript()); +}; + +Address.prototype.getScriptAddress = function getScriptAddress() { + if (this.type !== 'scripthash') + return; + + return Address.hash2addr(this.getScriptHash(), this.type); +}; + +Address.prototype.getPublicKey = function getPublicKey(enc) { + return this.key.getPublic(enc); +}; + +Address.prototype.getKeyHash = function getKeyHash() { + return Address.key2hash(this.getPublicKey()); +}; + +Address.prototype.getKeyAddress = function getKeyAddress() { + return Address.hash2addr(this.getKeyHash(), 'pubkeyhash'); +}; + +Address.prototype.getHash = function getHash() { + if (this.type === 'scripthash') + return this.getScriptHash(); + return this.getKeyHash(); +}; + +Address.prototype.getAddress = function getAddress() { + if (this.type === 'scripthash') + return this.getScriptAddress(); + return this.getKeyAddress(); +}; + +Address.key2hash = function key2hash(key) { + key = utils.toBuffer(key); + return utils.ripesha(key); +}; + +Address.hash2addr = function hash2addr(hash, prefix) { + var addr; + + hash = utils.toArray(hash, 'hex'); + + prefix = network.prefixes[prefix || 'pubkeyhash']; + hash = [ prefix ].concat(hash); + + addr = hash.concat(utils.checksum(hash)); + + return utils.toBase58(addr); +}; + +Address.__defineGetter__('prefixes', function() { + if (Address._prefixes) + return Address._prefixes; + + Address._prefixes = ['pubkeyhash', 'scripthash'].reduce(function(out, prefix) { + var ch = Address.hash2addr(Address.key2hash([]), prefix)[0]; + out[ch] = prefix; + return out; + }, {}); + + return Address._prefixes; +}); + +Address.addr2hash = function addr2hash(addr, prefix) { + var chk; + + if (prefix == null && typeof addr === 'string') + prefix = Address.prefixes[addr[0]]; + + if (!utils.isBuffer(addr)) + addr = utils.fromBase58(addr); + + prefix = network.prefixes[prefix || 'pubkeyhash']; + + if (addr.length !== 25) + return []; + + if (addr[0] !== prefix) + return []; + + chk = utils.checksum(addr.slice(0, -4)); + + if (utils.readU32(chk, 0) !== utils.readU32(addr, 21)) + return []; + + return addr.slice(1, -4); +}; + +Address.validate = function validateAddress(addr, prefix) { + if (!addr || typeof addr !== 'string') + return false; + + var p = Address.addr2hash(addr, prefix); + + return p.length !== 0; +}; + +Address.validateAddress = Address.validate; + +Address.prototype.ownOutput = function ownOutput(tx, index) { + var scripthash = this.getScriptHash(); + var hash = this.getKeyHash(); + var key = this.getPublicKey(); + var keys = this.keys; + + var outputs = tx.outputs.filter(function(output, i) { + var s = output.script; + + if (index != null && index !== i) + return false; + + if (bcoin.script.isPubkey(s, key)) + return true; + + if (bcoin.script.isPubkeyhash(s, hash)) + return true; + + if (bcoin.script.isMultisig(s, keys)) + return true; + + if (scripthash) { + if (bcoin.script.isScripthash(s, scripthash)) + return true; + } + + return false; + }, this); + + if (outputs.length === 0) + return false; + + return outputs; +}; + +Address.prototype.ownInput = function ownInput(tx, index) { + var scripthash = this.getScriptHash(); + var hash = this.getKeyHash(); + var key = this.getPublicKey(); + var redeem = this.getScript(); + var keys = this.keys; + + var inputs = tx.inputs.filter(function(input, i) { + var s; + + if (!input.prevout.tx && this.tx._all[input.prevout.hash]) + input.prevout.tx = this.tx._all[input.prevout.hash]; + + if (index != null && index !== i) + return false; + + // if (bcoin.script.isPubkeyInput(input.script, key, tx, i)) + // return true; + + if (bcoin.script.isPubkeyhashInput(input.script, key)) + return true; + + // if (bcoin.script.isMultisigInput(input.script, keys, tx, i)) + // return true; + + if (redeem) { + if (bcoin.script.isScripthashInput(input.script, redeem)) + return true; + } + + if (!input.prevout.tx) + return false; + + s = input.prevout.tx.getSubscript(input.prevout.index); + + if (bcoin.script.isPubkey(s, key)) + return true; + + if (bcoin.script.isPubkeyhash(s, hash)) + return true; + + if (bcoin.script.isMultisig(s, keys)) + return true; + + if (scripthash) { + if (bcoin.script.isScripthash(s, scripthash)) + return true; + } + + return false; + }, this); + + if (inputs.length === 0) + return false; + + return inputs; +}; + +Address.prototype.toJSON = function toJSON(encrypt) { + return { + v: 1, + name: 'address', + network: network.type, + label: this.label, + change: this.change, + address: this.getKeyAddress(), + scriptaddress: this.getScriptAddress(), + key: this.key.toJSON(encrypt), + type: this.type, + subtype: this.subtype, + redeem: this.redeem ? utils.toHex(this.redeem) : null, + keys: this.keys.map(utils.toBase58), + m: this.m, + n: this.n + }; +}; + +Address.fromJSON = function fromJSON(json, decrypt) { + var priv, pub, xprivkey, multisig, compressed, key, w; + + assert.equal(json.v, 1); + assert.equal(json.name, 'address'); + + if (json.network) + assert.equal(json.network, network.type); + + w = new Address({ + label: json.label, + change: json.change, + key: bcoin.keypair.fromJSON(json.key, decrypt), + multisig: multisig, + type: json.type, + subtype: json.subtype, + redeem: json.redeem ? utils.toArray(json.redeem, 'hex') : null, + keys: json.keys.map(utils.fromBase58), + m: json.m, + n: json.n + }); + + return w; +}; + +/** + * Expose + */ + +module.exports = Address; diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index b0e6a73d..72f4816b 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -126,9 +126,12 @@ function HDPrivateKey(options) { if (!options) options = { seed: bcoin.hd.seed() }; - if (typeof options === 'string' && options.indexOf('xprv') === 0) + if (HDPrivateKey.isExtended(options)) options = { xkey: options }; + if (HDPublicKey.isExtended(options)) + return new HDPublicKey(options); + if (options.passphrase !== undefined || options.bits || options.entropy @@ -157,8 +160,171 @@ function HDPrivateKey(options) { this.data = data; this._build(data); + + if (new bn(data.parentFingerPrint).cmpn(0) === 0) { + this.isMaster = true; + this.master = this; + } else { + this.master = options.master; + } + + this.purpose = options.purpose != null + ? options.purpose + : 44; + + this.coinType = options.coinType != null + ? options.coinType + : (network.type === 'main' ? 0 : 1); + + this.accountIndex = options.accountIndex != null + ? options.accountIndex + : 0; + + this.isChange = options.isChange != null + ? options.isChange + : false; + + // this.addressIndex = options.addressIndex != null + // ? options.addressIndex + // : 0; + + this.isPrivate = true; } +HDPrivateKey.prototype.scan = function scan(txs) { + var keys = []; + + assert(this.isMaster); + + // 0. get the root node + var root = master + .derive(44, true) + .derive(network.type === 'main' ? 0 : 1, true); + + return (function scanner(accountIndex) { + var account, chain, addressIndex, address, addr, i; + var gap = 0; + + // 1. derive the first account's node (index = 0) + account = root.derive(accountIndex, true); + + // 2. derive the external chain node of this account + chain = account.derive(0); + + // 3. scan addresses of the external chain; + // respect the gap limit described below + addressIndex = 0; +main: + for (;;) { + address = chain.derive(addressIndex++); + addr = bcoin.address.hash2addr( + bcoin.address.key2hash(address.publicKey), + 'pubkey'); + + for (i = 0; i < txs.length; i++) { + txs[i].fillPrevout(txs); + if (txs[i].testInputs(addr) || txs[i].testOutputs(addr)) { + keys.push(address); + gap = 0; + } else { + // 4. if no transactions are found on the + // external chain, stop discovery + if (++gap >= 20) + return keys; + } + } + } + + // 5. if there are some transactions, increase + // the account index and go to step 1 + return scanner(accountIndex + 1); + })(0); +}; + +HDPrivateKey.prototype.__defineGetter__('addressIndex', function() { + var index = this.childIndex; + assert(index < contants.hd.hardened); + return index; +}); + +HDPrivateKey.prototype._deriveBIP44 = function _deriveBIP44() { + var child; + + assert(this.isMaster); + + if (options.purpose == null) + options.purpose = 44; + + if (options.coinType == null) + options.coinType = network.type === 'main' ? 0 : 1; + + child = this + .derive(options.purpose, true) + .derive(options.coinType, true) + .derive(options.accountIndex, true) + .derive(options.isChange ? 1 : 0) + .derive(options.addressIndex); + + child.purpose = options.purpose; + child.coinType = options.coinType; + child.accountIndex = options.accountIndex; + child.isChange = options.isChange; + child.addressIndex = options.addressIndex; + + return child; +}; + +HDPrivateKey.prototype.deriveAccount = function deriveAccount(accountIndex) { + return this._deriveBIP44({ + purpose: this.purpose, + coinType: this.coinType, + accountIndex: accountIndex != null ? accountIndex : this.accountIndex + 1, + isChange: false, + addressIndex: 0 + }); +}; + +HDPrivateKey.prototype.deriveChange = function deriveChange(addressIndex, accountIndex) { + return this._deriveBIP44({ + purpose: this.purpose, + coinType: this.coinType, + accountIndex: accountIndex != null ? accountIndex : this.accountIndex, + isChange: true, + addressIndex: addressIndex != null ? addressIndex : this.addressIndex + 1 + }); +}; + +HDPrivateKey.prototype.deriveAddress = function deriveAddress(addressIndex, accountIndex) { + return this._deriveBIP44({ + purpose: this.purpose, + coinType: this.coinType, + accountIndex: accountIndex != null ? accountIndex : this.accountIndex, + isChange: false, + addressIndex: addressIndex != null ? addressIndex : this.addressIndex + 1 + }); +}; + +HDPrivateKey.prototype.toPath = function toPath(data) { + assert(!this.isMaster); + return HDPrivateKey.getPath( + this.accountIndex, this.addressIndex, this.isChange); +}; + +HDPrivateKey.getPath = function toPath(accountIndex, addressIndex, isChange) { + return 'm/44\'/' + + (network.type === 'main' ? '0' : '1') + '\'' + '/' + + accountIndex + '\'/' + + (isChange ? '1' : '0') + '/' + + addressIndex; +}; + +HDPrivateKey.isExtended = function isExtended(data) { + if (typeof data !== 'string') + return false; + + return data.indexOf('xprv') === 0 || data.indexOf('tprv') === 0; +}; + HDPrivateKey.prototype._normalize = function _normalize(data, version) { data.version = version || network.prefixes.xprivkey; data.privateKey = data.privateKey || data.priv; @@ -350,7 +516,8 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { childIndex: index, chainCode: chainCode, privateKey: privateKey, - checksum: null + checksum: null, + master: this.master }); }; @@ -378,6 +545,19 @@ HDPrivateKey._getIndexes = function _getIndexes(path) { return null; index = +step; + + if (i === 0) { + indexes.purpose = index; + } else if (i === 1) { + indexes.coinType = index; + } else if (i === 2) { + indexes.accountIndex = index; + } else if (i === 3) { + indexes.isChange = index === 1; + } else if (i === 4) { + indexes.addressIndex = index; + } + if (hardened) index += constants.hd.hardened; @@ -388,15 +568,16 @@ HDPrivateKey._getIndexes = function _getIndexes(path) { }; HDPrivateKey.isValidPath = function isValidPath(path, hardened) { + var indexes; + if (typeof path === 'string') { - var indexes = HDPrivateKey._getIndexes(path); + indexes = HDPrivateKey._getIndexes(path); return indexes !== null && indexes.every(HDPrivateKey.isValidPath); } if (typeof path === 'number') { - if (path < constants.hd.hardened && hardened) { + if (path < constants.hd.hardened && hardened) path += constants.hd.hardened; - } return path >= 0 && path < constants.hd.maxIndex; } @@ -404,14 +585,24 @@ HDPrivateKey.isValidPath = function isValidPath(path, hardened) { }; HDPrivateKey.prototype.deriveString = function deriveString(path) { + var indexes, child; + if (!HDPrivateKey.isValidPath(path)) throw new Error('invalid path'); - var indexes = HDPrivateKey._getIndexes(path); + indexes = HDPrivateKey._getIndexes(path); - return indexes.reduce(function(prev, index) { + child = indexes.reduce(function(prev, index, i) { return prev.derive(index); }, this); + + child.purpose = indexes.purpose; + child.coinType = indexes.coinType; + child.accountIndex = indexes.accountIndex; + child.isChange = indexes.isChange; + child.addressIndex = indexes.addressIndex; + + return child; }; /** @@ -427,7 +618,7 @@ function HDPublicKey(options) { if (!options) throw new Error('No options for HDPublicKey'); - if (typeof options === 'string' && options.indexOf('xpub') === 0) + if (HDPublicKey.isExtended(data)) options = { xkey: options }; data = options.xkey @@ -439,8 +630,56 @@ function HDPublicKey(options) { this.data = data; this._build(data); + + if (new bn(data.parentFingerPrint).cmpn(0) === 0) { + this.isMaster = true; + this.master = this; + } else { + this.master = options.master; + } + + this.purpose = options.purpose != null + ? options.purpose + : 44; + + this.coinType = options.coinType != null + ? options.coinType + : (network.type === 'main' ? 0 : 1); + + this.accountIndex = options.accountIndex != null + ? options.accountIndex + : 0; + + this.isChange = options.isChange != null + ? options.isChange + : false; + + // this.addressIndex = options.addressIndex != null + // ? options.addressIndex + // : 0; + + this.isPublic = true; } +HDPublicKey.prototype.__defineGetter__('addressIndex', function() { + var index = this.childIndex; + assert(index < contants.hd.hardened); + return index; +}); + +HDPublicKey.prototype._deriveBIP44 = HDPrivateKey.prototype._deriveBIP44; +HDPublicKey.prototype.deriveAccount = HDPrivateKey.prototype.deriveAccount; +HDPublicKey.prototype.deriveChange = HDPrivateKey.prototype.deriveChange; +HDPublicKey.prototype.deriveAddress = HDPrivateKey.prototype.deriveAddress; +HDPublicKey.prototype.toPath = HDPrivateKey.prototype.toPath; + +HDPublicKey.isExtended = function isExtended(data) { + if (typeof data !== 'string') + return false; + + return data.indexOf('xpub') === 0 || data.indexOf('tpub') === 0; +}; + HDPublicKey.prototype._normalize = HDPrivateKey.prototype._normalize; HDPublicKey.prototype._unbuild = function _unbuild(xkey) { @@ -546,7 +785,8 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { childIndex: index, chainCode: chainCode, publicKey: publicKey, - checksum: null + checksum: null, + master: this.master }); }; diff --git a/lib/bcoin/keypair.js b/lib/bcoin/keypair.js new file mode 100644 index 00000000..ba81013d --- /dev/null +++ b/lib/bcoin/keypair.js @@ -0,0 +1,270 @@ +/** + * keypair.js - keypair object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var hash = require('hash.js'); +var bn = require('bn.js'); +var inherits = require('inherits'); +var EventEmitter = require('events').EventEmitter; +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * KeyPair + */ + +function KeyPair(options) { + if (!(this instanceof KeyPair)) + return new KeyPair(options); + + if (!options) + options = {}; + + if (options instanceof KeyPair) + return options; + + if (options.key instanceof KeyPair) + return options.key; + + this.options = options; + this.key = options.key || null; + this.hd = options.hd || null; + this.compressed = options.compressed !== false; + + if (options.priv instanceof bcoin.hd.priv) { + this.hd = options.priv; + this.key = options.priv.pair; + } else if (options.pub instanceof bcoin.hd.pub) { + this.hd = options.pub; + this.key = options.pub.pair; + } else if (options.hd) { + this.hd = typeof options.hd === 'object' + ? bcoin.hd.priv(options.hd) + : bcoin.hd.priv(); + this.key = this.hd.pair; + } else if (options.key) { + if ((options.key instanceof bcoin.hd.priv) + || (options.key instanceof bcoin.hd.pub)) { + this.hd = options.key; + this.key = options.key.pair; + } else { + this.key = options.key; + } + } else if (options.priv || options.pub) { + this.key = bcoin.ecdsa.keyPair({ + priv: options.priv, + pub: options.pub + }); + } else { + this.key = bcoin.ecdsa.genKeyPair({ + pers: options.personalization, + entropy: options.entropy + || (options.passphrase ? utils.sha256(options.passphrase) : null) + }); + } +} + +KeyPair.prototype.__defineGetter__('priv', function() { + return this.key.getPrivate(); +}); + +KeyPair.prototype.__defineGetter__('pub', function() { + return this.key.getPublic(); +}); + +KeyPair.prototype.getPrivate = function getPrivate(enc) { + var priv = this.key.getPrivate(); + + if (!priv) + return; + + priv = priv.toArray(); + + if (enc === 'base58') + return KeyPair.toSecret(priv, this.compressed); + + if (enc === 'hex') + return utils.toHex(priv); + + return priv; +}; + +KeyPair.prototype.getPublic = function getPublic(enc) { + var pub = this.key.getPublic(this.compressed, 'array'); + + if (enc === 'base58') + return utils.toBase58(pub); + + if (enc === 'hex') + return utils.toHex(pub); + + return pub; +}; + +KeyPair.prototype.toSecret = function toSecret() { + return KeyPair.toSecret(this.getPrivate(), this.compressed); +}; + +KeyPair.toSecret = function toSecret(priv, compressed) { + var arr, chk; + + // We'll be using ncompressed public key as an address + arr = [network.prefixes.privkey]; + + // 0-pad key + while (arr.length + priv.length < 33) + arr.push(0); + + arr = arr.concat(priv); + + if (compressed) + arr.push(1); + + chk = utils.checksum(arr); + + return utils.toBase58(arr.concat(chk)); +}; + +KeyPair.fromSecret = function fromSecret(priv) { + var key, compressed; + + key = utils.fromBase58(priv); + assert(utils.isEqual(key.slice(-4), utils.checksum(key.slice(0, -4)))); + assert.equal(key[0], network.prefixes.privkey); + + key = key.slice(0, -4); + if (key.length === 34) { + assert.equal(key[33], 1); + priv = key.slice(1, -1); + compressed = true; + } else { + priv = key.slice(1); + compressed = false; + } + + return new KeyPair({ + priv: priv, + compressed: compressed + }); +}; + +KeyPair.prototype.toJSON = function toJSON(encrypt) { + var json = { + v: 1, + name: 'keypair', + encrypted: encrypt ? true : false + }; + + if (this.hd) { + if (this.hd.xprivkey) { + if (this.hd.seed) { + json.mnemonic = encrypt + ? encrypt(this.hd.seed.mnemonic) + : this.hd.seed.mnemonic; + json.passphrase = encrypt + ? encrypt(this.hd.seed.passphrase) + : this.hd.seed.passphrase; + return json; + } + json.xpriv = encrypt + ? encrypt(this.hd.xprivkey) + : this.hd.xprivkey; + return json; + } + + json.xpub = this.hd.xpubkey; + + return json; + } + + if (this.key.priv) { + json.priv = encrypt + ? encrypt(this.getPrivate('base58')) + : this.getPrivate('base58'); + return json; + } + + json.pub = this.getPublic('hex'); + return json; +}; + +KeyPair.fromJSON = function fromJSON(json, decrypt) { + var key, priv, pub, compressed, xprivkey; + var path = {}; + + assert.equal(json.v, 1); + assert.equal(json.name, 'keypair'); + + if (json.encrypted && !decrypt) + throw new Error('Cannot decrypt address'); + + if (json.mnemonic) { + return new KeyPair({ + key: bcoin.hd.priv({ + seed: bcoin.hd.seed({ + mnemonic: json.encrypted + ? decrypt(json.mnemonic) + : json.mnemonic, + passphrase: json.encrypted + ? decrypt(json.passphrase) + : json.passphrase + }) + }) + }); + } + + if (json.xpriv) { + xprivkey = json.xpriv; + if (json.encrypted) + xprivkey = decrypt(xprivkey); + return new KeyPair({ + key: bcoin.hd.priv({ + xkey: xprivkey + }) + }); + } + + if (json.xpub) { + return new KeyPair({ + key: bcoin.hd.pub({ + xkey: json.xpub + }) + }); + } + + if (json.priv) { + priv = json.priv; + if (json.encrypted) + priv = decrypt(priv); + + key = KeyPair.fromSecret(json.priv); + priv = key.priv; + compressed = key.compressed; + return new KeyPair({ + priv: priv, + compressed: compressed + }); + } + + if (json.pub) { + pub = bcoin.utils.toArray(json.pub, 'hex'); + compressed = pub[0] !== 0x04; + return new KeyPair({ + pub: pub, + compressed: compressed + }); + } + + assert(false); +}; + +/** + * Expose + */ + +module.exports = KeyPair; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index a1aaeced..6b02d561 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -921,6 +921,7 @@ Pool.prototype._removePeer = function _removePeer(peer) { }; Pool.prototype.watch = function watch(id) { + var self = this; var hid, i; if (id instanceof bcoin.wallet) { @@ -941,14 +942,25 @@ Pool.prototype.watch = function watch(id) { this.bloom.add(id, 'hex'); } - if (this.peers.load) - this.peers.load.updateWatch(); + // Send it to peers + if (this._pendingWatch) + return; - for (i = 0; i < this.peers.block.length; i++) - this.peers.block[i].updateWatch(); + this._pendingWatch = true; + + utils.nextTick(function() { + self._pendingWatch = false; + + if (self.peers.load) + self.peers.load.updateWatch(); + + for (i = 0; i < self.peers.block.length; i++) + self.peers.block[i].updateWatch(); + }); }; Pool.prototype.unwatch = function unwatch(id) { + var self = this; var i; id = utils.toHex(id); @@ -968,11 +980,20 @@ Pool.prototype.unwatch = function unwatch(id) { }, this); // Resend it to peers - if (this.peers.load) - this.peers.load.updateWatch(); + if (this._pendingWatch) + return; - for (i = 0; i < this.peers.block.length; i++) - this.peers.block[i].updateWatch(); + this._pendingWatch = true; + + utils.nextTick(function() { + self._pendingWatch = false; + + if (self.peers.load) + self.peers.load.updateWatch(); + + for (i = 0; i < self.peers.block.length; i++) + self.peers.block[i].updateWatch(); + }); }; // See "Filter matching algorithm": @@ -1084,32 +1105,58 @@ Pool.prototype.removeWallet = function removeWallet(w) { this.unwatchWallet(w); }; -Pool.prototype.watchWallet = function watchWallet(w) { - if (w.type === 'scripthash') { +Pool.prototype.watchAddress = function watchAddress(address) { + if (address.type === 'scripthash') { // For the redeem script hash in outputs: - this.watch(w.getScriptHash()); + this.watch(address.getScriptHash()); // For the redeem script in inputs: - this.watch(w.getScript()); + this.watch(address.getScript()); } // For the pubkey hash in outputs: - this.watch(w.getKeyHash()); + this.watch(address.getKeyHash()); // For the pubkey in inputs: - this.watch(w.getPublicKey()); + this.watch(address.getPublicKey()); }; -Pool.prototype.unwatchWallet = function unwatchWallet(w) { - if (w.type === 'scripthash') { +Pool.prototype.unwatchAddress = function unwatchAddress(address) { + if (address.type === 'scripthash') { // For the redeem script hash in p2sh outputs: - this.unwatch(w.getScriptHash()); + this.unwatch(address.getScriptHash()); // For the redeem script in p2sh inputs: - this.unwatch(w.getScript()); + this.unwatch(address.getScript()); } // For the pubkey hash in p2pk/multisig outputs: - this.unwatch(w.getKeyHash()); + this.unwatch(address.getKeyHash()); // For the pubkey in p2pkh inputs: - this.unwatch(w.getPublicKey()); + this.unwatch(address.getPublicKey()); +}; + +Pool.prototype.watchWallet = function watchWallet(w) { + var self = this; + + w.addresses.forEach(function(address) { + this.watchAddress(address); + }, this); + + w.on('add address', w._poolOnAdd = function(address) { + self.watchAddress(address); + }); + + w.on('remove address', w._poolOnRemove = function(address) { + self.unwatchAddress(address); + }); +}; + +Pool.prototype.unwatchWallet = function unwatchWallet(w) { + w.addresses.forEach(function(address) { + this.unwatchAddress(address); + }, this); + w.removeListener('add address', w._poolOnAdd); + w.removeListener('remove address', w._poolOnRemove); + delete w._poolOnAdd; + delete w._poolOnRemove; }; Pool.prototype.searchWallet = function(w) { diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 83f3aa63..956052e5 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -197,19 +197,20 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { input = this.inputs[index]; assert(input); + // Already has a script template (at least) + // if (input.script.length) + // return; + // We should have previous outputs by now. assert(input.prevout.tx); - // Already has a script template (at least) - if (input.script.length) - return; - // Get the previous output's subscript s = input.prevout.tx.getSubscript(input.prevout.index); // P2SH if (bcoin.script.isScripthash(s)) { - assert(redeem); + if (!redeem) + return false; s = bcoin.script.getSubscript(bcoin.script.decode(redeem)); } else { redeem = null; @@ -217,12 +218,34 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { if (bcoin.script.isPubkey(s)) { // P2PK + if (!utils.isEqual(s[0], pub)) + return false; + // Already has a script template (at least) + if (input.script.length) + return true; input.script = [[]]; } else if (bcoin.script.isPubkeyhash(s)) { // P2PKH + if (!utils.isEqual(s[2], bcoin.wallet.key2hash(pub))) + return false; + // Already has a script template (at least) + if (input.script.length) + return true; input.script = [[], pub]; } else if (bcoin.script.isMultisig(s)) { // Multisig + for (i = 0; i < s.length; i++) { + if (utils.isEqual(s[i], pub)) + break; + } + + if (i === s.length) + return false; + + // Already has a script template (at least) + if (input.script.length) + return true; + // Technically we should create m signature slots, // but we create n signature slots so we can order // the signatures properly. @@ -235,6 +258,18 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { for (i = 0; i < n; i++) input.script[i + 1] = []; } else { + for (i = 0; i < s.length; i++) { + if (utils.isEqual(s[i], pub)) + break; + } + + if (i === s.length) + return false; + + // Already has a script template (at least) + if (input.script.length) + return true; + // Likely a non-standard scripthash multisig // input. Determine n value by counting keys. // Also, only allow nonstandard types for @@ -256,6 +291,8 @@ TX.prototype.scriptInput = function scriptInput(index, pub, redeem) { // now that the redeem script is available. this._recalculateFee(); } + + return true; }; TX.prototype.createSignature = function createSignature(index, key, type) { @@ -501,10 +538,12 @@ TX.prototype.scriptSig = function scriptSig(index, key, pub, redeem, type) { assert(input); // Build script for input - this.scriptInput(index, pub, redeem); + if (!this.scriptInput(index, pub, redeem)) + return false; // Sign input - this.signInput(index, key, type); + if (!this.signInput(index, key, type)) + return false; return input.script; }; @@ -1131,6 +1170,111 @@ TX.prototype.getFunds = function getFunds(side) { // Legacy TX.prototype.funds = TX.prototype.getFunds; +TX.prototype.testInputs = function testInputs(addressTable, index, collect) { + var inputs = []; + var i, input, prev, data, j; + + if (typeof addressTable === 'string') + addressTable = [addressTable]; + + if (Array.isArray(addressTable)) { + addressTable = addressTable.reduce(function(address) { + out[address] = true; + return out; + }, {}); + } + + for (i = 0; i < this.inputs.length; i++) { + if (index != null && i !== index) + continue; + + input = this.inputs[i]; + + if (input.prevout.tx) + prev = input.prevout.tx.outputs[input.prevout.index].script; + else + prev = null; + + data = bcoin.script.getInputData(input.script, prev); + + if (data.addresses) { + for (j = 0; j < data.addresses.length; j++) { + if (addressTable[data.addresses[j]] != null) { + if (!collect) + return true; + inputs.push(input); + } + } + } + + if (data.scriptaddress) { + if (addressTable[data.scriptaddress] != null) { + if (!collect) + return true; + inputs.push(input); + } + } + } + + if (!collect) + return false; + + if (inputs.length === 0) + return false; + + return inputs; +}; + +TX.prototype.testOutputs = function testOutputs(addressTable, index, collect) { + var outputs = []; + var i, output, data, j; + + if (typeof addressTable === 'string') + addressTable = [addressTable]; + + if (Array.isArray(addressTable)) { + addressTable = addressTable.reduce(function(address) { + out[address] = true; + return out; + }, {}); + } + + for (i = 0; i < this.outputs.length; i++) { + if (index != null && i !== index) + continue; + + output = this.outputs[i]; + + data = bcoin.script.getOutputData(output.script); + + if (data.addresses) { + for (j = 0; j < data.addresses.length; j++) { + if (addressTable[data.addresses[j]] != null) { + if (!collect) + return true; + outputs.push(output); + } + } + } + + if (data.scriptaddress) { + if (addressTable[data.scriptaddress] != null) { + if (!collect) + return true; + outputs.push(output); + } + } + } + + if (!collect) + return false; + + if (outputs.length === 0) + return false; + + return outputs; +}; + TX.prototype.avoidFeeSnipping = function avoidFeeSnipping() { if (!this.chain) return; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 6f0513bf..5b565994 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -18,115 +18,36 @@ var network = bcoin.protocol.network; * Wallet */ -function Wallet(options, passphrase) { +function Wallet(options) { if (!(this instanceof Wallet)) - return new Wallet(options, passphrase); + return new Wallet(options); EventEmitter.call(this); - if (typeof options === 'string' && typeof passphrase === 'string') { - options = { - scope: options, - passphrase: passphrase - }; - } - if (!options) options = {}; this.options = options; - this.compressed = options.compressed !== false; + this.addresses = []; + this.master = null; + this._addressTable = {}; + + this.accountIndex = options.accountIndex || 0; + this.addressIndex = options.addressIndex || 0; + this.changeIndex = options.changeIndex || 0; + + if (options.addresses) { + options.addresses.forEach(function(address) { + this.addAddress(address); + }, this); + } else { + this.addAddress(options); + } + this.storage = options.storage; this.label = options.label || ''; - this.key = null; this.loaded = false; this.lastTs = 0; - this.changeAddress = options.changeAddress || null; - this.redeem = options.redeem || options.script; - - if (options.priv instanceof bcoin.hd.priv) { - this.hd = options.priv; - this.key = this.hd; - } else if (options.pub instanceof bcoin.hd.pub) { - this.hd = options.pub; - this.key = this.hd; - } else if (options.hd) { - this.hd = typeof options.hd === 'object' - ? bcoin.hd.priv(options.hd) - : bcoin.hd.priv(); - this.key = this.hd; - } else if (options.key) { - if ((options.key instanceof bcoin.hd.priv) - || (options.key instanceof bcoin.hd.pub)) { - this.hd = options.key; - this.key = options.key; - } else { - this.key = options.key; - } - } else if (options.passphrase) { - this.key = bcoin.ecdsa.genKeyPair({ - pers: options.scope, - entropy: hash.sha256().update(options.passphrase).digest() - }); - } else if (options.priv || options.pub) { - this.key = bcoin.ecdsa.keyPair({ - priv: options.priv, - pub: options.pub - }); - } else { - this.key = bcoin.ecdsa.genKeyPair(); - } - - // Compatability - if (options.multisig) { - if (options.multisig.type) - options.type = options.multisig.type; - if (options.multisig.keys) - options.keys = options.multisig.keys; - if (options.multisig.m) - options.m = options.multisig.m; - if (options.multisig.n) - options.n = options.multisig.n; - } - - this.type = options.type || 'pubkeyhash'; - this.subtype = options.subtype; - this.keys = []; - this.m = options.m || 1; - this.n = options.n || 1; - - if (this.n > 1) { - if (this.type !== 'multisig') - this.type = 'scripthash'; - if (this.type === 'scripthash') - this.subtype = 'multisig'; - } - - if (network.prefixes[this.type] == null) - throw new Error('Unknown prefix: ' + this.type); - - this.nmax = this.type === 'scripthash' - ? (this.compressed ? 15 : 7) - : 3; - - if (this.m < 1 || this.m > this.n) - throw new Error('m ranges between 1 and n'); - - if (this.n < 1 || this.n > this.nmax) - throw new Error('n ranges between 1 and ' + this.nmax); - - this.addKey(this.getPublicKey()); - - (options.keys || []).forEach(function(key) { - this.addKey(key); - }, this); - - if (this.redeem) { - if (!utils.isBytes(this.redeem)) - this.redeem = bcoin.script.encode(this.redeem); - this.type = 'scripthash'; - this.subtype = null; - } this.prefix = 'bt/wallet/' + this.getKeyAddress() + '/'; @@ -170,342 +91,205 @@ Wallet.prototype._init = function init() { }); }; -Wallet.prototype.addKey = function addKey(key) { - key = utils.toBuffer(key); +Wallet.prototype.__defineGetter__('address', function() { + return this.addresses[0]; +}); - var has = this.keys.some(function(k) { - return utils.isEqual(k, key); - }); +Wallet.prototype._getAddressTable = function() { + var addresses = {}; + var i, address; - if (has) + for (i = 0; i < this.addresses.length; i++) { + address = this.addresses[i]; + if (address.type === 'scripthash') + addresses[address.getScriptAddress()] = i; + addresses[address.getKeyAddress()] = i; + } + + return addresses; +}; + +// Faster than indexOf if we have tons of addresses +Wallet.prototype._addressIndex = function _addressIndex(address) { + var addr; + + if (!(address instanceof bcoin.address)) + address = bcoin.address(address); + + if (address.type === 'scripthash') { + addr = address.getScriptAddress(); + if (this._addressTable[addr] != null) + return this._addressTable[addr]; + } + + addr = address.getKeyAddress(); + if (this._addressTable[addr] != null) + return this._addressTable[addr]; + + return -1; +}; + +Wallet.prototype.createChangeAddress = function createChangeAddress(address) { + if (!options) + options = {}; + + if (this.master) { + options.change = true; + options.priv = this.master.deriveAddress(this.addressIndex++, this.accountIndex); + } + + return this.addAddress({ change: true }); +}; + +Wallet.prototype.createNewAddress = function createNewAddress(address, options) { + if (!options) + options = {}; + + if (this.master) + options.priv = this.master.deriveAddress(this.addressIndex++, this.accountIndex); + + return this.addAddress(options); +}; + +Wallet.prototype.hasAddress = function hasAddress(address) { + return this._addressIndex(address) != -1; +}; + +Wallet.prototype.findAddress = function findAddress(address) { + var i = this._addressIndex(address); + + if (i === -1) return; - this.keys.push(key); + return this.addresses[i]; +}; - this.keys = utils.sortKeys(this.keys); +Wallet.prototype.addAddress = function addAddress(address) { + var self = this; + var index; + + if (!(address instanceof bcoin.address)) + address = bcoin.address(address); + + if (this._addressIndex(address) !== -1) + return; + + if (address._wallet) + address._wallet.removeAddress(address); + + address._wallet = this; + + index = this.addresses.push(address) - 1; + + if (address.hd && address.hd.isMaster && adress.hd.isPrivate) { + assert(!this.master); + this.master = address; + } + + address.on('scriptaddress', address._onUpdate = function(old, cur) { + self._addressTable[cur] = self._addressTable[old]; + delete self._addressTable[old]; + }); + + if (address.type === 'scripthash') + this._addressTable[address.getScriptAddress()] = index; + + this._addressTable[address.getKeyAddress()] = index; + + this.emit('add address', address); + + return address; +}; + +Wallet.prototype.removeAddress = function removeAddress(address) { + var i; + + assert(address instanceof bcoin.address); + + i = this._addressIndex(address); + + if (i === -1) + return; + + assert(address._wallet === this); + assert(address._onUpdate); + + this.addresses.splice(i, 1); + + address.removeListener('scriptaddress', address._onUpdate); + + this._addressTable = this._getAddressTable(); + + delete address._onUpdate; + delete address._wallet; + + this.emit('remove address', address); + + return address; +}; + +Wallet.prototype.addKey = function addKey(key, i) { + return this.address.addKey(key); }; Wallet.prototype.removeKey = function removeKey(key) { - key = utils.toBuffer(key); - - var index = this.keys.map(function(key, i) { - return utils.isEqual(key, pub) ? i : null; - }).filter(function(i) { - return i !== null; - })[0]; - - if (index == null) - return; - - this.keys.splice(index, 1); - - this.keys = utils.sortKeys(this.keys); + return this.address.removeKey(key); }; Wallet.prototype.derive = function derive() { - var options = this.options; - - if (!this.hd) - throw new Error('Wallet is not HD'); - - options.priv = this.hd.derive.apply(this.hd, arguments); - - return new Wallet(options); + this.addAddress(this.address.derive.apply(this.address, arguments)); }; Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { - var priv = this.key.getPrivate(); - var arr, chk; - - if (!priv) - return; - - priv = priv.toArray(); - - if (!enc) - return priv; - - if (enc === 'base58') - return Wallet.toSecret(priv, this.compressed); - else if (enc === 'hex') - return utils.toHex(priv); - else - return priv; -}; - -Wallet.toSecret = function toSecret(priv, compressed) { - var arr, chk; - - // We'll be using ncompressed public key as an address - arr = [network.prefixes.privkey]; - - // 0-pad key - while (arr.length + priv.length < 33) - arr.push(0); - - arr = arr.concat(priv); - - if (compressed) - arr.push(1); - - chk = utils.checksum(arr); - - return utils.toBase58(arr.concat(chk)); -}; - -Wallet.fromSecret = function fromSecret(priv) { - var key, compressed; - - key = bcoin.utils.fromBase58(priv); - assert(utils.isEqual(key.slice(-4), utils.checksum(key.slice(0, -4)))); - assert.equal(key[0], network.prefixes.privkey); - - key = key.slice(0, -4); - if (key.length === 34) { - assert.equal(key[33], 1); - priv = key.slice(1, -1); - compressed = true; - } else { - priv = key.slice(1); - compressed = false; - } - - return { - priv: priv, - compressed: compressed - }; + return this.address.getPrivateKey(enc); }; Wallet.prototype.getScript = function getScript() { - if (this.type !== 'scripthash') - return; - - if (this.redeem) - return this.redeem.slice(); - - if (this.subtype === 'pubkey') - return bcoin.script.encode([this.getPublicKey(), 'checksig']); - - if (this.subtype === 'pubkeyhash') { - return bcoin.script.encode([ - 'dup', - 'hash160', - this.getKeyHash(), - 'equalverify', - 'checksig' - ]); - } - - return bcoin.script.encode( - bcoin.script.createMultisig(this.keys, this.m, this.n) - ); + return this.address.getScript(); }; Wallet.prototype.getScriptHash = function getScriptHash() { - if (this.type !== 'scripthash') - return; - - return utils.ripesha(this.getScript()); + return this.address.getScriptHash(); }; Wallet.prototype.getScriptAddress = function getScriptAddress() { - if (this.type !== 'scripthash') - return; - - return Wallet.hash2addr(this.getScriptHash(), this.type); + return this.address.getScriptAddress(); }; Wallet.prototype.getPublicKey = function getPublicKey(enc) { - var pub = this.key.getPublic(this.compressed, 'array'); - - if (enc === 'base58') - return utils.toBase58(pub); - else if (enc === 'hex') - return utils.toHex(pub); - else - return pub; + return this.address.getPublicKey(enc); }; Wallet.prototype.getKeyHash = function getKeyHash() { - return Wallet.key2hash(this.getPublicKey()); + return this.address.getKeyHash(); }; Wallet.prototype.getKeyAddress = function getKeyAddress() { - return Wallet.hash2addr(this.getKeyHash(), 'pubkeyhash'); + return this.address.getKeyAddress(); }; Wallet.prototype.getHash = function getHash() { - if (this.type === 'scripthash') - return this.getScriptHash(); - return this.getKeyHash(); + return this.address.getHash(); }; Wallet.prototype.getAddress = function getAddress() { - if (this.type === 'scripthash') - return this.getScriptAddress(); - return this.getKeyAddress(); -}; - -Wallet.key2hash = function key2hash(key) { - key = utils.toBuffer(key); - return utils.ripesha(key); -}; - -Wallet.hash2addr = function hash2addr(hash, prefix) { - var addr; - - hash = utils.toArray(hash, 'hex'); - - prefix = network.prefixes[prefix || 'pubkeyhash']; - hash = [ prefix ].concat(hash); - - addr = hash.concat(utils.checksum(hash)); - - return utils.toBase58(addr); -}; - -Wallet.__defineGetter__('prefixes', function() { - if (Wallet._prefixes) return Wallet._prefixes; - Wallet._prefixes = ['pubkeyhash', 'scripthash'].reduce(function(out, prefix) { - var ch = Wallet.hash2addr(Wallet.key2hash([]), prefix)[0]; - out[ch] = prefix; - return out; - }, {}); - return Wallet._prefixes; -}); - -Wallet.addr2hash = function addr2hash(addr, prefix) { - var chk; - - if (prefix == null && typeof addr === 'string') - prefix = Wallet.prefixes[addr[0]]; - - if (!utils.isBuffer(addr)) - addr = utils.fromBase58(addr); - - prefix = network.prefixes[prefix || 'pubkeyhash']; - - if (addr.length !== 25) - return []; - - if (addr[0] !== prefix) - return []; - - chk = utils.checksum(addr.slice(0, -4)); - - if (utils.readU32(chk, 0) !== utils.readU32(addr, 21)) - return []; - - return addr.slice(1, -4); -}; - -Wallet.validateAddress = function validateAddress(addr, prefix) { - if (!addr) - return false; - - var p = Wallet.addr2hash(addr, prefix); - - return p.length !== 0; -}; - -Wallet.prototype.ownOutput = function ownOutput(tx, index) { - var scripthash = this.getScriptHash(); - var hash = this.getKeyHash(); - var key = this.getPublicKey(); - var keys = this.keys; - - var outputs = tx.outputs.filter(function(output, i) { - var s = output.script; - - if (index != null && index !== i) - return false; - - if (bcoin.script.isPubkey(s, key)) - return true; - - if (bcoin.script.isPubkeyhash(s, hash)) - return true; - - if (bcoin.script.isMultisig(s, keys)) - return true; - - if (scripthash) { - if (bcoin.script.isScripthash(s, scripthash)) - return true; - } - - return false; - }, this); - - if (outputs.length === 0) - return false; - - return outputs; + return this.address.getAddress(); }; Wallet.prototype.ownInput = function ownInput(tx, index) { - var scripthash = this.getScriptHash(); - var hash = this.getKeyHash(); - var key = this.getPublicKey(); - var redeem = this.getScript(); - var keys = this.keys; + this.fillPrevout(tx); + return tx.testInputs(this._addressTable, index, true); +}; - var inputs = tx.inputs.filter(function(input, i) { - var s; - - if (!input.prevout.tx && this.tx._all[input.prevout.hash]) - input.prevout.tx = this.tx._all[input.prevout.hash]; - - if (index != null && index !== i) - return false; - - // if (bcoin.script.isPubkeyInput(input.script, key, tx, i)) - // return true; - - if (bcoin.script.isPubkeyhashInput(input.script, key)) - return true; - - // if (bcoin.script.isMultisigInput(input.script, keys, tx, i)) - // return true; - - if (redeem) { - if (bcoin.script.isScripthashInput(input.script, redeem)) - return true; - } - - if (!input.prevout.tx) - return false; - - s = input.prevout.tx.getSubscript(input.prevout.index); - - if (bcoin.script.isPubkey(s, key)) - return true; - - if (bcoin.script.isPubkeyhash(s, hash)) - return true; - - if (bcoin.script.isMultisig(s, keys)) - return true; - - if (scripthash) { - if (bcoin.script.isScripthash(s, scripthash)) - return true; - } - - return false; - }, this); - - if (inputs.length === 0) - return false; - - return inputs; +Wallet.prototype.ownOutput = function ownOutput(tx, index) { + return tx.testOutputs(this._addressTable, index, true); }; Wallet.prototype.fill = function fill(tx, address, fee) { var result; if (!address) - address = this.changeAddress || this.getAddress(); + address = this.createChangeAddress(); result = tx.fill(this.getUnspent(), address, fee); @@ -527,65 +311,72 @@ Wallet.prototype.fillPrevout = function fillPrevout(tx) { Wallet.prototype.fillTX = Wallet.prototype.fillPrevout; Wallet.prototype.scriptInputs = function scriptInputs(tx) { - var pub = this.getPublicKey(); - var redeem = this.getScript(); - var inputs = tx.inputs; + return this.addresses.reduce(function(total, address) { + var pub = address.getPublicKey(); + var redeem = address.getScript(); + tx.inputs.forEach(function(input, i) { + if (!input.prevout.tx && this.tx._all[input.prevout.hash]) + input.prevout.tx = this.tx._all[input.prevout.hash]; - inputs = inputs.filter(function(input, i) { - if (!input.prevout.tx && this.tx._all[input.prevout.hash]) - input.prevout.tx = this.tx._all[input.prevout.hash]; + if (!input.prevout.tx || !this.ownOutput(input.prevout.tx)) + return; - if (!input.prevout.tx || !this.ownOutput(input.prevout.tx)) - return false; + if (tx.scriptInput(i, pub, redeem)) + total++; + }, this); - tx.scriptInput(i, pub, redeem); - - return true; - }, this); - - return inputs.length; + return total; + }, 0, this); }; Wallet.prototype.signInputs = function signInputs(tx, type) { - var key = this.key; - var inputs = tx.inputs; + var self = this; - inputs = inputs.filter(function(input, i) { - if (!input.prevout.tx && this.tx._all[input.prevout.hash]) - input.prevout.tx = this.tx._all[input.prevout.hash]; + return this.addresses.reduce(function(total, address) { + if (!address.key.priv) + return total; - if (!input.prevout.tx || !this.ownOutput(input.prevout.tx)) - return false; + tx.inputs.forEach(function(input, i) { + if (!input.prevout.tx && self.tx._all[input.prevout.hash]) + input.prevout.tx = self.tx._all[input.prevout.hash]; - tx.signInput(i, key, type); + if (!input.prevout.tx || !self.ownOutput(input.prevout.tx)) + return; - return true; - }, this); + if (tx.signInput(i, address.key, type)) + total++; + }); - return inputs.length; + return total; + }, 0); }; Wallet.prototype.sign = function sign(tx, type) { - var pub = this.getPublicKey(); - var redeem = this.getScript(); - var key = this.key; - var inputs = tx.inputs; + var self = this; - // Add signature script to each input - inputs = inputs.filter(function(input, i) { - if (!input.prevout.tx && this.tx._all[input.prevout.hash]) - input.prevout.tx = this.tx._all[input.prevout.hash]; + return this.addresses.reduce(function(total, address) { + var pub = address.getPublicKey(); + var redeem = address.getScript(); + var key = address.key; - // Filter inputs that this wallet own - if (!input.prevout.tx || !this.ownOutput(input.prevout.tx)) - return false; + if (!key.priv) + return total; - tx.scriptSig(i, key, pub, redeem, type); + // Add signature script to each input + tx.inputs.forEach(function(input, i) { + if (!input.prevout.tx && self.tx._all[input.prevout.hash]) + input.prevout.tx = self.tx._all[input.prevout.hash]; - return true; - }, this); + // Filter inputs that this wallet own + if (!input.prevout.tx || !self.ownOutput(input.prevout.tx)) + return; - return inputs.length; + if (tx.scriptSig(i, key, pub, redeem, type)) + total++; + }); + + return total; + }, 0); }; Wallet.prototype.addTX = function addTX(tx, block) { @@ -647,82 +438,87 @@ Wallet.prototype.toAddress = function toAddress() { Wallet.prototype.toJSON = function toJSON(encrypt) { return { - v: 2, + v: 3, name: 'wallet', network: network.type, - encrypted: encrypt ? true : false, label: this.label, - address: this.getKeyAddress(), - scriptaddress: this.getScriptAddress(), + accountIndex: this.accountIndex, + addressIndex: this.addressIndex, + changeIndex: this.changeIndex, + addresses: this.addresses.filter(function(address) { + if (!address.hd) + return true; + + if (address.change) + return false; + + return true; + }).map(function(address) { + return address.toJSON(encrypt); + }), balance: utils.toBTC(this.getBalance()), - pub: this.getPublicKey('hex'), - priv: encrypt - ? encrypt(this.getPrivateKey('base58')) - : this.getPrivateKey('base58'), - xprivkey: this.hd - ? (encrypt ? encrypt(this.hd.xprivkey) : this.hd.xprivkey) - : null, - type: this.type, - subtype: this.subtype, - redeem: this.redeem ? utils.toHex(this.redeem) : null, - keys: this.keys.map(utils.toBase58), - m: this.m, - n: this.n, tx: this.tx.toJSON() }; }; Wallet.fromJSON = function fromJSON(json, decrypt) { - var priv, pub, xprivkey, multisig, compressed, key, w; + var priv, pub, xprivkey, multisig, compressed, key, w, i; - assert.equal(json.v, 2); + assert.equal(json.v, 3); assert.equal(json.name, 'wallet'); if (json.network) assert.equal(json.network, network.type); - if (json.encrypted && !decrypt) - throw new Error('Cannot decrypt wallet'); - - if (json.priv) { - priv = json.priv; - if (json.encrypted) - priv = decrypt(priv); - - key = Wallet.fromSecret(json.priv); - priv = key.priv; - compressed = key.compressed; - } else { - pub = bcoin.utils.toArray(json.pub, 'hex'); - compressed = pub[0] !== 0x04; - } - - if (json.xprivkey) { - xprivkey = json.xprivkey; - if (json.encrypted) - xprivkey = decrypt(xprivkey); - priv = bcoin.hd.priv(xprivkey); - } - w = new Wallet({ label: json.label, - priv: priv, - pub: pub, - compressed: compressed, - multisig: multisig, - type: json.type, - subtype: json.subtype, - redeem: json.redeem ? utils.toArray(json.redeem, 'hex') : null, - keys: json.keys.map(utils.fromBase58), - m: json.m, - n: json.n + accountIndex: json.accountIndex, + addressIndex: json.addressIndex, + changeIndex: json.changeIndex, + addresses: json.addresses.map(function(address) { + return bcoin.address.fromJSON(address, decrypt); + }) }); w.tx.fromJSON(json.tx); + // Make sure we have all the change + // addresses (we don't save them) + for (i = 0; i < w.changeIndex; i++) { + w.addKey({ + change: true, + key: w.master.deriveChange(i, w.accountIndex) + }); + } + return w; }; +// Compat - Legacy +Wallet.toSecret = function toSecret(priv, compressed) { + return bcoin.keypair.toSecret(priv, compressed); +}; + +Wallet.fromSecret = function fromSecret(priv) { + return bcoin.keypair.fromSecret(priv); +}; + +Wallet.key2hash = function key2hash(key) { + return bcoin.address.key2hash(key); +}; + +Wallet.hash2addr = function hash2addr(hash, prefix) { + return bcoin.address.hash2addr(hash, prefix); +}; + +Wallet.addr2hash = function addr2hash(addr, prefix) { + return bcoin.address.addr2hash(addr, prefix); +}; + +Wallet.validateAddress = function validateAddress(addr, prefix) { + return bcoin.address.validateAddress(addr, prefix); +}; + /** * Expose */