From 84551e17e2b042378c98334620b30a1d89b46c92 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 5 Feb 2016 02:37:11 -0800 Subject: [PATCH] paths. scanning. unspents. --- lib/bcoin/address.js | 9 +- lib/bcoin/hd.js | 139 ++++++++++++---------- lib/bcoin/pool.js | 2 +- lib/bcoin/tx.js | 119 +++++++++---------- lib/bcoin/wallet.js | 277 +++++++++++++++++++++++++++---------------- test/wallet-test.js | 8 +- 6 files changed, 322 insertions(+), 232 deletions(-) diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index c600c78d..40283cad 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -37,6 +37,7 @@ function Address(options) { this.derived = !!options.derived; this.key = bcoin.keypair(options); + this.path = options.path; this.type = options.type || 'pubkeyhash'; this.subtype = options.subtype; @@ -44,7 +45,6 @@ function Address(options) { this.m = options.m || 1; this.n = options.n || 1; this.redeem = null; - this.multisig = false; if (this.n > 1) { if (this.type !== 'multisig') @@ -53,9 +53,6 @@ function Address(options) { this.subtype = 'multisig'; } - if (this.type === 'multisig' || this.subtype === 'multisig') - this.multisig = true; - if (network.prefixes[this.type] == null) throw new Error('Unknown prefix: ' + this.type); @@ -521,6 +518,8 @@ Address.prototype.toJSON = function toJSON(encrypt) { network: network.type, label: this.label, change: this.change, + derived: this.derived, + path: this.path, address: this.getKeyAddress(), scriptAddress: this.getScriptAddress(), key: this.key.toJSON(encrypt), @@ -545,6 +544,8 @@ Address.fromJSON = function fromJSON(json, decrypt) { w = new Address({ label: json.label, change: json.change, + derived: json.derived, + path: json.path, key: bcoin.keypair.fromJSON(json.key, decrypt), type: json.type, subtype: json.subtype, diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 944bebc1..4a464a53 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -163,11 +163,12 @@ function HDPrivateKey(options) { HDPrivateKey.prototype.scan44 = function scan44(options, txByAddress, callback) { var self = this; - var keys = []; - var coinType; + var accounts = []; + var isAccount = this.isAccount44(); + var coinType, root; // 0. get the root node - if (!(this instanceof HDPublicKey)) { + if (!isAccount) { coinType = options.coinType; if (coinType == null) @@ -180,65 +181,77 @@ HDPrivateKey.prototype.scan44 = function scan44(options, txByAddress, callback) .derive(coinType, true); } - return (function scanner(accountIndex) { - var addressIndex = 0; - var total = 0; - var gap = 0; + return (function chainCheck(chainConstant) { + return (function scanner(accountIndex) { + var addressIndex = 0; + var total = 0; + var gap = 0; - // 1. derive the first account's node (index = 0) - var account = (self instanceof HDPublicKey) - ? self - : root.derive(accountIndex, true); + // 1. derive the first account's node (index = 0) + var account = isAccount + ? self + : root.derive(accountIndex, true); - // 2. derive the external chain node of this account - var chain = account.derive(0); + // 2. derive the external chain node of this account + var chain = account.derive(chainConstant); - // 3. scan addresses of the external chain; - // respect the gap limit described below - return (function next() { - var address = chain.derive(addressIndex++); - var addr = bcoin.address.hash2addr( - bcoin.address.key2hash(address.publicKey), - 'pubkey'); + // 3. scan addresses of the external chain; + // respect the gap limit described below + return (function next() { + var address = chain.derive(addressIndex++); + var addr = bcoin.address.key2addr(address.publicKey); - return txByAddress(addr, function(err, txs) { - var result; + return txByAddress(addr, function(err, txs) { + var result; - if (err) - return callback(err); + if (err) + return callback(err); - if (txs) { - if (typeof txs === 'boolean') - result = txs; - else if (Array.isArray(txs)) - result = txs.length > 0; + if (txs) { + if (typeof txs === 'boolean') + result = txs; + else if (Array.isArray(txs)) + result = txs.length > 0; + else + result = false; + } + + if (result) { + total++; + gap = 0; + return next(); + } + + if (++gap < 20) + return next(); + + assert(accounts[accountIndex] == null || chainConstant === 1); + + if (chainConstant === 0) + accounts[accountIndex] = { addressDepth: addressIndex }; else - result = false; - } + accounts[accountIndex].changeDepth = addressIndex; - if (result) { - keys.push(address); - total++; - gap = 0; - return next(); - } + // 4. if no transactions are found on the + // external chain, stop discovery + if (total === 0) { + if (chainConstant === 0) + return chainCheck(1); + return callback(null, accounts); + } - if (++gap < 20) - return next(); + // 5. if there are some transactions, increase + // the account index and go to step 1 + if (isAccount) { + if (chainConstant === 0) + return chainCheck(1); + return callback(null, accounts[0]); + } - // 4. if no transactions are found on the - // external chain, stop discovery - if (total === 0) - return callback(null, keys); - - // 5. if there are some transactions, increase - // the account index and go to step 1 - if (self instanceof HDPublicKey) - return callback(null, keys); - - return scanner(accountIndex + 1); - }); - })(); + return scanner(accountIndex + 1); + }); + })(); + })(0); })(0); }; @@ -315,10 +328,12 @@ HDPrivateKey.prototype.deriveAddress = function deriveAddress(accountIndex, addr }; HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) { - var keys = []; + var cosigners = []; var root; - root = this.derivePurpose45(options); + root = this.isPurpose45() + ? this + : this.derivePurpose45(options); return (function chainCheck(chainConstant) { return (function scanner(cosignerIndex) { @@ -331,9 +346,7 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) return (function next() { var address = chain.derive(addressIndex++); - var addr = bcoin.address.hash2addr( - bcoin.address.key2hash(address.publicKey), - 'pubkey'); + var addr = bcoin.address.key2addr(address.publicKey); return txByAddress(addr, function(err, txs) { var result; @@ -351,7 +364,6 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) } if (result) { - keys.push(address); total++; gap = 0; return next(); @@ -360,13 +372,20 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) if (++gap < 20) return next(); + assert(cosigners[cosignerIndex] == null || chainConstant === 1); + + if (chainConstant === 0) + cosigners[cosignerIndex] = { addressDepth: addressIndex }; + else + cosigners[cosginerIndex].changeDepth = addressIndex; + if (total === 0) { if (chainConstant === 0) return chainCheck(1); - return callback(null, keys); + return callback(null, cosigners); } - return scanner(accountIndex + 1); + return scanner(cosignerIndex + 1); }); })(); })(0); diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index ca071f27..1040da92 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -1453,7 +1453,7 @@ Pool.prototype.sendTX = function sendTX(tx) { // bitcoind nodes. Possibly check // sigops. Call isStandard and/or // isStandardInputs as well. - if (tx.isFull()) { + if (tx.hasPrevout()) { if (!tx.verify(null, true)) { utils.debug( 'Could not relay TX (%s). It does not verify.', diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 9567a7e1..cc08dba8 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -554,6 +554,9 @@ TX.prototype.scriptSig = function scriptSig(index, key, pub, redeem, type) { TX.prototype.isSigned = function isSigned(index, required) { var i, input, s, len, m, j, total; + if (this._signed) + return true; + if (index && typeof index === 'object') index = this.inputs.indexOf(index); @@ -619,6 +622,9 @@ TX.prototype.isSigned = function isSigned(index, required) { continue; } + if (required == null) + continue; + // Unknown total = 0; for (j = 0; j < input.script.length; j++) { @@ -630,13 +636,13 @@ TX.prototype.isSigned = function isSigned(index, required) { return false; } - return true; + return this._signed = true; }; TX.prototype.addOutput = function addOutput(obj, value) { var options, output; - if (obj instanceof bcoin.wallet) + if (obj.getAddress) obj = obj.getAddress(); if (typeof obj === 'string') { @@ -667,47 +673,18 @@ TX.prototype.output = TX.prototype.addOutput; TX.prototype.scriptOutput = function scriptOutput(index, options) { var output, script, keys, m, n, hash, flags; + if (options instanceof bcoin.output) + return; + if (typeof index !== 'number') index = this.outputs.indexOf(index); output = this.outputs[index]; assert(output); - if (!options) - options = output; - script = output.script; - if (options instanceof bcoin.output) { - options = Object.keys(options).reduce(function(out, key) { - out[key] = options[key]; - return out; - }, {}); - } - - if (options.addr) { - options.address = options.addr; - delete options.addr; - } - - if (Array.isArray(options.address)) { - options.keys = options.address.map(function(address) { - return bcoin.wallet.addr2hash(address, 'pubkeyhash'); - }); - delete options.address; - } - - if (options.minSignatures) { - options.m = options.minSignatures; - delete options.minSignatures; - } - - if (options.color) { - options.flags = options.color; - delete options.color; - } - - if (Array.isArray(options.keys)) { + if (options.keys) { // Bare Multisig Transaction // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki @@ -715,13 +692,13 @@ TX.prototype.scriptOutput = function scriptOutput(index, options) { // m [key1] [key2] ... n checkmultisig keys = options.keys.map(utils.toBuffer); - m = options.m || keys.length; + m = options.m; n = options.n || keys.length; if (!(m >= 1 && m <= n)) return; - if (!(n >= 1 && n <= (options.scripthash ? 15 : 3))) + if (!(n >= 1 && n <= (options.scriptHash ? 15 : 3))) return; script = bcoin.script.createMultisig(keys, m, n); @@ -758,7 +735,7 @@ TX.prototype.scriptOutput = function scriptOutput(index, options) { // P2SH Transaction // hash160 [hash] eq - if (options.scripthash) { + if (options.scriptHash) { if (options.locktime != null) { script = [ bcoin.script.array(options.locktime), @@ -1043,7 +1020,7 @@ TX.prototype.maxSize = function maxSize() { TX.prototype.getInputs = function getInputs(unspent, address, fee) { var tx = this.clone(); - var cost = tx.getFunds('output'); + var cost = tx.getOutputValue(); var totalkb = 1; var total = cost.addn(constants.tx.fee); var inputs = []; @@ -1055,6 +1032,11 @@ TX.prototype.getInputs = function getInputs(unspent, address, fee) { this.hardFee = fee; } + // Oldest unspents first + unspent = unspent.slice().sort(function(a, b) { + return b.tx.getConfirmations() - a.tx.getConfirmations(); + }); + function addInput(unspent) { // Add new inputs until TX will have enough // funds to cover both minimum post cost @@ -1062,7 +1044,7 @@ TX.prototype.getInputs = function getInputs(unspent, address, fee) { var index = tx._addInput(unspent); inputs.push(tx.inputs[index]); lastAdded++; - return tx.getFunds('input').cmp(total) < 0; + return tx.getInputValue().cmp(total) < 0; } // Transfer `total` funds maximum. @@ -1084,7 +1066,7 @@ TX.prototype.getInputs = function getInputs(unspent, address, fee) { // break; // } // } - // total = tx.getFunds('output'); + // total = tx.getInputValue(); // } // Change fee value if it is more than 1024 @@ -1098,17 +1080,17 @@ TX.prototype.getInputs = function getInputs(unspent, address, fee) { totalkb += newkb; // Failed to get enough funds, add more inputs. - if (tx.getFunds('input').cmp(total) < 0) + if (tx.getInputValue().cmp(total) < 0) unspent.slice(lastAdded).every(addInput); - } while (tx.getFunds('input').cmp(total) < 0 && lastAdded < unspent.length); + } while (tx.getInputValue().cmp(total) < 0 && lastAdded < unspent.length); } - if (tx.getFunds('input').cmp(total) < 0) { + if (tx.getInputValue().cmp(total) < 0) { // Still failing to get enough funds. inputs = null; } else { // How much money is left after filling outputs. - change = tx.getFunds('input').sub(total); + change = tx.getInputValue().sub(total); } // Return necessary inputs and change. @@ -1253,32 +1235,32 @@ TX.prototype._recalculateFee = function recalculateFee() { }; TX.prototype.getFee = function getFee() { - if (this.getFunds('input').cmp(this.getFunds('output')) < 0) + if (!this.hasPrevout()) return new bn(0); - return this.getFunds('input').sub(this.getFunds('output')); + return this.getInputValue().sub(this.getOutputValue()); }; -TX.prototype.getFunds = function getFunds(side) { +TX.prototype.getInputValue = function getInputValue() { var acc = new bn(0); - var inputs; - if (side === 'in' || side === 'input') { - inputs = this.inputs.filter(function(input) { - return input.prevout.tx; - }); - - if (inputs.length === 0) - return acc; - - inputs.reduce(function(acc, input) { - return acc.iadd(input.prevout.tx.outputs[input.prevout.index].value); - }, acc); + var inputs = this.inputs.filter(function(input) { + return input.prevout.tx; + }); + if (inputs.length === 0) return acc; - } - // Output + inputs.reduce(function(acc, input) { + return acc.iadd(input.prevout.tx.outputs[input.prevout.index].value); + }, acc); + + return acc; +}; + +TX.prototype.getOutputValue = function getOutputValue() { + var acc = new bn(0); + if (this.outputs.length === 0) return acc; @@ -1289,6 +1271,15 @@ TX.prototype.getFunds = function getFunds(side) { return acc; }; +TX.prototype.getFunds = function getFunds(side) { + var acc = new bn(0); + + if (side === 'in' || side === 'input') + return this.getInputValue(); + + return this.getOutputValue(); +}; + // Legacy TX.prototype.funds = TX.prototype.getFunds; @@ -1471,7 +1462,7 @@ TX.prototype.increaseFee = function increaseFee(fee) { } }; -TX.prototype.isFull = function isFull() { +TX.prototype.hasPrevout = function hasPrevout() { if (this.inputs.length === 0) return false; return this.inputs.every(function(input) { @@ -1740,7 +1731,7 @@ TX.prototype.getConfirmations = function getConfirmations() { }; TX.prototype.getValue = function getValue() { - return this.getFunds('output'); + return this.getOutputValue(); }; TX.prototype.__defineGetter__('chain', function() { diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 7532b2ee..2102c047 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -31,7 +31,8 @@ function Wallet(options) { options = utils.merge({}, options); - options.hd = options.hd !== false; + if (options.derivation === 'bip44' || options.derivation === 'bip45') + options.hd = true; if (options.hd && !options.master) { options.master = options.hd === true @@ -40,14 +41,20 @@ function Wallet(options) { delete options.hd; } - if (options.key) + if (options.key) { options.pair = options.key; + delete options.key; + } - if (options.priv) + if (options.priv) { options.privateKey = options.priv; + delete options.priv; + } - if (options.pub) + if (options.pub) { options.publicKey = options.pub; + delete options.pub; + } if ((options.pair instanceof bcoin.hd.privateKey) || (options.pair instanceof bcoin.hd.publicKey)) { @@ -71,22 +78,20 @@ function Wallet(options) { this.addressDepth = options.addressDepth || 0; this.changeDepth = options.changeDepth || 0; this.cosignerIndex = -1; + this.sharedCosignerIndex = constants.hd.hardened - 1; this.purposeKeys = options.purposeKeys || []; this.keys = options.keys || []; - this.normal = false; - this.hd = false; - this.bip44 = false; - this.bip45 = false; - this.multisig = false; - + this.hd = !!this.master; this.type = options.type || 'pubkeyhash'; - this.subtype = options.subtype; + this.subtype = options.subtype || null; + this.derivation = options.derivation || null; + this.compressed = options.compressed !== false; this.keys = []; this.m = options.m || 1; this.n = options.n || 1; this.nmax = this.type === 'scripthash' - ? (this.compressed !== false ? 15 : 7) + ? (this.compressed ? 15 : 7) : 3; if (this.n > 1) { @@ -96,19 +101,17 @@ function Wallet(options) { this.subtype = 'multisig'; } - if (this.master) { - this.hd = true; - if (this.type === 'scripthash' && this.subtype === 'multisig') - this.bip45 = true; - else - this.bip44 = true; - } else { - this.normal = true; + if (!this.derivation) { + if (this.master) { + if (this.type === 'scripthash' && this.subtype === 'multisig') + this.derivation = 'bip45'; + else + this.derivation = 'bip44'; + } else { + this.derivation = 'normal'; + } } - if (this.type === 'multisig' || this.subtype === 'multisig') - this.multisig = true; - if (network.prefixes[this.type] == null) throw new Error('Unknown prefix: ' + this.type); @@ -118,12 +121,12 @@ function Wallet(options) { if (this.n < 1 || this.n > this.nmax) throw new Error('n ranges between 1 and ' + this.nmax); - if (this.bip45) { + if (this.derivation === 'bip45') { this.purposeKey = this.master.isPurpose45() ? this.master : this.master.derivePurpose45(); - } else if (this.bip44) { - this.accountKey = this.master.isAccount44() + } else if (this.derivation === 'bip44') { + this.purposeKey = this.master.isAccount44() ? this.master : this.master.deriveAccount44(this.accountIndex); } @@ -158,12 +161,14 @@ function Wallet(options) { // generate the last receiving address. However, since "normal" wallets // cannot deterministically generate keys, we have to buffer the generated // key for later. - if (this.bip44) { + if (this.derivation === 'bip44') { // Generate the last known receiving address key = this.createKey(false, Math.max(0, this.addressDepth - 1)); - this.current = bcoin.address({ + this.currentAddress = bcoin.address({ privateKey: key.privateKey, publicKey: key.publicKey, + compressed: key.compressed, + path: key.path, type: this.type, subtype: this.subtype, m: this.m, @@ -171,20 +176,23 @@ function Wallet(options) { keys: options.keys, derived: true }); - } else if (this.normal) { + } else 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.current = bcoin.address(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.current = bcoin.address({ + this.currentAddress = bcoin.address({ privateKey: key.privateKey, + publicKey: key.publicKey, + compressed: key.compressed, + path: key.path, type: this.type, subtype: this.subtype, m: this.m, @@ -194,10 +202,10 @@ function Wallet(options) { } } - if (this.bip45) + if (this.derivation === 'bip44' || this.derivation === 'bip45') this.addKey(this.purposeKey); else - this.addKey(this.current.publicKey); + this.addKey(this.currentAddress.publicKey); (options.keys || []).forEach(function(key) { this.addKey(key); @@ -213,7 +221,7 @@ Wallet.prototype._pruneAddresses = function _pruneAddresses(options) { for (i = 0; i < addresses.length; i++) { address = addresses[i]; - if (address === this.current || address === this.changeAddress) + if (address === this.currentAddress || address === this.changeAddress) continue; if (!address.change) @@ -238,17 +246,19 @@ Wallet.prototype._isKeyOptions = function _isKeyOptions(options) { // bip44: Account key address // normal: Address of first key in wallet Wallet.prototype.getID = function getID() { - if (this.bip45) + if (this.derivation === 'bip45') return bcoin.address.key2addr(this.purposeKey.publicKey); - if (this.bip44) - return bcoin.address.key2addr(this.accountKey.publicKey); + if (this.derivation === 'bip44') + return bcoin.address.key2addr(this.purposeKey.publicKey); - if (this.addresses.length) - return this.addresses[0].getKeyAddress(); + if (this.derivation === 'normal') { + if (this.addresses.length) + return this.addresses[0].getKeyAddress(); - if (this._firstKey) - return bcoin.address.key2addr(this._firstKey.publicKey); + if (this._firstKey) + return bcoin.address.key2addr(this._firstKey.publicKey); + } assert(false); }; @@ -260,34 +270,37 @@ Wallet.prototype._initAddresses = function _initAddresses() { assert(!this._initialized); this._initialized = true; - delete this.current; + if (this.copayBIP45) + this.cosignerIndex = this.sharedCosignerIndex; + + delete this.currentAddress; options.addresses.forEach(function(address) { address = this.addAddress(address); - if (!this.hd) { + if (!this.master) { if (!address.change) - this.current = address; + this.currentAddress = address; else this.changeAddress = address; } }, this); - if (this.hd) { + if (this.master) { for (i = 0; i < this.addressDepth; i++) - this.current = this.createAddress(false, i); + this.currentAddress = this.createAddress(false, i); for (i = 0; i < this.changeDepth; i++) this.changeAddress = this.createAddress(true, i); } - if (!this.current) - this.current = this.createAddress(); + if (!this.currentAddress) + this.currentAddress = this.createAddress(); if (!this.changeAddress) this.changeAddress = this.createAddress(true); - assert(this.current); - assert(!this.current.change); + assert(this.currentAddress); + assert(!this.currentAddress.change); assert(this.changeAddress.change); this.prefix = 'bt/wallet/' + this.getID() + '/'; @@ -316,9 +329,16 @@ Wallet.prototype.addKey = function addKey(key) { key = hdKey.publicKey; } - if (this.bip45) { - if (!hdKey || !hdKey.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); + 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; @@ -358,7 +378,7 @@ Wallet.prototype.finalizeKeys = function finalizeKeys(key) { assert(!this._keysFinalized); this._keysFinalized = true; - if (this.bip45) { + if (this.derivation === 'bip44' || this.derivation === 'bip45') { this.purposeKeys = utils.sortHDKeys(this.purposeKeys); for (i = 0; i < this.purposeKeys.length; i++) { @@ -377,7 +397,7 @@ Wallet.prototype.finalizeKeys = function finalizeKeys(key) { this.keys = utils.sortKeys(this.keys); for (i = 0; i < this.keys.length; i++) { - if (utils.isEqual(this.keys[i], this.current.publicKey)) { + if (utils.isEqual(this.keys[i], this.currentAddress.publicKey)) { this.cosignerIndex = i; break; } @@ -409,9 +429,16 @@ Wallet.prototype.removeKey = function removeKey(key) { key = hd.publicKey; } - if (this.bip45) { - if (!hdKey || !hdKey.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); + 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; @@ -464,8 +491,8 @@ Wallet.prototype._init = function init() { // TX using this address was confirmed. // Allocate a new address. if (tx.block) { - if (self.current.ownOutput(tx)) - self.current = self.createAddress(); + if (self.currentAddress.ownOutput(tx)) + self.currentAddress = self.createAddress(); if (self.changeAddress.ownOutput(tx)) self.changeAddress = self.createAddress(true); self._pruneAddresses(); @@ -503,6 +530,9 @@ Wallet.prototype._getAddressTable = function() { Wallet.prototype._addressIndex = function _addressIndex(address) { var addr; + if (typeof address === 'string') + return this._addressTable[addr]; + if (!(address instanceof bcoin.address)) address = bcoin.address(address); @@ -519,6 +549,7 @@ Wallet.prototype._addressIndex = function _addressIndex(address) { return -1; }; +// TODO: fromPath here Wallet.prototype.createAddress = function createAddress(change, index) { var self = this; var key = this.createKey(change, index); @@ -529,6 +560,8 @@ Wallet.prototype.createAddress = function createAddress(change, index) { var options = { privateKey: key.privateKey, publicKey: key.publicKey, + compressed: key.compressed, + path: key.path, type: this.type, subtype: this.subtype, m: this.m, @@ -540,7 +573,7 @@ Wallet.prototype.createAddress = function createAddress(change, index) { if (index == null) { index = change ? self.changeDepth : self.addressDepth; - if (this.hd) { + if (this.master) { if (change) this.changeDepth++; else @@ -548,7 +581,15 @@ Wallet.prototype.createAddress = function createAddress(change, index) { } } - if (this.bip45) { + 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) @@ -557,7 +598,7 @@ Wallet.prototype.createAddress = function createAddress(change, index) { options.keys.push(key.publicKey); }); this.keys = utils.sortKeys(options.keys); - } else { + } else if (this.derivation === 'normal') { this.keys.forEach(function(key, i) { if (i !== this.cosignerIndex) options.keys.push(key); @@ -654,77 +695,120 @@ Wallet.prototype.removeAddress = function removeAddress(address) { }; Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { - return this.current.getPrivateKey(enc); + return this.currentAddress.getPrivateKey(enc); }; Wallet.prototype.getScript = function getScript() { - return this.current.getScript(); + return this.currentAddress.getScript(); }; Wallet.prototype.getScriptHash = function getScriptHash() { - return this.current.getScriptHash(); + return this.currentAddress.getScriptHash(); }; Wallet.prototype.getScriptAddress = function getScriptAddress() { - return this.current.getScriptAddress(); + return this.currentAddress.getScriptAddress(); }; Wallet.prototype.getPublicKey = function getPublicKey(enc) { - return this.current.getPublicKey(enc); + return this.currentAddress.getPublicKey(enc); }; Wallet.prototype.createKey = function createKey(change, index) { var key; - if (!this.hd) { + if (this.derivation === 'normal') { if (this._firstKey) { key = this._firstKey; delete this._firstKey; return key; } - key = bcoin.keypair(); + key = bcoin.keypair({ + compressed: this.compressed + }); return { privateKey: key.privateKey, - publicKey: key.publicKey + publicKey: key.publicKey, + compressed: key.compressed }; } if (index == null) index = change ? this.changeDepth : this.addressDepth; - if (this.bip45) { + if (this.derivation === 'bip44') { key = this.purposeKey - .derive(this.cosignerIndex) .derive(change ? 1 : 0) .derive(index); - } else { - key = this.accountKey + } else if (this.derivation === 'bip45') { + key = this.purposeKey + .derive(this.cosignerIndex) .derive(change ? 1 : 0) .derive(index); } return { privateKey: key.privateKey, - publicKey: key.publicKey + publicKey: key.publicKey, + compressed: true, + path: 'm/' + (change ? 1 : 0) + '/' + index }; }; +Wallet.prototype._deriveKey = function _deriveKey(purposeKey, cosignerIndex, change, index) { + var path, key; + + assert(this.derivation !== 'normal'); + + if (typeof change === 'string') { + path = change; + } else { + if (index == null) + index = change ? this.changeDepth : this.addressDepth; + path = 'm/' + (change ? 1 : 0) + '/' + index; + } + + key = this.derivation === 'bip45' + ? purposeKey.derive(cosignerIndex) + : purposeKey; + + key = key.derive(path); + + key.path = path; + + return key; +}; + +Wallet.prototype.setAddressDepth = function setAddressDepth(depth) { + assert(this.derivation !== 'normal'); + for (var i = this.addressDepth; i < depth; i++) + this.currentAddress = this.createAddress(false, i); + this.addressDepth = depth; +}; + +Wallet.prototype.setChangeDepth = function setChangeDepth(depth) { + assert(this.derivation !== 'normal'); + for (var i = this.addressDepth; i < depth; i++) + this.changeAddress = this.createAddress(true, i); + this.changeDepth = depth; +}; + Wallet.prototype.getKeyHash = Wallet.prototype.getKeyhash = function getKeyhash() { - return this.current.getKeyHash(); + return this.currentAddress.getKeyHash(); }; Wallet.prototype.getKeyAddress = Wallet.prototype.getKeyaddress = function getKeyaddress() { - return this.current.getKeyAddress(); + return this.currentAddress.getKeyAddress(); }; Wallet.prototype.getHash = function getHash() { - return this.current.getHash(); + return this.currentAddress.getHash(); }; Wallet.prototype.getAddress = function getAddress() { - return this.current.getAddress(); + return this.currentAddress.getAddress(); }; Wallet.prototype.ownInput = function ownInput(tx, index) { @@ -746,19 +830,15 @@ Wallet.prototype.fill = function fill(tx, address, fee) { unspent = this.getUnspent(); - // Avoid multisig if first address is not multisig items = unspent.filter(function(item) { var output = item.tx.outputs[item.index]; - if (bcoin.script.isScripthash(output.script)) { - if (this.current.type === 'scripthash') - return true; - return false; - } - if (bcoin.script.isMultisig(output.script)) { - if (this.current.n > 1) - return true; - return false; - } + + if (bcoin.script.isScripthash(output.script)) + return this.type === 'scripthash'; + + if (bcoin.script.isMultisig(output.script)) + return this.type === 'multisig'; + return true; }, this); @@ -819,9 +899,6 @@ Wallet.prototype.createTX = function createTX(outputs, fee) { else tx.avoidFeeSnipping(); - // Sign the inputs - this.sign(tx); - return tx; }; @@ -949,6 +1026,7 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { subtype: this.subtype, m: this.m, n: this.n, + derivation: this.derivation, accountIndex: this.accountIndex, addressDepth: this.addressDepth, changeDepth: this.changeDepth, @@ -959,7 +1037,7 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { }, this).map(function(address) { return address.toJSON(encrypt); }), - keys: this.bip45 + keys: this.derivation === 'bip44' || this.derivation === 'bip45' ? this.purposeKeys.map(function(key) { return key.xpubkey; }) @@ -972,7 +1050,7 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { }; Wallet.fromJSON = function fromJSON(json, decrypt) { - var w; + var wallet; assert.equal(json.v, 3); assert.equal(json.name, 'wallet'); @@ -980,11 +1058,12 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { if (json.network) assert.equal(json.network, network.type); - w = new Wallet({ + wallet = new Wallet({ type: json.type, subtype: json.subtype, m: json.m, n: json.n, + derivation: json.derivation, accountIndex: json.accountIndex, addressDepth: json.addressDepth, changeDepth: json.changeDepth, @@ -998,9 +1077,9 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { keys: json.keys }); - w.tx.fromJSON(json.tx); + wallet.tx.fromJSON(json.tx); - return w; + return wallet; }; // Compat - Legacy diff --git a/test/wallet-test.js b/test/wallet-test.js index 8b0a523b..52b05485 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -87,7 +87,7 @@ describe('Wallet', function() { var src = bcoin.tx({ outputs: [{ value: 5460 * 2, - minSignatures: 1, + m: 1, keys: [ w.getPublicKey(), k2 ] }, { value: 5460 * 2, @@ -269,7 +269,7 @@ describe('Wallet', function() { it('should verify 2-of-3 p2sh tx', function(cb) { // Create 3 2-of-3 wallets with our pubkeys as "shared keys" var w1 = bcoin.wallet({ - hd: true, + derivation: 'bip44', type: 'scripthash', subtype: 'multisig', m: 2, @@ -277,7 +277,7 @@ describe('Wallet', function() { }); var w2 = bcoin.wallet({ - hd: true, + derivation: 'bip44', type: 'scripthash', subtype: 'multisig', m: 2, @@ -285,7 +285,7 @@ describe('Wallet', function() { }); var w3 = bcoin.wallet({ - hd: true, + derivation: 'bip44', type: 'scripthash', subtype: 'multisig', m: 2,