diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index f2963959..dce71a3c 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -168,6 +168,7 @@ function Environment(options) { this.hd = require('./hd'); this.keyring = require('./keyring'); this.wallet = require('./wallet'); + this.account = this.wallet.Account; this.walletdb = require('./walletdb'); this.provider = this.walletdb.Provider; this.peer = require('./peer'); diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index a9d59b5e..83be1dc5 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -1129,22 +1129,35 @@ MTX.prototype.selectCoins = function selectCoins(coins, options) { */ MTX.prototype.fill = function fill(coins, options) { - var result, i, change; + var result, i, change, changeAddress; assert(this.inputs.length === 0, 'TX is already filled.'); - assert(options, '`options` are required.'); - assert(options.changeAddress, '`changeAddress` is required.'); + + if (!options) + options = {}; // Select necessary coins. result = this.selectCoins(coins, options); + // We need a change address. + changeAddress = options.changeAddress; + + // If change address is not available, + // send back to one of the coins' addresses. + for (i = 0; i < result.coins.length && !changeAddress; i++) + changeAddress = result.coins[i].getAddress(); + + // Will only happen in rare cases where + // we're redeeming all non-standard coins. + assert(changeAddress, 'No change address available.'); + // Add coins to transaction. for (i = 0; i < result.coins.length; i++) this.addInput(result.coins[i]); // Add a change output. this.addOutput({ - address: options.changeAddress, + address: changeAddress, value: result.change }); diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 9dcf664d..43b688de 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -1726,35 +1726,31 @@ TXDB.prototype.getBalance = function getBalance(address, callback) { /** * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Number} now - Current time. * @param {Number} age - Age delta (delete transactions older than `now - age`). * @param {Function} callback */ -TXDB.prototype.zap = function zap(address, now, age, callback, force) { +TXDB.prototype.zap = function zap(address, age, callback, force) { var self = this; if (typeof address !== 'string') { force = callback; callback = age; - age = now; - now = address; + age = address; address = null; } - var unlock = this._lock(zap, [address, now, age, callback], force); + var unlock = this._lock(zap, [address, age, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); - assert(utils.isNumber(now)); assert(utils.isNumber(age)); - assert(now >= age); return this.getRange(address, { start: 0, - end: now - age + end: bcoin.now() - age }, function(err, txs) { if (err) return callback(err); diff --git a/lib/bcoin/types.js b/lib/bcoin/types.js index 244d3d6e..9bc8c3ea 100644 --- a/lib/bcoin/types.js +++ b/lib/bcoin/types.js @@ -4,6 +4,15 @@ * @global */ +/** + * @typedef {Object} Path + * @property {String} name - Account name. + * @property {Number} account - Account index. + * @property {Number} change - Change chain. + * @property {Number} index - Address index. + * @global + */ + /** * @typedef {Object} InvItem * @property {Number|String} type - Inv type. See {@link constants.inv}. diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 3a2cfbc7..98afee98 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2277,7 +2277,7 @@ utils.parallel = function parallel(stack, callback) { for (i = 0; i < stack.length; i++) { try { - if (stack[i].length >= 2) { + if (0 && stack[i].length >= 2) { stack[i](error, next); error = null; } else { @@ -2306,6 +2306,7 @@ utils.serial = function serial(stack, callback) { if (!cb) return callback(err); + if (0) if (cb.length >= 2) { try { return cb(err, next); diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 94e25daa..aa03b603 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -14,7 +14,7 @@ var BufferReader = require('./reader'); var BufferWriter = require('./writer'); /** - * HD BIP-44/45 wallet + * BIP44 Wallet * @exports Wallet * @constructor * @param {Object} options @@ -71,58 +71,17 @@ function Wallet(options) { this.id = options.id || null; this.master = options.master || null; - this.witness = options.witness || false; + this.accountDepth = options.accountDepth || 0; this.loaded = false; this.loading = false; - - this.accountKey = options.accountKey || null; - this.accountIndex = options.accountIndex || 0; - this.receiveDepth = options.receiveDepth || 1; - this.changeDepth = options.changeDepth || 1; - this.receiveAddress = null; - this.changeAddress = null; - - this.lookahead = options.lookahead != null ? options.lookahead : 5; + this.account = null; this.initialized = options.initialized || false; - this.type = options.type || 'pubkeyhash'; - this.compressed = options.compressed !== false; - this.keys = []; - this.m = options.m || 1; - this.n = options.n || 1; - - this.cache = new bcoin.lru(20, 1); - - if (this.n > 1) - this.type = 'multisig'; - - assert(this.type === 'pubkeyhash' || this.type === 'multisig', - '`type` must be multisig or pubkeyhash.'); - - if (this.m < 1 || this.m > this.n) - throw new Error('m ranges between 1 and n'); - - if (!this.accountKey) { - assert(this.master.key); - key = this.master.key.deriveAccount44(this.accountIndex); - this.accountKey = key.hdPublicKey; - } - if (!this.id) this.id = this.getID(); - if (options.passphrase) - this.master.encrypt(options.passphrase); - // Non-alphanumeric IDs will break leveldb sorting. assert(/^[a-zA-Z0-9]+$/.test(this.id), 'Wallet IDs must be alphanumeric.'); - - this.pushKey(this.accountKey); - - if (options.keys) { - for (i = 0; i < options.keys.length; i++) - this.pushKey(options.keys[i]); - } } utils.inherits(Wallet, EventEmitter); @@ -148,7 +107,7 @@ Wallet.prototype.open = function open(callback) { try { this.db.register(this); } catch (e) { - this.emit('error', err); + this.emit('error', e); return callback(err); } @@ -172,6 +131,26 @@ Wallet.prototype.open = function open(callback) { * @param {Function} callback */ +Wallet.prototype.__defineGetter__('receiveDepth', function() { + return this.account.receiveDepth; +}); + +Wallet.prototype.__defineGetter__('changeDepth', function() { + return this.account.changeDepth; +}); + +Wallet.prototype.__defineGetter__('accountKey', function() { + return this.account.accountKey; +}); + +Wallet.prototype.__defineGetter__('receiveAddress', function() { + return this.account.receiveAddress; +}); + +Wallet.prototype.__defineGetter__('changeAddress', function() { + return this.account.changeAddress; +}); + Wallet.prototype.close = Wallet.prototype.destroy = function destroy(callback) { callback = utils.ensure(callback); @@ -184,7 +163,7 @@ Wallet.prototype.destroy = function destroy(callback) { try { this.db.unregister(this); } catch (e) { - this.emit('error', err); + this.emit('error', e); return callback(err); } @@ -203,168 +182,44 @@ Wallet.prototype.destroy = function destroy(callback) { Wallet.prototype.init = function init(callback) { var self = this; - var addresses = []; - var i; + var options; - // Waiting for more keys. - if (this.keys.length !== this.n) { - assert(!this.initialized); - return this.db.open(function(err) { - if (err) - return callback(err); - self.save(callback); - }); + function done(err, account) { + if (err) + return callback(err); + + if (!account) + return callback(new Error('Account not found.')); + + self.account = account; + + if (self.options.passphrase) + self.master.encrypt(self.options.passphrase); + + if (Buffer.isBuffer(self.options.passphrase)) + self.options.passphrase.fill(0); + + self.options.passphrase = null; + + return callback(); } - if (this.initialized) { - this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); - this.changeAddress = this.deriveChange(this.changeDepth - 1); - return this.db.open(callback); - } - - this.initialized = true; - - for (i = 0; i < this.receiveDepth - 1; i++) - addresses.push(this.deriveReceive(i)); - - for (i = 0; i < this.changeDepth - 1; i++) - addresses.push(this.deriveChange(i)); - - for (i = this.receiveDepth; i < this.receiveDepth + this.lookahead; i++) - addresses.push(this.deriveReceive(i)); - - for (i = this.changeDepth; i < this.changeDepth + this.lookahead; i++) - addresses.push(this.deriveChange(i)); - - this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); - this.changeAddress = this.deriveChange(this.changeDepth - 1); - - addresses.push(this.receiveAddress); - addresses.push(this.changeAddress); - return this.db.open(function(err) { if (err) return callback(err); - return self.saveAddress(addresses, function(err) { - if (err) - return callback(err); - return self.save(callback); - }); + + if (self.initialized) + return self.getAccount(0, done); + + self.initialized = true; + + options = utils.merge({}, self.options); + options.name = 'default'; + + self.createAccount(options, done); }); }; -/** - * Add a public account key to the wallet (multisig). - * Does not update the database. - * @param {HDPublicKey} key - Account (bip44) - * key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ - -Wallet.prototype.pushKey = function pushKey(key) { - var result = false; - var index = -1; - var i; - - assert(key, 'Key required.'); - - if (Array.isArray(key)) { - for (i = 0; i < key.length; i++) { - if (this.pushKey(key[i])) - result = true; - } - return result; - } - - if (key instanceof bcoin.wallet) - key = key.accountKey; - - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(key); - - if (key.hdPublicKey) - key = key.hdPublicKey; - - if (!bcoin.hd.isHD(key)) - throw new Error('Must add HD keys to wallet.'); - - if (!key.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - - for (i = 0; i < this.keys.length; i++) { - if (this.keys[i].equal(key)) { - index = i; - break; - } - } - - if (index !== -1) - return false; - - if (this.keys.length === this.n) - throw new Error('Cannot add more keys.'); - - this.keys.push(key); - - return true; -}; - -/** - * Remove a public account key to the wallet (multisig). - * Does not update the database. - * @param {HDPublicKey} key - Account (bip44) - * key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ - -Wallet.prototype.spliceKey = function spliceKey(key) { - var result = false; - var index = -1; - var i; - - if (Array.isArray(key)) { - for (i = 0; i < key.length; i++) { - if (this.spliceKey(key[i])) - result = true; - } - return result; - } - - assert(key, 'Key required.'); - - if (key instanceof bcoin.wallet) - key = key.accountKey; - - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(key); - - if (key.hdPublicKey) - key = key.hdPublicKey; - - if (!bcoin.hd.isHD(key)) - throw new Error('Must add HD keys to wallet.'); - - if (!key.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - - for (i = 0; i < this.keys.length; i++) { - if (this.keys[i].equal(key)) { - index = i; - break; - } - } - - if (index === -1) - return false; - - if (this.keys.length === this.n) - throw new Error('Cannot remove key.'); - - this.keys.splice(index, 1); - - return true; -}; - /** * Add a public account key to the wallet (multisig). * Saves the key in the wallet database. @@ -372,23 +227,20 @@ Wallet.prototype.spliceKey = function spliceKey(key) { * @param {Function} callback */ -Wallet.prototype.addKey = function addKey(key, callback) { - var result = false; - - try { - result = this.pushKey(key); - } catch (e) { - return callback(e); +Wallet.prototype.addKey = function addKey(account, key, callback) { + if (typeof key === 'function') { + callback = key; + key = account; + account = 0; } - - if (!result) - return callback(null, result); - - this.init(function(err) { + this.getAccount(account, function(err, account) { if (err) return callback(err); - return callback(null, result); + if (!account) + return callback(new Error('Account not found.')); + + account.addKey(key, callback); }); }; @@ -399,165 +251,151 @@ Wallet.prototype.addKey = function addKey(key, callback) { * @param {Function} callback */ -Wallet.prototype.removeKey = function removeKey(key, callback) { - var result = false; - - try { - result = this.spliceKey(key); - } catch (e) { - return callback(e); +Wallet.prototype.removeKey = function removeKey(account, key, callback) { + if (typeof key === 'function') { + callback = key; + key = account; + account = 0; } - - if (!result) - return callback(null, result); - - this.save(function(err) { + this.getAccount(account, function(err, account) { if (err) return callback(err); - return callback(null, result); + if (!account) + return callback(new Error('Account not found.')); + + account.addKey(key, callback); }); }; /** - * Get the wallet ID which is either the passed in `id` - * option, or the account/purpose key converted to an - * address with a prefix of `0x03be04` (`WLT`). + * Generate the wallet ID if none was passed in. + * It is represented as `m/44'` (public) hashed + * and converted to an address with a prefix + * of `0x03be04` (`WLT` in base58). * @returns {Base58String} */ Wallet.prototype.getID = function getID() { - var publicKey = this.accountKey.publicKey; - var p; + var key, p; + + assert(this.master.key, 'Cannot derive id.'); + + key = this.master.key.derive(44, true); p = new BufferWriter(); p.writeU8(0x03); p.writeU8(0xbe); p.writeU8(0x04); - p.writeBytes(utils.ripesha(publicKey)); + p.writeBytes(utils.ripesha(key.publicKey)); p.writeChecksum(); return utils.toBase58(p.render()); }; /** - * Create a new receiving address (increments receiveDepth). - * @returns {KeyRing} + * Create an account. Requires passphrase if master key is encrypted. + * @param {Object} options - See {@link Account} options. + * @param {Function} callback - Returns [Error, {@link Account}]. */ -Wallet.prototype.createReceive = function createReceive(callback) { - return this.createAddress(false, callback); -}; - -/** - * Create a new change address (increments receiveDepth). - * @returns {KeyRing} - */ - -Wallet.prototype.createChange = function createChange(callback) { - return this.createAddress(true, callback); -}; - -/** - * Create a new address (increments depth). - * @param {Boolean} change - * @returns {KeyRing} - */ - -Wallet.prototype.createAddress = function createAddress(change, callback) { +Wallet.prototype.createAccount = function createAccount(options, callback) { var self = this; - var addresses = []; - var address; + var master, key; - if (typeof change === 'function') { - callback = change; - change = null; + try { + master = this.master.decrypt(options.passphrase); + } catch (e) { + return callback(e); } - if (change) { - address = this.deriveChange(this.changeDepth); - addresses.push(address); - addresses.push(this.deriveChange(this.changeDepth + this.lookahead)); - this.changeDepth++; - this.changeAddress = address; - } else { - address = this.deriveReceive(this.receiveDepth); - addresses.push(address); - addresses.push(this.deriveReceive(this.receiveDepth + this.lookahead)); - this.receiveDepth++; - this.receiveAddress = address; - } + key = master.deriveAccount44(this.accountDepth); - this.saveAddress(addresses, function(err) { + options = utils.merge({}, options, { + wid: this.id, + accountIndex: this.accountDepth, + accountKey: key.hdPublicKey, + initialized: false + }); + + this.db.createAccount(options, function(err, account) { if (err) return callback(err); + self.accountDepth++; + self.save(function(err) { if (err) return callback(err); - return callback(null, address); + return callback(null, account); }); }); }; /** - * Derive a receiving address at `index`. Do not increment depth. - * @param {Number} index - * @returns {KeyRing} + * Retrieve an account from the database. + * @param {Number|String} account + * @param {Function} callback - Returns [Error, {@link Account}]. */ -Wallet.prototype.deriveReceive = function deriveReceive(index) { - return this.deriveAddress(false, index); -}; - -/** - * Derive a change address at `index`. Do not increment depth. - * @param {Number} index - * @returns {KeyRing} - */ - -Wallet.prototype.deriveChange = function deriveChange(index) { - return this.deriveAddress(true, index); -}; - -/** - * Derive an address at `index`. Do not increment depth. - * @param {Boolean} change - Whether the address on the change branch. - * @param {Number} index - * @returns {KeyRing} - */ - -Wallet.prototype.deriveAddress = function deriveAddress(change, index) { - var self = this; - var i, key, options; - - assert(this.initialized); - - change = +change; - - key = this.accountKey.derive(change).derive(index); - - options = { - network: this.network, - key: key.publicKey, - account: this.accountIndex, - change: change, - index: index, - type: this.type, - name: 'default', - witness: this.witness, - m: this.m, - n: this.n, - keys: [] - }; - - for (i = 0; i < this.keys.length; i++) { - key = this.keys[i]; - key = key.derive(change).derive(index); - options.keys.push(key.publicKey); +Wallet.prototype.getAccount = function getAccount(account, callback) { + if (this.account) { + if (account === 0 || account === 'default') + return callback(null, this.account); } + this.db.getAccount(this.id, account, callback); +}; - return new bcoin.keyring(options); +/** + * Create a new receiving address (increments receiveDepth). + * @param {(Number|String)?} account + * @param {Function} callback - Returns [Error, {@link KeyRing}]. + */ + +Wallet.prototype.createReceive = function createReceive(account, callback) { + if (typeof account === 'function') { + callback = account; + account = 0; + } + return this.createAddress(account, false, callback); +}; + +/** + * Create a new change address (increments receiveDepth). + * @param {(Number|String)?} account + * @param {Function} callback - Returns [Error, {@link KeyRing}]. + */ + +Wallet.prototype.createChange = function createChange(account, callback) { + if (typeof account === 'function') { + callback = account; + account = 0; + } + return this.createAddress(account, true, callback); +}; + +/** + * Create a new address (increments depth). + * @param {(Number|String)?} account + * @param {Boolean} change + * @param {Function} callback - Returns [Error, {@link KeyRing}]. + */ + +Wallet.prototype.createAddress = function createAddress(account, change, callback) { + if (typeof change === 'function') { + callback = change; + change = account; + account = 0; + } + this.getAccount(account, function(err, account) { + if (err) + return callback(err); + + if (!account) + return callback(new Error('Account not found.')); + + account.createAddress(change, callback); + }); }; /** @@ -570,102 +408,16 @@ Wallet.prototype.save = function save(callback) { this.db.save(this, callback); }; -/** - * Save addresses to path map. - * @param {KeyRing[]} address - * @param {Function} callback - */ - -Wallet.prototype.saveAddress = function saveAddress(address, callback) { - this.db.saveAddress(this.id, address, callback); -}; - /** * Test whether the wallet possesses an address. * @param {Base58Address} address - * @returns {Boolean} + * @param {Function} callback - Returns [Error, Boolean]. */ Wallet.prototype.hasAddress = function hasAddress(address, callback) { this.db.hasAddress(this.id, address, callback); }; -/** - * Set receiving depth (depth is the index of the _next_ address). - * Allocate all addresses up to depth. Note that this also allocates - * new lookahead addresses. - * @param {Number} depth - * @returns {Boolean} True if new addresses were allocated. - */ - -Wallet.prototype.setReceiveDepth = function setReceiveDepth(depth, callback) { - var self = this; - var addresses = []; - var i; - - if (!(depth > this.receiveDepth)) - return callback(null, false); - - for (i = this.receiveDepth; i < depth; i++) { - this.receiveAddress = this.deriveReceive(i); - addresses.push(this.receiveAddress); - } - - for (i = this.receiveDepth + this.lookahead; i < depth + this.lookahead; i++) - addresses.push(this.deriveReceive(i)); - - this.receiveDepth = depth; - - this.saveAddress(addresses, function(err) { - if (err) - return callback(err); - - self.save(function(err) { - if (err) - return callback(err); - - return callback(null, true); - }); - }); -}; - -/** - * Set change depth (depth is the index of the _next_ address). - * Allocate all addresses up to depth. Note that this also allocates - * new lookahead addresses. - * @param {Number} depth - * @returns {Boolean} True if new addresses were allocated. - */ - -Wallet.prototype.setChangeDepth = function setChangeDepth(depth, callback) { - var self = this; - var addresses = []; - var i; - - if (!(depth > this.changeDepth)) - return callback(null, false); - - for (i = this.changeDepth; i < depth; i++) { - this.changeAddress = this.deriveChange(i); - addresses.push(this.changeAddress); - } - - for (i = this.changeDepth + this.lookahead; i < depth + this.lookahead; i++) - addresses.push(this.deriveChange(i)); - - this.changeDepth = depth; - - this.saveAddress(addresses, function(err) { - if (err) - return callback(err); - self.save(function(err) { - if (err) - return callback(err); - return callback(null, true); - }); - }); -}; - /** * Fill a transaction with inputs, estimate * transaction size, calculate fee, and add a change output. @@ -673,6 +425,8 @@ Wallet.prototype.setChangeDepth = function setChangeDepth(depth, callback) { * @see MTX#fill * @param {MTX} tx - _Must_ be a mutable transaction. * @param {Object?} options + * @param {(String|Number)?} options.account - If no account is + * specified, coins from the entire wallet will be filled. * @param {String?} options.selection - Coin selection priority. Can * be `age`, `random`, or `all`. (default=age). * @param {Boolean} options.round - Whether to round to the nearest @@ -701,7 +455,7 @@ Wallet.prototype.fill = function fill(tx, options, callback) { if (!this.initialized) return callback(new Error('Cannot use uninitialized wallet.')); - this.getCoins(function(err, coins) { + this.getCoins(options.account, function(err, coins) { if (err) return callback(err); @@ -713,7 +467,7 @@ Wallet.prototype.fill = function fill(tx, options, callback) { free: options.free, fee: options.fee, subtractFee: options.subtractFee, - changeAddress: self.changeAddress.getAddress(), + changeAddress: self.account.changeAddress.getAddress(), height: self.network.height, rate: options.rate != null ? options.rate @@ -853,18 +607,31 @@ Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) { if (err) return callback(err); - for (i = 0; i < paths.length; i++) { - path = paths[i]; - addresses.push(self.deriveAddress(path.change, path.index)); - } + utils.forEachSerial(paths, function(path, next) { + self.getAccount(path.account, function(err, account) { + if (err) + return next(err); - return callback(null, addresses); + if (!account) + return next(); + + addresses.push(account.deriveAddress(path.change, path.index)); + + return next(); + }); + }, function(err) { + if (err) + return callback(err); + + return callback(null, addresses); + }); }); }; /** - * Get path by address. - * @param {Base58Address} address - Base58 address. + * Get path by address hash. + * @param {Hash} address + * @param {Function} callback - Returns [Error, {@link Path}]. */ Wallet.prototype.getPath = function getPath(address, callback) { @@ -874,8 +641,7 @@ Wallet.prototype.getPath = function getPath(address, callback) { /** * Map input addresses to paths. * @param {TX|Input} tx - * @param {Number?} index - * @returns {String[]} + * @param {Function} callback - Returns [Error, {@link Path}[]]. */ Wallet.prototype.getInputPaths = function getInputPaths(tx, callback) { @@ -922,9 +688,8 @@ Wallet.prototype.getInputPaths = function getInputPaths(tx, callback) { /** * Map output addresses to paths. - * @param {TX|Output} - * @param {Number?} index - * @returns {String[]} + * @param {TX|Output} tx + * @param {Function} callback - Returns [Error, {@link Path}[]]. */ Wallet.prototype.getOutputPaths = function getOutputPaths(tx, callback) { @@ -962,14 +727,18 @@ Wallet.prototype.getOutputPaths = function getOutputPaths(tx, callback) { }; /** - * Get the maximum address depth based on a transactions outputs. + * Sync address depths based on a transaction's outputs. + * This is used for deriving new addresses when + * a confirmed transaction is seen. * @param {TX} tx - * @returns {Object} { changeDepth: Number, receiveDepth: Number } + * @param {Function} callback - Returns [Errr, Boolean] + * (true if new addresses were allocated). */ -Wallet.prototype.getOutputDepth = function getOutputDepth(tx, callback) { +Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { var self = this; - var depth = { changeDepth: -1, receiveDepth: -1 }; + var accounts = {}; + var result = false; var i, path; this.getOutputPaths(tx, function(err, paths) { @@ -978,54 +747,62 @@ Wallet.prototype.getOutputDepth = function getOutputDepth(tx, callback) { for (i = 0; i < paths.length; i++) { path = paths[i]; - if (path.change) { - if (path.index > depth.changeDepth) - depth.changeDepth = path.index; - } else { - if (path.index > depth.receiveDepth) - depth.receiveDepth = path.index; - } + + if (!accounts[path.account]) + accounts[path.account] = []; + + accounts[path.account].push(path); } - depth.changeDepth++; - depth.receiveDepth++; + utils.forEachSerial(Object.keys(accounts), function(index, next) { + var paths = accounts[index]; + var depth = { changeDepth: -1, receiveDepth: -1 }; - return callback(null, depth); - }); -}; + for (i = 0; i < paths.length; i++) { + path = paths[i]; -/** - * Sync address depths based on a transaction's outputs. - * This is used for deriving new addresses when - * a confirmed transaction is seen. - * @param {TX} tx - * @returns {Boolean} Whether new addresses were allocated. - */ + if (path.change) { + if (path.index > depth.changeDepth) + depth.changeDepth = path.index; + } else { + if (path.index > depth.receiveDepth) + depth.receiveDepth = path.index; + } + } -Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { - var self = this; - var result = false; + depth.changeDepth++; + depth.receiveDepth++; - this.getOutputDepth(tx, function(err, depth) { - if (err) - return callback(err); + self.getAccount(+index, function(err, account) { + if (err) + return next(err); - self.setChangeDepth(depth.changeDepth + 1, function(err, res) { + if (!account) + return next(); + + account.setChangeDepth(depth.changeDepth + 1, function(err, res) { + if (err) + return next(err); + + if (res) + result = true; + + account.setReceiveDepth(depth.receiveDepth + 1, function(err, res) { + if (err) + return next(err); + + if (res) + result = true; + + next(); + }); + }); + }); + }, function(err) { if (err) return callback(err); - if (res) - result = true; - - self.setReceiveDepth(depth.receiveDepth + 1, function(err, res) { - if (err) - return callback(err); - - if (res) - result = true; - - return callback(null, result); - }); + return callback(null, result); }); }); }; @@ -1050,121 +827,76 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) { if (!path) return callback(); - address = self.deriveAddress(path); + self.getAccount(path.account, function(err, account) { + if (err) + return callback(err); - if (address.program && hash.length === 20) { - if (utils.equal(hash, address.programHash)) - return callback(null, address.program); - } + if (!account) + return callback(); - return callback(null, address.script); + address = account.deriveAddress(path.change, path.index); + + if (address.program && hash.length === 20) { + if (utils.equal(hash, address.programHash)) + return callback(null, address.program); + } + + return callback(null, address.script); + }); }); }; /** * Zap stale TXs from wallet (accesses db). - * @param {Number} now - Current time (unix time). * @param {Number} age - Age threshold (unix time, default=72 hours). * @param {Function} callback - Returns [Error]. */ -Wallet.prototype.zap = function zap(now, age, callback) { - return this.db.zap(this.id, now, age, callback); +Wallet.prototype.zap = function zap(account, age, callback) { + var self = this; + + if (typeof age === 'function') { + callback = age; + age = account; + account = 0; + } + + this._getID(account, callback, function(id, callback) { + self.db.zap(id, age, callback); + }); }; /** * Scan for addresses. * @param {Function} getByAddress - Must be a callback which accepts * a callback and returns transactions by address. - * @param {Function} callback - Returns [Boolean, TX[]]. + * @param {Function} callback - Returns [Error, Boolean, TX[]]. */ Wallet.prototype.scan = function scan(getByAddress, callback) { var self = this; var result = false; + var txs = []; - return this._scan(getByAddress, function(err, depth, txs) { - if (err) - return callback(err); - - self.setChangeDepth(depth.changeDepth + 1, function(err, res) { + (function next() { + self.createAccount(self.options, function(err, account) { if (err) return callback(err); - if (res) - result = true; - - self.setReceiveDepth(depth.receiveDepth + 1, function(err, res) { + account.scan(getByAddress, function(err, res, tx) { if (err) return callback(err); - if (res) - result = true; + if (!res) + return callback(null, result, txs); - return callback(null, result, txs); + result = true; + txs = txs.concat(tx); + + next(); }); }); - }); -}; - -Wallet.prototype._scan = function _scan(getByAddress, callback) { - var self = this; - var depth = { changeDepth: 0, receiveDepth: 0 }; - var all = []; - - assert(this.initialized); - - (function chainCheck(change) { - var addressIndex = 0; - var total = 0; - var gap = 0; - - (function next() { - var address = self.deriveAddress(change, addressIndex++); - - getByAddress(address.getAddress(), function(err, txs) { - var result; - - if (err) - return callback(err); - - if (txs) { - if (typeof txs === 'boolean') - result = txs; - else if (typeof txs === 'number') - result = txs > 0; - else if (Array.isArray(txs)) - result = txs.length > 0; - else - result = false; - - if (Array.isArray(txs) && (txs[0] instanceof bcoin.tx)) - all = all.concat(txs); - } - - if (result) { - total++; - gap = 0; - return next(); - } - - if (++gap < 20) - return next(); - - assert(depth.receiveDepth === 0 || change === true); - - if (change === false) - depth.receiveDepth = addressIndex - gap; - else - depth.changeDepth = addressIndex - gap; - - if (change === false) - return chainCheck(true); - - return callback(null, depth, all); - }); - })(); - })(false); + })(); }; /** @@ -1174,7 +906,8 @@ Wallet.prototype._scan = function _scan(getByAddress, callback) { * @param {MTX} tx * @param {Number?} index - Index of input. If not present, * it will attempt to sign all redeemable inputs. - * @returns {Number} Total number of scripts built. + * @param {Function} callback - Returns [Error, Number] + * (total number of scripts built). */ Wallet.prototype.scriptInputs = function scriptInputs(tx, callback) { @@ -1200,7 +933,8 @@ Wallet.prototype.scriptInputs = function scriptInputs(tx, callback) { * @param {Number?} index - Index of input. If not present, * it will attempt to build and sign all redeemable inputs. * @param {SighashType?} type - * @returns {Number} Total number of inputs scripts built and signed. + * @param {Function} callback - Returns [Error, Number] (total number + * of inputs scripts built and signed). */ Wallet.prototype.sign = function sign(tx, options, callback) { @@ -1233,11 +967,10 @@ Wallet.prototype.sign = function sign(tx, options, callback) { return callback(null, 0); } - master = master.deriveAccount44(self.accountIndex); - for (i = 0; i < addresses.length; i++) { address = addresses[i]; - key = master.derive(address.change).derive(address.index); + key = master.deriveAccount44(address.account); + key = key.derive(address.change).derive(address.index); assert(utils.equal(key.getPublicKey(), address.key)); total += address.sign(tx, key, options.index, options.type); } @@ -1261,68 +994,140 @@ Wallet.prototype.addTX = function addTX(tx, callback) { * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -Wallet.prototype.getHistory = function getHistory(callback) { - return this.db.getHistory(this.id, callback); +Wallet.prototype.getHistory = function getHistory(account, callback) { + var self = this; + this._getID(account, callback, function(id, callback) { + self.db.getHistory(id, callback); + }); +}; + +/** + * Parse arguments and return an id + * consisting of `walletid/accountname`. + * @private + * @param {String|Number} account + * @param {Function} errback + * @param {Function} callback - Returns [String, Function]. + */ + +Wallet.prototype._getID = function _getID(account, errback, callback) { + var self = this; + + if (typeof account === 'function') { + errback = account; + account = null; + } + + if (account == null) + return callback(this.id, errback); + + this.db.getAccountIndex(this.id, account, function(err, index) { + if (err) + return errback(err); + + if (index === -1) + return errback(new Error('Account not found.')); + + return callback(self.id + '/' + index, errback); + }); }; /** * Get all available coins (accesses db). + * @param {(String|Number)?} account * @param {Function} callback - Returns [Error, {@link Coin}[]]. */ -Wallet.prototype.getCoins = function getCoins(callback) { - return this.db.getCoins(this.id, callback); +Wallet.prototype.getCoins = function getCoins(account, callback) { + var self = this; + this._getID(account, callback, function(id, callback) { + self.db.getCoins(id, callback); + }); }; /** * Get all pending/unconfirmed transactions (accesses db). + * @param {(String|Number)?} account * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -Wallet.prototype.getUnconfirmed = function getUnconfirmed(callback) { - return this.db.getUnconfirmed(this.id, callback); +Wallet.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { + var self = this; + this._getID(account, callback, function(id, callback) { + self.db.getUnconfirmed(id, callback); + }); }; /** * Get wallet balance (accesses db). + * @param {(String|Number)?} account * @param {Function} callback - Returns [Error, {@link Balance}]. */ -Wallet.prototype.getBalance = function getBalance(callback) { - return this.db.getBalance(this.id, callback); +Wallet.prototype.getBalance = function getBalance(account, callback) { + var self = this; + this._getID(account, callback, function(id, callback) { + self.db.getBalance(id, callback); + }); }; /** * Get last timestamp and height this wallet was active * at (accesses db). Useful for resetting the chain * to a certain height when in SPV mode. + * @param {(String|Number)?} account * @param {Function} callback - Returns [Error, Number(ts), Number(height)]. */ -Wallet.prototype.getLastTime = function getLastTime(callback) { - return this.db.getLastTime(this.id, callback); +Wallet.prototype.getLastTime = function getLastTime(account, callback) { + var self = this; + this._getID(account, callback, function(id, callback) { + self.db.getLastTime(id, callback); + }); }; /** * Get the last N transactions (accesses db). + * @param {(String|Number)?} account * @param {Number} limit * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -Wallet.prototype.getLast = function getLast(limit, callback) { - return this.db.getLast(this.id, limit, callback); +Wallet.prototype.getLast = function getLast(account, limit, callback) { + var self = this; + + if (typeof limit === 'function') { + callback = limit; + limit = account; + account = null; + } + + this._getID(account, callback, function(id, callback) { + self.db.getLast(id, callback); + }); }; /** * Get a range of transactions between two timestamps (accesses db). + * @param {(String|Number)?} account * @param {Object} options * @param {Number} options.start * @param {Number} options.end * @param {Function} callback - Returns [Error, {@link TX}[]]. */ -Wallet.prototype.getTimeRange = function getTimeRange(options, callback) { - return this.db.getTimeRange(this.id, options, callback); +Wallet.prototype.getTimeRange = function getTimeRange(account, options, callback) { + var self = this; + + if (typeof options === 'function') { + callback = options; + options = account; + account = null; + } + + this._getID(account, callback, function(id, callback) { + self.db.getTimeRange(id, options, callback); + }); }; /** @@ -1513,27 +1318,9 @@ Wallet.prototype.inspect = function inspect() { id: this.id, network: this.network.type, initialized: this.initialized, - type: this.type, - m: this.m, - n: this.n, - keyAddress: this.initialized - ? this.keyAddress - : null, - scriptAddress: this.initialized - ? this.scriptAddress - : null, - programAddress: this.initialized - ? this.programAddress - : null, - witness: this.witness, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, + accountDepth: this.accountDepth, master: this.master.toJSON(), - accountKey: this.accountKey.xpubkey, - keys: this.keys.map(function(key) { - return key.xpubkey; - }) + account: this.account }; }; @@ -1546,23 +1333,11 @@ Wallet.prototype.inspect = function inspect() { Wallet.prototype.toJSON = function toJSON() { return { - v: 3, - name: 'wallet', network: this.network.type, id: this.id, initialized: this.initialized, - type: this.type, - m: this.m, - n: this.n, - witness: this.witness, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, - master: this.master.toJSON(), - accountKey: this.accountKey.xpubkey, - keys: this.keys.map(function(key) { - return key.xpubkey; - }) + accountDepth: this.accountDepth, + master: this.master.toJSON() }; }; @@ -1578,25 +1353,12 @@ Wallet.prototype.toJSON = function toJSON() { */ Wallet.parseJSON = function parseJSON(json) { - assert.equal(json.v, 3); - assert.equal(json.name, 'wallet'); - return { network: json.network, id: json.id, initialized: json.initialized, - type: json.type, - m: json.m, - n: json.n, - witness: json.witness, - accountIndex: json.accountIndex, - receiveDepth: json.receiveDepth, - changeDepth: json.changeDepth, - master: MasterKey.fromJSON(json.master), - accountKey: bcoin.hd.fromBase58(json.accountKey), - keys: json.keys.map(function(key) { - return bcoin.hd.fromBase58(key); - }) + accountDepth: json.accountDepth, + master: MasterKey.fromJSON(json.master) }; }; @@ -1612,19 +1374,8 @@ Wallet.prototype.toRaw = function toRaw(writer) { p.writeU32(this.network.magic); p.writeVarString(this.id, 'utf8'); p.writeU8(this.initialized ? 1 : 0); - p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); - p.writeU8(this.m); - p.writeU8(this.n); - p.writeU8(this.witness ? 1 : 0); - p.writeU32(this.accountIndex); - p.writeU32(this.receiveDepth); - p.writeU32(this.changeDepth); + p.writeU32(this.accountDepth); p.writeVarBytes(this.master.toRaw()); - p.writeBytes(this.accountKey.toRaw()); - p.writeU8(this.keys.length); - - for (i = 0; i < this.keys.length; i++) - p.writeBytes(this.keys[i].toRaw()); if (!writer) p = p.render(); @@ -1645,35 +1396,15 @@ Wallet.parseRaw = function parseRaw(data) { var network = bcoin.network.fromMagic(p.readU32()); var id = p.readVarString('utf8'); var initialized = p.readU8() === 1; - var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; - var m = p.readU8(); - var n = p.readU8(); - var witness = p.readU8() === 1; - var accountIndex = p.readU32(); - var receiveDepth = p.readU32(); - var changeDepth = p.readU32(); + var accountDepth = p.readU32(); var master = MasterKey.fromRaw(p.readVarBytes()); - var accountKey = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); - var keys = new Array(p.readU8()); - var i; - - for (i = 0; i < keys.length; i++) - keys[i] = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); return { network: network.type, id: id, initialized: initialized, - type: type, - m: m, - n: n, - witness: witness, - accountIndex: accountIndex, - receiveDepth: receiveDepth, - changeDepth: changeDepth, - master: master, - accountKey: accountKey, - keys: keys + accountDepth: accountDepth, + master: master }; }; @@ -1710,6 +1441,901 @@ Wallet.isWallet = function isWallet(obj) { && obj.deriveAddress === 'function'; }; +/** + * BIP44 Account + * @exports Account + * @constructor + * @param {Object} options + * @param {WalletDB} options.db + * present, no coins will be available. + * @param {(HDPrivateKey|HDPublicKey)?} options.master - Master HD key. If not + * present, it will be generated. + * @param {Boolean?} options.witness - Whether to use witness programs. + * @param {Number?} options.accountIndex - The BIP44 account index (default=0). + * @param {Number?} options.receiveDepth - The index of the _next_ receiving + * address. + * @param {Number?} options.changeDepth - The index of the _next_ change + * address. + * @param {Number?} options.lookahead - Amount of lookahead addresses + * (default=5). + * @param {String?} options.type - Type of wallet (pubkeyhash, multisig) + * (default=pubkeyhash). + * @param {Number?} options.m - `m` value for multisig. + * @param {Number?} options.n - `n` value for multisig. + * @param {String?} options.wid - Account ID (used for storage) + * (default=account key "address"). + */ + +function Account(options) { + var i; + + if (!(this instanceof Account)) + return new Account(options); + + EventEmitter.call(this); + + if (!options) + options = {}; + + options = utils.merge({}, options); + + this.options = options; + this.network = bcoin.network.get(options.network); + this.db = options.db; + + this.wid = options.wid || null; + this.name = options.name || null; + this.witness = options.witness || false; + this.loaded = false; + this.loading = false; + + this.accountKey = options.accountKey || null; + this.accountIndex = options.accountIndex || 0; + this.receiveDepth = options.receiveDepth || 1; + this.changeDepth = options.changeDepth || 1; + this.receiveAddress = null; + this.changeAddress = null; + + this.lookahead = options.lookahead != null ? options.lookahead : 5; + this.initialized = options.initialized || false; + + this.type = options.type || 'pubkeyhash'; + this.keys = []; + this.m = options.m || 1; + this.n = options.n || 1; + + this.cache = new bcoin.lru(20, 1); + + if (this.n > 1) + this.type = 'multisig'; + + assert(this.type === 'pubkeyhash' || this.type === 'multisig', + '`type` must be multisig or pubkeyhash.'); + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (!this.name) + this.name = this.accountIndex + ''; + + // Non-alphanumeric IDs will break leveldb sorting. + assert(/^[a-zA-Z0-9]+$/.test(this.name), 'Account IDs must be alphanumeric.'); + + this.pushKey(this.accountKey); + + if (options.keys) { + for (i = 0; i < options.keys.length; i++) + this.pushKey(options.keys[i]); + } +} + +utils.inherits(Account, EventEmitter); + +/** + * Open the account, register with the database. + * @param {Function} callback + */ + +Account.prototype.open = function open(callback) { + var self = this; + + callback = utils.ensure(callback); + + if (this.loaded) + return utils.nextTick(callback); + + if (this.loading) + return this.once('open', callback); + + this.loading = true; + + try { + //this.db.register(this); + } catch (e) { + this.emit('error', err); + return callback(err); + } + + this.init(function(err) { + if (err) { + self.emit('error', err); + return callback(err); + } + + self.loading = false; + self.loaded = true; + self.emit('open'); + + return callback(); + }); +}; + +/** + * Close the account, unregister with the database. + * @method + * @param {Function} callback + */ + +Account.prototype.close = +Account.prototype.destroy = function destroy(callback) { + callback = utils.ensure(callback); + + if (!this.loaded) + return utils.nextTick(callback); + + assert(!this.loading); + + try { + //this.db.unregister(this); + } catch (e) { + this.emit('error', err); + return callback(err); + } + + this.loaded = false; + + return utils.nextTick(callback); +}; + +/** + * Attempt to intialize the account (generating + * the first addresses along with the lookahead + * addresses). Called automatically from the + * walletdb and open(). + * @param {Function} callback + */ + +Account.prototype.init = function init(callback) { + var self = this; + var addresses = []; + var i; + + // Waiting for more keys. + if (this.keys.length !== this.n) { + assert(!this.initialized); + return this.db.open(function(err) { + if (err) + return callback(err); + self.save(callback); + }); + } + + if (this.initialized) { + this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); + this.changeAddress = this.deriveChange(this.changeDepth - 1); + return this.db.open(callback); + } + + this.initialized = true; + + for (i = 0; i < this.receiveDepth - 1; i++) + addresses.push(this.deriveReceive(i)); + + for (i = 0; i < this.changeDepth - 1; i++) + addresses.push(this.deriveChange(i)); + + for (i = this.receiveDepth; i < this.receiveDepth + this.lookahead; i++) + addresses.push(this.deriveReceive(i)); + + for (i = this.changeDepth; i < this.changeDepth + this.lookahead; i++) + addresses.push(this.deriveChange(i)); + + this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); + this.changeAddress = this.deriveChange(this.changeDepth - 1); + + addresses.push(this.receiveAddress); + addresses.push(this.changeAddress); + + return this.db.open(function(err) { + if (err) + return callback(err); + return self.saveAddress(addresses, function(err) { + if (err) + return callback(err); + return self.save(callback); + }); + }); +}; + +/** + * Add a public account key to the account (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). + * @throws Error on non-hdkey/non-accountkey. + */ + +Account.prototype.pushKey = function pushKey(key) { + var result = false; + var index = -1; + var i; + + assert(key, 'Key required.'); + + if (Array.isArray(key)) { + for (i = 0; i < key.length; i++) { + if (this.pushKey(key[i])) + result = true; + } + return result; + } + + if (key.accountKey) + key = key.accountKey; + + if (bcoin.hd.isExtended(key)) + key = bcoin.hd.fromBase58(key); + + if (key.hdPublicKey) + key = key.hdPublicKey; + + if (!bcoin.hd.isHD(key)) + throw new Error('Must add HD keys to wallet.'); + + if (!key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + + for (i = 0; i < this.keys.length; i++) { + if (this.keys[i].equal(key)) { + index = i; + break; + } + } + + if (index !== -1) + return false; + + if (this.keys.length === this.n) + throw new Error('Cannot add more keys.'); + + this.keys.push(key); + + return true; +}; + +/** + * Remove a public account key to the account (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). + * @throws Error on non-hdkey/non-accountkey. + */ + +Account.prototype.spliceKey = function spliceKey(key) { + var result = false; + var index = -1; + var i; + + if (Array.isArray(key)) { + for (i = 0; i < key.length; i++) { + if (this.spliceKey(key[i])) + result = true; + } + return result; + } + + assert(key, 'Key required.'); + + if (key.accountKey) + key = key.accountKey; + + if (bcoin.hd.isExtended(key)) + key = bcoin.hd.fromBase58(key); + + if (key.hdPublicKey) + key = key.hdPublicKey; + + if (!bcoin.hd.isHD(key)) + throw new Error('Must add HD keys to wallet.'); + + if (!key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + + for (i = 0; i < this.keys.length; i++) { + if (this.keys[i].equal(key)) { + index = i; + break; + } + } + + if (index === -1) + return false; + + if (this.keys.length === this.n) + throw new Error('Cannot remove key.'); + + this.keys.splice(index, 1); + + return true; +}; + +/** + * Add a public account key to the account (multisig). + * Saves the key in the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ + +Account.prototype.addKey = function addKey(key, callback) { + var result = false; + + try { + result = this.pushKey(key); + } catch (e) { + return callback(e); + } + + if (!result) + return callback(null, result); + + this.init(function(err) { + if (err) + return callback(err); + + return callback(null, result); + }); +}; + +/** + * Remove a public account key from the account (multisig). + * Remove the key from the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ + +Account.prototype.removeKey = function removeKey(key, callback) { + var result = false; + + try { + result = this.spliceKey(key); + } catch (e) { + return callback(e); + } + + if (!result) + return callback(null, result); + + this.save(function(err) { + if (err) + return callback(err); + + return callback(null, result); + }); +}; + +/** + * Create a new receiving address (increments receiveDepth). + * @returns {KeyRing} + */ + +Account.prototype.createReceive = function createReceive(callback) { + return this.createAddress(false, callback); +}; + +/** + * Create a new change address (increments receiveDepth). + * @returns {KeyRing} + */ + +Account.prototype.createChange = function createChange(callback) { + return this.createAddress(true, callback); +}; + +/** + * Create a new address (increments depth). + * @param {Boolean} change + * @returns {KeyRing} + */ + +Account.prototype.createAddress = function createAddress(change, callback) { + var self = this; + var addresses = []; + var address; + + if (typeof change === 'function') { + callback = change; + change = null; + } + + if (change) { + address = this.deriveChange(this.changeDepth); + addresses.push(address); + addresses.push(this.deriveChange(this.changeDepth + this.lookahead)); + this.changeDepth++; + this.changeAddress = address; + } else { + address = this.deriveReceive(this.receiveDepth); + addresses.push(address); + addresses.push(this.deriveReceive(this.receiveDepth + this.lookahead)); + this.receiveDepth++; + this.receiveAddress = address; + } + + this.saveAddress(addresses, function(err) { + if (err) + return callback(err); + + self.save(function(err) { + if (err) + return callback(err); + return callback(null, address); + }); + }); +}; + +/** + * Derive a receiving address at `index`. Do not increment depth. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveReceive = function deriveReceive(index) { + return this.deriveAddress(false, index); +}; + +/** + * Derive a change address at `index`. Do not increment depth. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveChange = function deriveChange(index) { + return this.deriveAddress(true, index); +}; + +/** + * Derive an address at `index`. Do not increment depth. + * @param {Boolean} change - Whether the address on the change branch. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveAddress = function deriveAddress(change, index) { + var self = this; + var i, key, options; + + assert(this.initialized); + + change = +change; + + key = this.accountKey.derive(change).derive(index); + + options = { + network: this.network, + key: key.publicKey, + account: this.accountIndex, + change: change, + index: index, + type: this.type, + name: this.name, + witness: this.witness, + m: this.m, + n: this.n, + keys: [] + }; + + for (i = 0; i < this.keys.length; i++) { + key = this.keys[i]; + key = key.derive(change).derive(index); + options.keys.push(key.publicKey); + } + + return new bcoin.keyring(options); +}; + +/** + * Save the account to the database. Necessary + * when address depth and keys change. + * @param {Function} callback + */ + +Account.prototype.save = function save(callback) { + this.db.saveAccount(this, callback); +}; + +/** + * Save addresses to path map. + * @param {KeyRing[]} address + * @param {Function} callback + */ + +Account.prototype.saveAddress = function saveAddress(address, callback) { + this.db.saveAddress(this.wid, address, callback); +}; + +/** + * Set receiving depth (depth is the index of the _next_ address). + * Allocate all addresses up to depth. Note that this also allocates + * new lookahead addresses. + * @param {Number} depth + * @returns {Boolean} True if new addresses were allocated. + */ + +Account.prototype.setReceiveDepth = function setReceiveDepth(depth, callback) { + var self = this; + var addresses = []; + var i; + + if (!(depth > this.receiveDepth)) + return callback(null, false); + + for (i = this.receiveDepth; i < depth; i++) { + this.receiveAddress = this.deriveReceive(i); + addresses.push(this.receiveAddress); + } + + for (i = this.receiveDepth + this.lookahead; i < depth + this.lookahead; i++) + addresses.push(this.deriveReceive(i)); + + this.receiveDepth = depth; + + this.saveAddress(addresses, function(err) { + if (err) + return callback(err); + + self.save(function(err) { + if (err) + return callback(err); + + return callback(null, true); + }); + }); +}; + +/** + * Set change depth (depth is the index of the _next_ address). + * Allocate all addresses up to depth. Note that this also allocates + * new lookahead addresses. + * @param {Number} depth + * @returns {Boolean} True if new addresses were allocated. + */ + +Account.prototype.setChangeDepth = function setChangeDepth(depth, callback) { + var self = this; + var addresses = []; + var i; + + if (!(depth > this.changeDepth)) + return callback(null, false); + + for (i = this.changeDepth; i < depth; i++) { + this.changeAddress = this.deriveChange(i); + addresses.push(this.changeAddress); + } + + for (i = this.changeDepth + this.lookahead; i < depth + this.lookahead; i++) + addresses.push(this.deriveChange(i)); + + this.changeDepth = depth; + + this.saveAddress(addresses, function(err) { + if (err) + return callback(err); + self.save(function(err) { + if (err) + return callback(err); + return callback(null, true); + }); + }); +}; + +/** + * Scan for addresses. + * @param {Function} getByAddress - Must be a callback which accepts + * a callback and returns transactions by address. + * @param {Function} callback - Returns [Boolean, TX[]]. + */ + +Account.prototype.scan = function scan(getByAddress, callback) { + var self = this; + var result = false; + + return this._scan(getByAddress, function(err, depth, txs) { + if (err) + return callback(err); + + self.setChangeDepth(depth.changeDepth + 1, function(err, res) { + if (err) + return callback(err); + + if (res) + result = true; + + self.setReceiveDepth(depth.receiveDepth + 1, function(err, res) { + if (err) + return callback(err); + + if (res) + result = true; + + return callback(null, result, txs); + }); + }); + }); +}; + +Account.prototype._scan = function _scan(getByAddress, callback) { + var self = this; + var depth = { changeDepth: 0, receiveDepth: 0 }; + var all = []; + + assert(this.initialized); + + (function chainCheck(change) { + var addressIndex = 0; + var total = 0; + var gap = 0; + + (function next() { + var address = self.deriveAddress(change, addressIndex++); + + getByAddress(address.getAddress(), function(err, txs) { + var result; + + if (err) + return callback(err); + + if (txs) { + if (typeof txs === 'boolean') + result = txs; + else if (typeof txs === 'number') + result = txs > 0; + else if (Array.isArray(txs)) + result = txs.length > 0; + else + result = false; + + if (Array.isArray(txs) && (txs[0] instanceof bcoin.tx)) + all = all.concat(txs); + } + + if (result) { + total++; + gap = 0; + return next(); + } + + if (++gap < 20) + return next(); + + assert(depth.receiveDepth === 0 || change === true); + + if (change === false) + depth.receiveDepth = addressIndex - gap; + else + depth.changeDepth = addressIndex - gap; + + if (change === false) + return chainCheck(true); + + return callback(null, depth, all); + }); + })(); + })(false); +}; + +/** + * Convert the account to a more inspection-friendly object. + * @returns {Object} + */ + +Account.prototype.inspect = function inspect() { + return { + wid: this.wid, + name: this.name, + network: this.network.type, + initialized: this.initialized, + type: this.type, + m: this.m, + n: this.n, + keyAddress: this.initialized + ? this.receiveAddress.getKeyAddress() + : null, + scriptAddress: this.initialized + ? this.receiveAddress.getScriptAddress() + : null, + programAddress: this.initialized + ? this.receiveAddress.getProgramAddress() + : null, + witness: this.witness, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + accountKey: this.accountKey.xpubkey, + keys: this.keys.map(function(key) { + return key.xpubkey; + }) + }; +}; + +/** + * Convert the account to an object suitable for + * serialization. Will automatically encrypt the + * master key based on the `passphrase` option. + * @returns {Object} + */ + +Account.prototype.toJSON = function toJSON() { + return { + network: this.network.type, + wid: this.wid, + name: this.name, + initialized: this.initialized, + type: this.type, + m: this.m, + n: this.n, + witness: this.witness, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + accountKey: this.accountKey.xpubkey, + keys: this.keys.map(function(key) { + return key.xpubkey; + }) + }; +}; + +/** + * Handle a deserialized JSON account object. + * @returns {Object} A "naked" account (a + * plain javascript object which is suitable + * for passing to the Account constructor). + * @param {Object} json + * @param {String?} passphrase + * @returns {Object} + * @throws Error on bad decrypt + */ + +Account.parseJSON = function parseJSON(json) { + return { + network: json.network, + wid: json.wid, + name: json.name, + initialized: json.initialized, + type: json.type, + m: json.m, + n: json.n, + witness: json.witness, + accountIndex: json.accountIndex, + receiveDepth: json.receiveDepth, + changeDepth: json.changeDepth, + accountKey: bcoin.hd.fromBase58(json.accountKey), + keys: json.keys.map(function(key) { + return bcoin.hd.fromBase58(key); + }) + }; +}; + +/** + * Serialize the account. + * @returns {Buffer} + */ + +Account.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + var i; + + p.writeU32(this.network.magic); + p.writeVarString(this.wid, 'utf8'); + p.writeVarString(this.name, 'utf8'); + p.writeU8(this.initialized ? 1 : 0); + p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); + p.writeU8(this.m); + p.writeU8(this.n); + p.writeU8(this.witness ? 1 : 0); + p.writeU32(this.accountIndex); + p.writeU32(this.receiveDepth); + p.writeU32(this.changeDepth); + p.writeBytes(this.accountKey.toRaw()); + p.writeU8(this.keys.length); + + for (i = 0; i < this.keys.length; i++) + p.writeBytes(this.keys[i].toRaw()); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Parse a serialized account. Return a "naked" + * account object, suitable for passing into + * the account constructor. + * @param {Buffer} data + * @returns {Object} + */ + +Account.parseRaw = function parseRaw(data) { + var p = new BufferReader(data); + var network = bcoin.network.fromMagic(p.readU32()); + var wid = p.readVarString('utf8'); + var name = p.readVarString('utf8'); + var initialized = p.readU8() === 1; + var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; + var m = p.readU8(); + var n = p.readU8(); + var witness = p.readU8() === 1; + var accountIndex = p.readU32(); + var receiveDepth = p.readU32(); + var changeDepth = p.readU32(); + var accountKey = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); + var keys = new Array(p.readU8()); + var i; + + for (i = 0; i < keys.length; i++) + keys[i] = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); + + return { + network: network.type, + wid: wid, + name: name, + initialized: initialized, + type: type, + m: m, + n: n, + witness: witness, + accountIndex: accountIndex, + receiveDepth: receiveDepth, + changeDepth: changeDepth, + accountKey: accountKey, + keys: keys + }; +}; + +/** + * Instantiate a account from serialized data. + * @param {Buffer} data + * @returns {Account} + */ + +Account.fromRaw = function fromRaw(data) { + return new Account(Account.parseRaw(data)); +}; + +/** + * Instantiate a Account from a + * jsonified account object. + * @param {Object} json - The jsonified account object. + * @returns {Account} + */ + +Account.fromJSON = function fromJSON(json) { + return new Account(Account.parseJSON(json)); +}; + +/** + * Test an object to see if it is a Account. + * @param {Object} obj + * @returns {Boolean} + */ + +Account.isAccount = function isAccount(obj) { + return obj + && typeof obj.receiveDepth === 'number' + && obj.deriveAddress === 'function'; +}; + /* * Master Key */ @@ -1871,4 +2497,8 @@ MasterKey.isMasterKey = function isMasterKey(obj) { * Expose */ -module.exports = Wallet; +exports = Wallet; +exports.Account = Account; +exports.MasterKey = MasterKey; + +module.exports = exports; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 37d769d9..b9783429 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -354,7 +354,7 @@ WalletDB.prototype.save = function save(wallet, callback) { */ WalletDB.prototype.getAccountIndex = function getAccountIndex(wid, name, callback) { - return this.db.get('a/' + wid + '/' + name, function(err, index) { + return this.db.get('i/' + wid + '/' + name, function(err, index) { if (err && err.type !== 'NotFoundError') return callback(); @@ -370,7 +370,7 @@ WalletDB.prototype.getAccount = function getAccount(wid, id, callback) { var aid = wid + '/' + id; var account; - if (!id) + if (id == null) return callback(); if (typeof id === 'string') { @@ -387,9 +387,6 @@ WalletDB.prototype.getAccount = function getAccount(wid, id, callback) { this.db.get('a/' + aid, function(err, data) { if (err && err.type !== 'NotFoundError') - return callback(); - - if (err) return callback(err); if (!data) @@ -398,7 +395,7 @@ WalletDB.prototype.getAccount = function getAccount(wid, id, callback) { try { data = bcoin.account.parseRaw(data); data.db = self; - account = bcoin.account.fromRaw(data); + account = new bcoin.account(data); } catch (e) { return callback(e); } @@ -435,16 +432,52 @@ WalletDB.prototype.remove = function remove(id, callback) { WalletDB.prototype.saveAccount = function saveAccount(account, callback) { var index = new Buffer(4); - this.db.put('a/' + account.wid + '/' + account.index, account.toRaw(), function(err) { + var batch = this.db.batch(); + index.writeUInt32LE(account.accountIndex, 0, true); + batch.put('a/' + account.wid + '/' + account.accountIndex, account.toRaw()); + batch.put('i/' + account.wid + '/' + account.name, index); + batch.write(callback); +}; + +WalletDB.prototype.createAccount = function createAccount(options, callback) { + var self = this; + var account; + + this.hasAccount(options.wid, options.accountIndex, function(err, exists) { if (err) return callback(err); - index.writeUInt32LE(account.index, 0, true); + if (err) + return callback(err); - self.db.put('a/' + account.wid + '/' + account.name, index, callback); + if (exists) + return callback(new Error('account already exists.')); + + options = utils.merge({}, options); + + if (self.network.witness) + options.witness = options.witness !== false; + + options.network = self.network; + options.db = self; + account = new bcoin.account(options); + + account.open(function(err) { + if (err) + return callback(err); + + return callback(null, account); + }); }); }; +WalletDB.prototype.hasAccount = function hasAccount(wid, account, callback) { + if (!wid || account == null) + return callback(null, false); + + this.db.has('a/' + wid + '/' + account, callback); +}; + /** * Create a new wallet, save to database, setup watcher. * @param {Object} options - See {@link Wallet}. @@ -493,7 +526,7 @@ WalletDB.prototype.has = function has(id, callback) { if (!id) return callback(null, false); - this.db.hash('w/' + id, callback); + this.db.has('w/' + id, callback); }; /** @@ -727,8 +760,8 @@ WalletDB.prototype.fillCoins = function fillCoins(tx, callback) { * @see {@link TXDB#zap}. */ -WalletDB.prototype.zap = function zap(id, now, age, callback) { - return this.tx.zap(id, now, age, callback); +WalletDB.prototype.zap = function zap(id, age, callback) { + return this.tx.zap(id, age, callback); }; /**