diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 4a70dcc5..051133c7 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -33,12 +33,12 @@ function Address(options) { this.options = options; this.storage = options.storage; this.label = options.label || ''; - this.change = !!options.change; this.derived = !!options.derived; this.key = bcoin.keypair(options); - this.index = options.index; this.path = options.path; + this.change = !!options.change; + this.index = options.index; this.type = options.type || 'pubkeyhash'; this.keys = []; diff --git a/lib/bcoin/tx-pool.js b/lib/bcoin/tx-pool.js index 69ca7f2a..ae41432e 100644 --- a/lib/bcoin/tx-pool.js +++ b/lib/bcoin/tx-pool.js @@ -342,7 +342,7 @@ TXPool.prototype.getAll = function getAll(address) { return this._all[key]; }, this).filter(function(tx) { if (address) { - if (!address.ownOutput(tx) && !address.ownInput(tx)) + if (!tx.testInputs(address) && !tx.testOutputs(address)) return false; } return true; @@ -354,7 +354,7 @@ TXPool.prototype.getUnspent = function getUnspent(address) { return this._unspent[key]; }, this).filter(function(item) { if (address) { - if (!address.ownOutput(item)) + if (!tx.testInputs(address) && !tx.testOutputs(address)) return false; } return true; @@ -366,7 +366,7 @@ TXPool.prototype.getPending = function getPending(address) { return this._all[key]; }, this).filter(function(tx) { if (address) { - if (!address.ownInput(tx) && !address.ownOutput(tx)) + if (!tx.testInputs(address) && !tx.testOutputs(address)) return false; } return tx.ts === 0; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 3fecf65d..74866cb4 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -31,13 +31,10 @@ function Wallet(options) { options = utils.merge({}, options); - if (options.derivation === 'bip44' || options.derivation === 'bip45') - options.hd = true; - - if (options.hd && !options.master) { - options.master = options.hd === true - ? bcoin.hd.privateKey() - : bcoin.hd.privateKey(options.hd); + if (!options.master) { + options.master = options.hd + ? bcoin.hd.privateKey(options.hd) + : bcoin.hd.privateKey(); delete options.hd; } @@ -71,19 +68,18 @@ function Wallet(options) { this.options = options; this.addresses = []; this.master = options.master || null; - this._addressTable = {}; - this._labelMap = {}; + this.addressMap = options.addressMap || {}; + this.labelMap = {}; + this.change = []; + this.receive = []; this.accountIndex = options.accountIndex || 0; - this.addressDepth = options.addressDepth || 0; - this.changeDepth = options.changeDepth || 0; + this.receiveDepth = options.receiveDepth || 1; + this.changeDepth = options.changeDepth || 1; this.copayBIP45 = options.copayBIP45 || false; + this.lookahead = options.lookahead != null ? options.lookahead : 5; this.cosignerIndex = -1; - this.sharedCosignerIndex = constants.hd.hardened - 1; - this.purposeKeys = options.purposeKeys || []; - this.keys = options.keys || []; - this.hd = !!this.master; this.type = options.type || 'pubkeyhash'; this.derivation = options.derivation || null; this.compressed = options.compressed !== false; @@ -98,14 +94,10 @@ function Wallet(options) { this.prefixType = this.type === 'multisig' ? 'scripthash' : 'pubkeyhash'; if (!this.derivation) { - if (this.master) { - if (this.type === 'multisig') - this.derivation = 'bip45'; - else - this.derivation = 'bip44'; - } else { - this.derivation = 'normal'; - } + if (this.type === 'multisig') + this.derivation = 'bip45'; + else + this.derivation = 'bip44'; } if (network.prefixes[this.prefixType] == null) @@ -115,76 +107,21 @@ function Wallet(options) { throw new Error('m ranges between 1 and n'); if (this.derivation === 'bip45') { - this.purposeKey = this.master.isPurpose45() + this.accountKey = this.master.isPurpose45() ? this.master : this.master.derivePurpose45(); } else if (this.derivation === 'bip44') { - this.purposeKey = this.master.isAccount44() + this.accountKey = this.master.isAccount44() ? this.master : this.master.deriveAccount44(this.accountIndex); } - if (!options.addresses) - options.addresses = []; - - if (this._isKeyOptions(options)) { - options.addresses.unshift({ - privateKey: options.privateKey, - publicKey: options.publicKey, - pair: options.pair, - type: this.type, - m: this.m, - n: this.n, - keys: [], - change: false - }); - } - this.storage = options.storage; this.loading = true; this.lastTs = 0; this.lastHeight = 0; - // This is a chicken and egg problem for BIP45. Real address keys cannot be - // generated until all shared keys have been added to the wallet. The flow of - // this wallet is, the actual address objects will be generated once all - // shared keys have been added. This presents a problem for non-bip45 - // wallets: if they want to use the addKey() interface with - // wallet.getPublicKey(), we need to expose a key for them to use. Here, we - // generate the last receiving address. However, since "normal" wallets - // cannot deterministically generate keys, we have to buffer the generated - // key for later. - if (this.derivation === 'normal') { - // Try to find the last receiving address if there is one. - receiving = options.addresses.filter(function(address) { - return !address.change && this._isKeyOptions(address); - }, this).pop(); - if (receiving) { - this.currentAddress = bcoin.address(receiving); - } else { - // No receiving address is in this wallet yet, generate - // it and save it so createKey can recreate it later. - key = this.createKey(); - this._firstKey = key; - this.currentAddress = bcoin.address({ - privateKey: key.privateKey, - publicKey: key.publicKey, - compressed: key.compressed, - index: key.index, - path: key.path, - type: this.type, - m: this.m, - n: this.n, - keys: options.keys - }); - this.currentAddress._wallet = this; - } - } - - if (this.derivation === 'bip44' || this.derivation === 'bip45') - this.addKey(this.purposeKey); - else - this.addKey(this.currentAddress.publicKey); + this.addKey(this.accountKey); (options.keys || []).forEach(function(key) { this.addKey(key); @@ -193,286 +130,42 @@ function Wallet(options) { inherits(Wallet, EventEmitter); -Wallet.prototype._pruneAddresses = function _pruneAddresses(options) { - var addresses = this.addresses.slice(); - var i, address; - - for (i = 0; i < addresses.length; i++) { - address = addresses[i]; - - if (address === this.currentAddress || address === this.changeAddress) - continue; - - if (!address.change) - continue; - - if (!address.derived) - continue; - - if (address.getBalance().cmpn(0) === 0) - this.removeAddress(address); - } -}; - -Wallet.prototype._isKeyOptions = function _isKeyOptions(options) { - return (options.priv || options.privateKey) - || (options.pub || options.publicKey) - || (options.key || options.pair); -}; - -// Wallet ID: -// bip45: Purpose key address -// bip44: Account key address -// normal: Address of first key in wallet -Wallet.prototype.getID = function getID() { - if (this.derivation === 'bip45') - return bcoin.address.compile(this.purposeKey.publicKey); - - if (this.derivation === 'bip44') - return bcoin.address.compile(this.purposeKey.publicKey); - - if (this.derivation === 'normal') { - if (this.addresses.length) - return this.addresses[0].getKeyAddress(); - - if (this._firstKey) - return bcoin.address.compile(this._firstKey.publicKey); - } - - assert(false); -}; - -Wallet.prototype._initAddresses = function _initAddresses() { +Wallet.prototype._init = function _init() { + var self = this; + var prevBalance = null; var options = this.options; - var i; + var addr, i; assert(!this._initialized); this._initialized = true; - if (this.copayBIP45) - this.cosignerIndex = this.sharedCosignerIndex; + if (Object.keys(this.addressMap).length === 0) { + for (i = 0; i < this.receiveDepth - 1; i++) + this.deriveReceive(i); - delete this.currentAddress; + for (i = 0; i < this.changeDepth - 1; i++) + this.deriveChange(i); - options.addresses.forEach(function(address) { - address = this.addAddress(address); - if (!this.master) { - if (!address.change) - this.currentAddress = address; - else - this.changeAddress = address; - } - }, this); + for (i = this.receiveDepth; i < this.receiveDepth + this.lookahead; i++) + this.deriveReceive(i); - if (this.master) { - for (i = 0; i < this.addressDepth; i++) - this.currentAddress = this.createAddress(false, i); - - for (i = 0; i < this.changeDepth; i++) - this.changeAddress = this.createAddress(true, i); + for (i = this.changeDepth; i < this.changeDepth + this.lookahead; i++) + this.deriveChange(i); } - if (!this.currentAddress) - this.currentAddress = this.createAddress(); + this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); + this.changeAddress = this.deriveChange(this.changeDepth - 1); - if (!this.changeAddress) - this.changeAddress = this.createAddress(true); - - assert(this.currentAddress); - assert(!this.currentAddress.change); + assert(this.receiveAddress); + assert(!this.receiveAddress.change); assert(this.changeAddress.change); this.prefix = 'bt/wallet/' + this.getID() + '/'; this.tx = options.tx || bcoin.txPool(this); - this._init(); -}; - -Wallet.prototype.addKey = function addKey(key) { - var hdKey, has, i; - - if (key instanceof bcoin.wallet) { - assert(key.derivation === this.derivation); - if (key.derivation === 'bip44' || key.derivation === 'bip45') - key = key.purposeKey; - else - key = key.currentAddress.publicKey; - } - - if (bcoin.hd.privateKey.isExtended(key)) - key = bcoin.hd.privateKey(key); - else if (bcoin.hd.publicKey.isExtended(key)) - key = bcoin.hd.publicKey(key); - - if (key instanceof bcoin.keypair) - key = key.hd; - - if (key instanceof bcoin.hd.privateKey) - key = key.hdpub; - - if (key instanceof bcoin.hd.publicKey) { - hdKey = key; - key = hdKey.publicKey; - } - - if (this.derivation === 'bip44' || this.derivation === 'bip45') { - if (this.derivation === 'bip44') { - if (!hdKey || !hdKey.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - } - - if (this.derivation === 'bip45') { - if (!hdKey || !hdKey.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); - } - - has = this.purposeKeys.some(function(k) { - return k.xpubkey === hdKey.xpubkey; - }); - - if (has) - return; - - assert(!this._keysFinalized); - - this.purposeKeys.push(hdKey); - - if (this.purposeKeys.length === this.n) - this.finalizeKeys(); - - return; - } - - key = utils.toBuffer(key); - - has = this.keys.some(function(k) { - return utils.isEqual(k, key); - }); - - if (has) - return; - - assert(!this._keysFinalized); - - this.keys.push(key); - - if (this.keys.length === this.n) - this.finalizeKeys(); -}; - -Wallet.prototype.finalizeKeys = function finalizeKeys(key) { - assert(!this._keysFinalized); - this._keysFinalized = true; - - if (this.derivation === 'bip44' || this.derivation === 'bip45') { - this.purposeKeys = utils.sortHDKeys(this.purposeKeys); - - for (i = 0; i < this.purposeKeys.length; i++) { - if (this.purposeKeys[i].xpubkey === this.purposeKey.xpubkey) { - this.cosignerIndex = i; - this._cosignerIndex = i; - break; - } - } - - assert(this.cosignerIndex !== -1); - - this._initAddresses(); - - return; - } - - this.keys = utils.sortKeys(this.keys); - - for (i = 0; i < this.keys.length; i++) { - if (utils.isEqual(this.keys[i], this.currentAddress.publicKey)) { - this.cosignerIndex = i; - this._cosignerIndex = i; - break; - } - } - - assert(this.cosignerIndex !== -1); - - this._initAddresses(); -}; - -Wallet.prototype.removeKey = function removeKey(key) { - var hdKey, index; - - assert(!this._keysFinalized); - - if (key instanceof bcoin.wallet) { - assert(key.derivation === this.derivation); - if (key.derivation === 'bip44' || key.derivation === 'bip45') - key = key.purposeKey; - else - key = key.currentAddress.publicKey; - } - - if (bcoin.hd.privateKey.isExtended(key)) - key = bcoin.hd.privateKey(key); - else if (bcoin.hd.publicKey.isExtended(key)) - key = bcoin.hd.publicKey(key); - - if (key instanceof bcoin.keypair) - key = key.hd; - - if (key instanceof bcoin.hd.privateKey) - key = key.hdpub; - - if (key instanceof bcoin.hd.publicKey) { - hdKey = key; - key = hd.publicKey; - } - - if (this.derivation === 'bip44' || this.derivation === 'bip45') { - if (this.derivation === 'bip44') { - if (!hdKey || !hdKey.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - } - - if (this.derivation === 'bip45') { - if (!hdKey || !hdKey.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); - } - - index = this.purposeKeys.map(function(k, i) { - return k.xpubkey === hdKey.xpubkey ? i : null; - }).filter(function(i) { - return i !== null; - })[0]; - - if (index == null) - return; - - this.purposeKeys.splice(index, 1); - - return; - } - - key = utils.toBuffer(key); - - index = this.keys.map(function(k, i) { - return utils.isEqual(k, key) ? i : null; - }).filter(function(i) { - return i !== null; - })[0]; - - if (index == null) - return; - - this.keys.splice(index, 1); -}; - -Wallet.prototype._init = function init() { - var self = this; - var prevBalance = null; - if (this.tx._loaded) { this.loading = false; - // this._pruneAddresses(); return; } @@ -495,11 +188,7 @@ Wallet.prototype._init = function init() { this.tx.on('confirmed', function(tx) { // TX using this address was confirmed. // Allocate a new address. - if (self.currentAddress.ownOutput(tx)) - self.currentAddress = self.createAddress(); - if (self.changeAddress.ownOutput(tx)) - self.changeAddress = self.createAddress(true); - // self._pruneAddresses(); + self.syncOutputDepth(tx); self.emit('confirmed', tx); }); @@ -507,7 +196,6 @@ Wallet.prototype._init = function init() { self.loading = false; self.lastTs = ts; self.lastHeight = height; - // self._pruneAddresses(); self.emit('load', ts); }); @@ -516,254 +204,256 @@ Wallet.prototype._init = function init() { }); }; -Wallet.prototype._getAddressTable = function() { - var addresses = {}; - var i, address; +Wallet.prototype.addKey = function addKey(key) { + var has, i; - for (i = 0; i < this.addresses.length; i++) { - address = this.addresses[i]; - addresses[address.getAddress()] = i; + if (key instanceof bcoin.wallet) { + assert(key.derivation === this.derivation); + key = key.accountKey; } - return addresses; + if (bcoin.hd.privateKey.isExtended(key)) + key = bcoin.hd.privateKey(key); + else if (bcoin.hd.publicKey.isExtended(key)) + key = bcoin.hd.publicKey(key); + + if (key instanceof bcoin.hd.privateKey) + key = key.hdpub; + + if (this.derivation === 'bip44') { + if (!key || !key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + } else if (this.derivation === 'bip45') { + if (!key || !key.isPurpose45()) + throw new Error('Must add HD purpose keys to BIP45 wallet.'); + } + + has = this.keys.some(function(k) { + return k.xpubkey === key.xpubkey; + }); + + if (has) + return; + + assert(!this._keysFinalized); + + this.keys.push(key); + + if (this.keys.length === this.n) + this._finalizeKeys(); }; -// Faster than indexOf if we have tons of addresses -Wallet.prototype._addressIndex = function _addressIndex(address) { - var addr; +Wallet.prototype.removeKey = function removeKey(key) { + var index; - if (typeof address === 'string') - return this._addressTable[addr]; + assert(!this._keysFinalized); - if (!(address instanceof bcoin.address)) - address = bcoin.address(address); + if (key instanceof bcoin.wallet) { + assert(key.derivation === this.derivation); + key = key.accountKey; + } - addr = address.getAddress(); - if (this._addressTable[addr] != null) - return this._addressTable[addr]; + if (bcoin.hd.privateKey.isExtended(key)) + key = bcoin.hd.privateKey(key); + else if (bcoin.hd.publicKey.isExtended(key)) + key = bcoin.hd.publicKey(key); - return -1; + if (key instanceof bcoin.keypair) + key = key.hd; + + if (key instanceof bcoin.hd.privateKey) + key = key.hdpub; + + if (this.derivation === 'bip44') { + if (!key || !key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + } else if (this.derivation === 'bip45') { + if (!key || !key.isPurpose45()) + throw new Error('Must add HD purpose keys to BIP45 wallet.'); + } + + index = this.keys.map(function(k, i) { + return k.xpubkey === key.xpubkey ? i : null; + }).filter(function(i) { + return i !== null; + })[0]; + + if (index == null) + return; + + this.keys.splice(index, 1); }; -Wallet.prototype.createAddress = function createAddress(change, index) { - var self = this; - var key = this.createKey(change, index); +Wallet.prototype._finalizeKeys = function _finalizeKeys(key) { + assert(!this._keysFinalized); + this._keysFinalized = true; + + this.keys = utils.sortHDKeys(this.keys); + + for (i = 0; i < this.keys.length; i++) { + if (this.keys[i].xpubkey === this.accountKey.xpubkey) { + this.cosignerIndex = i; + break; + } + } + + assert(this.cosignerIndex !== -1); + + this._init(); +}; + +// Wallet ID: +// bip45: Purpose key address +// bip44: Account key address +Wallet.prototype.getID = function getID() { + return bcoin.address.compile(this.accountKey.publicKey); +}; + +Wallet.prototype.createReceive = function createReceive() { + return this.createAddress(false); +}; + +Wallet.prototype.createChange = function createChange() { + return this.createAddress(true); +}; + +Wallet.prototype.createAddress = function createAddress(change) { var address; + if (typeof change === 'string') + change = this.parsePath(change).change; + + if (change) { + address = this.deriveChange(this.changeDepth); + this.deriveChange(this.changeDepth + this.lookahead); + this.changeDepth++; + } else { + address = this.deriveReceive(this.receiveDepth); + this.deriveReceive(this.receiveDepth + this.lookahead); + this.receiveDepth++; + } + + return address; +}; + +Wallet.prototype.deriveReceive = function deriveReceive(index) { + if (typeof index === 'string') + index = this.parsePath(index).index; + + return this.deriveAddress(false, index); +}; + +Wallet.prototype.deriveChange = function deriveChange(index) { + if (typeof index === 'string') + index = this.parsePath(index).index; + + return this.deriveAddress(true, index); +}; + +Wallet.prototype.deriveAddress = function deriveAddress(change, index) { + var self = this; + var path, data, key, options, address; + assert(this._initialized); - var options = { + if (typeof change === 'string') + path = change; + + if (path) { + // Map address to path + if (path.indexOf('/') === -1) { + path = this.addressMap[path]; + if (!path) + return; + } + data = this.parsePath(path); + } else { + data = { + path: this.createPath(this.cosignerIndex, change, index), + cosignerIndex: this.cosignerIndex, + change: change, + index: index + }; + } + + key = this.accountKey.derive(data.path); + + options = { privateKey: key.privateKey, publicKey: key.publicKey, compressed: key.compressed, - index: key.index, - path: key.path, + change: data.change, + index: data.index, + path: data.path, type: this.type, m: this.m, n: this.n, keys: [], - change: change, - derived: !!this.hd + derived: true }; - if (index == null) { - index = change ? self.changeDepth : self.addressDepth; - if (this.master) { - if (change) - this.changeDepth++; - else - this.addressDepth++; - } - } - - if (this.derivation === 'bip44') { - this.purposeKeys.forEach(function(key, cosignerIndex) { - key = key - .derive(change ? 1 : 0) - .derive(index); - options.keys.push(key.publicKey); - }); - this.keys = utils.sortKeys(options.keys); - } else if (this.derivation === 'bip45') { - this.purposeKeys.forEach(function(key, cosignerIndex) { - key = key - .derive(cosignerIndex) - .derive(change ? 1 : 0) - .derive(index); - options.keys.push(key.publicKey); - }); - this.keys = utils.sortKeys(options.keys); - } else if (this.derivation === 'normal') { - this.keys.forEach(function(key, i) { - if (i !== this._cosignerIndex) - options.keys.push(key); - }, this); + this.keys.forEach(function(key, cosignerIndex) { + var path = this.createPath(cosignerIndex, data.change, data.index); + key = key.derive(path); options.keys.push(key.publicKey); - } + }, this); - address = this.addAddress(options); + address = bcoin.address(options); + + this.addressMap[address.getAddress()] = data.path; return address; }; Wallet.prototype.hasAddress = function hasAddress(address) { - return this._addressIndex(address) != -1; + return this.addressMap[address] != null; }; -Wallet.prototype.findAddress = function findAddress(address) { - var i = this._addressIndex(address); +Wallet.prototype.createPath = function createPath(cosignerIndex, change, index) { + if (this.copayBIP45) + cosignerIndex = constants.hd.hardened - 1; - if (i === -1) - return; - - return this.addresses[i]; + return 'm' + + (this.derivation === 'bip45' ? '/' + cosignerIndex : '') + + '/' + (change ? 1 : 0) + + '/' + index; }; -Wallet.prototype.addAddress = function addAddress(address) { - var self = this; - var index; +Wallet.prototype.parsePath = function parsePath(path) { + var parts; - assert(this._initialized); + if (this.derivation === 'bip45') + assert(/^m\/\d+\/\d+\/\d+$/.test(path)); + else + assert(/^m\/\d+\/\d+$/.test(path)); - if (!(address instanceof bcoin.address)) - address = bcoin.address(address); + parts = path.split('/'); - if (this._addressIndex(address) !== -1) - return; - - assert(!address._wallet); - - address._wallet = this; - - index = this.addresses.push(address) - 1; - - address.on('update script', address._onUpdate = function(old, cur) { - self._addressTable[cur] = self._addressTable[old]; - delete self._addressTable[old]; - self.emit('add address', address); - }); - - this._addressTable[address.getAddress()] = index; - - if (address.label && this._labelTable[address.label] == null) - this._labelTable[address.label] = index; - - this.emit('add address', address); - - return address; -}; - -Wallet.prototype.removeAddress = function removeAddress(address) { - var i; - - assert(this._initialized); - - 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('update script', address._onUpdate); - - this._addressTable = this._getAddressTable(); - - delete address._onUpdate; - delete address._wallet; - - if (this._labelTable[address.label] === i) - delete this._labelTable[address.label]; - - this.emit('remove address', address); - - return address; -}; - -Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { - return this.currentAddress.getPrivateKey(enc); -}; - -Wallet.prototype.getScript = function getScript() { - return this.currentAddress.getScript(); -}; - -Wallet.prototype.getScriptHash = function getScriptHash() { - return this.currentAddress.getScriptHash(); -}; - -Wallet.prototype.getScriptAddress = function getScriptAddress() { - return this.currentAddress.getScriptAddress(); -}; - -Wallet.prototype.getPublicKey = function getPublicKey(enc) { - return this.currentAddress.getPublicKey(enc); -}; - -Wallet.prototype.createKey = function createKey(change, index) { - var key; - - if (this.derivation === 'normal') { - if (this._firstKey) { - key = this._firstKey; - delete this._firstKey; - return key; - } - key = bcoin.keypair({ - compressed: this.compressed - }); - return { - privateKey: key.privateKey, - publicKey: key.publicKey, - compressed: key.compressed - }; - } - - if (index == null) - index = change ? this.changeDepth : this.addressDepth; - - if (this.derivation === 'bip44') { - key = this.purposeKey - .derive(change ? 1 : 0) - .derive(index); - } else if (this.derivation === 'bip45') { - key = this.purposeKey - .derive(this.cosignerIndex) - .derive(change ? 1 : 0) - .derive(index); - } + if (this.derivation === 'bip45' && this.copayBIP45) + assert(+parts[parts.length - 3] === constants.hd.hardened - 1); return { - privateKey: key.privateKey, - publicKey: key.publicKey, - compressed: true, - index: index, - path: 'm' - // + (this.derivation === 'bip45' ? '/' + this.cosignerIndex : '') - + '/' + (change ? 1 : 0) - + '/' + index + path: path, + cosignerIndex: this.derivation === 'bip45' + ? +parts[parts.length - 3] + : null, + change: +parts[parts.length - 2] === 1, + index: +parts[parts.length - 1] }; }; -Wallet.prototype.setAddressDepth = function setAddressDepth(depth) { +Wallet.prototype.setReceiveDepth = function setReceiveDepth(depth) { var i; assert(this.derivation !== 'normal'); - if (depth <= this.addressDepth) - return false; + for (i = this.receiveDepth; i < depth; i++) + this.receiveAddress = this.deriveReceive(i); - for (i = this.addressDepth; i < depth; i++) - this.currentAddress = this.createAddress(false, i); + for (i = this.receiveDepth + this.lookahead; i < depth + this.lookahead; i++) + this.deriveReceive(i); - this.addressDepth = depth; - - return true; + this.receiveDepth = depth; }; Wallet.prototype.setChangeDepth = function setChangeDepth(depth) { @@ -771,47 +461,65 @@ Wallet.prototype.setChangeDepth = function setChangeDepth(depth) { assert(this.derivation !== 'normal'); - if (depth <= this.changeDepth) - return false; - for (i = this.changeDepth; i < depth; i++) - this.changeAddress = this.createAddress(true, i); + this.changeAddress = this.deriveChange(i); + + for (i = this.changeDepth + this.lookahead; i < depth + this.lookahead; i++) + this.deriveChange(i); this.changeDepth = depth; +}; - return true; +Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { + return this.receiveAddress.getPrivateKey(enc); +}; + +Wallet.prototype.getScript = function getScript() { + return this.receiveAddress.getScript(); +}; + +Wallet.prototype.getScriptHash = function getScriptHash() { + return this.receiveAddress.getScriptHash(); +}; + +Wallet.prototype.getScriptAddress = function getScriptAddress() { + return this.receiveAddress.getScriptAddress(); +}; + +Wallet.prototype.getPublicKey = function getPublicKey(enc) { + return this.receiveAddress.getPublicKey(enc); }; Wallet.prototype.getKeyHash = function getKeyHash() { - return this.currentAddress.getKeyHash(); + return this.receiveAddress.getKeyHash(); }; Wallet.prototype.getKeyAddress = function getKeyAddress() { - return this.currentAddress.getKeyAddress(); + return this.receiveAddress.getKeyAddress(); }; Wallet.prototype.getHash = function getHash() { - return this.currentAddress.getHash(); + return this.receiveAddress.getHash(); }; Wallet.prototype.getAddress = function getAddress() { - return this.currentAddress.getAddress(); + return this.receiveAddress.getAddress(); }; Wallet.prototype.ownInput = function ownInput(tx, index) { if (tx instanceof bcoin.input) - return tx.test(this._addressTable) ? [tx] : false; + return tx.test(this.addressMap) ? [tx] : false; this.fillPrevout(tx); - return tx.testInputs(this._addressTable, index, true); + return tx.testInputs(this.addressMap, index, true); }; Wallet.prototype.ownOutput = function ownOutput(tx, index) { if ((tx instanceof bcoin.output) || (tx instanceof bcoin.coin)) - return tx.test(this._addressTable) ? [tx] : false; + return tx.test(this.addressMap) ? [tx] : false; - return tx.testOutputs(this._addressTable, index, true); + return tx.testOutputs(this.addressMap, index, true); }; Wallet.prototype.fill = function fill(tx, options) { @@ -883,23 +591,113 @@ Wallet.prototype.createTX = function createTX(outputs, fee) { return tx; }; +Wallet.prototype.deriveInputs = function deriveInputs(tx) { + var paths = this.getInputPaths(tx); + var addresses = []; + var i; + + for (i = 0; i < paths.length; i++) + addresses.push(this.deriveAddress(paths[i])); + + return addresses; +}; + +Wallet.prototype.getPath = function getPath(address) { + if (!address || typeof address !== 'string') + return; + return this.addressMap[address]; +}; + +Wallet.prototype.getInputPaths = function getInputPaths(tx) { + var paths = []; + var i, input, output, address, path; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + output = input.output; + assert(output); + + address = output.getAddress(); + path = this.getPath(address); + + if (!path) + continue; + + paths.push(path); + } + + return utils.uniqs(paths); +}; + +Wallet.prototype.getOutputPaths = function getOutputPaths(tx) { + var paths = []; + var i, output, address, path; + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + + address = output.getAddress(); + path = this.getPath(address); + + if (!path) + continue; + + paths.push(path); + } + + return utils.uniqs(paths); +}; + +Wallet.prototype.getOutputDepth = function getOutputDepth(tx) { + var paths = this.getOutputPaths(tx); + var depth = { change: -1, receive: -1 }; + var i, path; + + for (i = 0; i < paths.length; i++) { + path = this.parsePath(path); + if (path.change) { + if (path.index > depth.change) + depth.change = path.index; + } else { + if (path.index > depth.receive) + depth.receive = path.index; + } + } + + depth.change++; + depth.receive++; + + return depth; +}; + +Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx) { + var depth = this.getOutputDepth(tx); + if (depth.change > this.changeDepth) + this.setChangeDepth(depth.change); + if (depth.receive > this.receiveDepth) + this.setReceiveDepth(depth.receive); +}; + Wallet.prototype.scriptInputs = function scriptInputs(tx) { this.fillPrevout(tx); - return this.addresses.reduce(function(total, address) { + var addresses = this.deriveInputs(tx); + return addresses.reduce(function(total, address) { return total + address.scriptInputs(tx); }, 0); }; Wallet.prototype.signInputs = function signInputs(tx, type) { this.fillPrevout(tx); - return this.addresses.reduce(function(total, address) { + var addresses = this.deriveInputs(tx); + return addresses.reduce(function(total, address) { return total + address.signInputs(tx, type); }, 0); }; Wallet.prototype.sign = function sign(tx, type) { this.fillPrevout(tx); - return this.addresses.reduce(function(total, address) { + var addresses = this.deriveInputs(tx); + return addresses.reduce(function(total, address) { return total + address.sign(tx, type); }, 0); }; @@ -909,56 +707,26 @@ Wallet.prototype.addTX = function addTX(tx, block) { }; Wallet.prototype.getAll = function getAll(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return []; - } return this.tx.getAll(address); }; Wallet.prototype.getUnspent = function getUnspent(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return []; - } return this.tx.getUnspent(address); }; Wallet.prototype.getPending = function getPending(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return []; - } return this.tx.getPending(address); }; Wallet.prototype.getSent = function getSent(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return new bn(0); - } return this.tx.getSent(address); }; Wallet.prototype.getReceived = function getReceived(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return new bn(0); - } return this.tx.getReceived(address); }; Wallet.prototype.getBalance = function getBalance(address) { - if (typeof address === 'string') { - address = this.findAddress(address); - if (!address) - return new bn(0); - } return this.tx.getBalance(address); }; @@ -1016,21 +784,13 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { derivation: this.derivation, copayBIP45: this.copayBIP45, accountIndex: this.accountIndex, - addressDepth: this.addressDepth, + receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, master: this.master ? this.master.toJSON(encrypt) : null, - addresses: this.addresses.filter(function(address) { - return !address.derived; - }, this).map(function(address) { - return address.toJSON(encrypt); + addressMap: this.addressMap, + keys: this.keys.map(function(key) { + return key.xpubkey; }), - keys: this.derivation === 'bip44' || this.derivation === 'bip45' - ? this.purposeKeys.map(function(key) { - return key.xpubkey; - }) - : this.keys.map(function(key) { - return utils.toBase58(key); - }), balance: utils.toBTC(this.getBalance()), tx: this.tx.toJSON() }; @@ -1052,14 +812,12 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { derivation: json.derivation, copayBIP45: json.copayBIP45, accountIndex: json.accountIndex, - addressDepth: json.addressDepth, + receiveDepth: json.receiveDepth, changeDepth: json.changeDepth, master: json.master ? bcoin.hd.fromJSON(json.master, decrypt) : null, - addresses: json.addresses.map(function(address) { - return bcoin.address.fromJSON(address, decrypt); - }), + addressMap: json.addressMap, keys: json.keys }); @@ -1068,31 +826,6 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { return wallet; }; -// Compat - Legacy -Wallet.toSecret = function toSecret(privateKey, compressed) { - return bcoin.keypair.toSecret(privateKey, compressed); -}; - -Wallet.fromSecret = function fromSecret(privateKey) { - return bcoin.keypair.fromSecret(privateKey); -}; - -Wallet.key2hash = function key2hash(key) { - return bcoin.address.hash160(key); -}; - -Wallet.hash2addr = function hash2addr(hash, prefix) { - return bcoin.address.toAddress(hash, prefix); -}; - -Wallet.addr2hash = function addr2hash(addr, prefix) { - return bcoin.address.toHash(addr, prefix); -}; - -Wallet.validateAddress = function validateAddress(addr, prefix) { - return bcoin.address.validate(addr, prefix); -}; - /** * Expose */ diff --git a/test/wallet-test.js b/test/wallet-test.js index 74fb1674..cd7ec392 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -35,15 +35,15 @@ describe('Wallet', function() { var w = bcoin.wallet(); var addr = w.getAddress(); assert(addr); - assert(bcoin.wallet.validateAddress(addr)); + assert(bcoin.address.validate(addr)); }); it('should validate existing address', function() { - assert(bcoin.wallet.validateAddress('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc')); + assert(bcoin.address.validate('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc')); }); it('should fail to validate invalid address', function() { - assert(!bcoin.wallet.validateAddress('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc')); + assert(!bcoin.address.validate('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc')); }); it('should sign/verify TX', function() { @@ -74,21 +74,20 @@ describe('Wallet', function() { it('should multisign/verify TX', function() { var w = bcoin.wallet({ + derivation: 'bip44', type: 'multisig', m: 1, n: 2 }); - // var k2 = w.getPublicKey().concat(1); - var k2 = bcoin.ecdsa.genKeyPair().getPublic(true, 'array'); + var k2 = bcoin.hd.priv().deriveAccount44(0).hdpub; w.addKey(k2); - // assert.equal(w.getKeyAddress(), w.getAddress()); // Input transcation var src = bcoin.tx({ outputs: [{ value: 5460 * 2, m: 1, - keys: [ w.getPublicKey(), k2 ] + keys: [ w.getPublicKey(), k2.derive('m/0/0').publicKey ] }, { value: 5460 * 2, address: w.getAddress() + 'x'