From f313ca166df9eb4155156f05dcda9326a085c835 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 16 Nov 2017 20:11:17 -0800 Subject: [PATCH] wallet: classify. --- lib/blockchain/chaindb.js | 2 +- lib/wallet/account.js | 1823 +++++++------ lib/wallet/http.js | 10 +- lib/wallet/masterkey.js | 1288 ++++----- lib/wallet/nullclient.js | 291 +- lib/wallet/path.js | 534 ++-- lib/wallet/plugin.js | 110 +- lib/wallet/records.js | 861 +++--- lib/wallet/server.js | 210 +- lib/wallet/txdb.js | 5261 +++++++++++++++++++------------------ lib/wallet/wallet.js | 4129 +++++++++++++++-------------- lib/wallet/walletdb.js | 4029 ++++++++++++++-------------- lib/wallet/walletkey.js | 93 - 13 files changed, 9296 insertions(+), 9345 deletions(-) diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index a4672952..7a864d50 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -908,7 +908,7 @@ class ChainDB { return this.db.values({ gte: layout.e(encoding.ZERO_HASH), lte: layout.e(encoding.MAX_HASH), - parse: value => ChainEntry.fromRaw(value) + parse: data => ChainEntry.fromRaw(data) }); } diff --git a/lib/wallet/account.js b/lib/wallet/account.js index 377b7097..93a9ce72 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -17,56 +17,920 @@ const HD = require('../hd/hd'); const {encoding} = bio; /** + * Account * Represents a BIP44 Account belonging to a {@link Wallet}. * Note that this object does not enforce locks. Any method * that does a write is internal API only and will lead * to race conditions if used elsewhere. * @alias module:wallet.Account - * @constructor - * @param {Object} options - * @param {HDPublicKey} options.accountKey - * @param {Boolean?} options.witness - Whether to use witness programs. - * @param {Number} options.accountIndex - The BIP44 account index. - * @param {Number?} options.receiveDepth - The index of the _next_ receiving - * address. - * @param {Number?} options.changeDepth - The index of the _next_ change - * address. - * @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 - Wallet ID - * @param {String?} options.name - Account name */ -function Account(wdb, options) { - if (!(this instanceof Account)) - return new Account(wdb, options); +class Account { + /** + * Create an account. + * @constructor + * @param {Object} options + */ - assert(wdb, 'Database is required.'); + constructor(wdb, options) { + assert(wdb, 'Database is required.'); - this.wdb = wdb; - this.network = wdb.network; + this.wdb = wdb; + this.network = wdb.network; - this.wid = 0; - this.id = null; - this.name = null; - this.initialized = false; - this.witness = wdb.options.witness === true; - this.watchOnly = false; - this.type = Account.types.PUBKEYHASH; - this.m = 1; - this.n = 1; - this.accountIndex = 0; - this.receiveDepth = 0; - this.changeDepth = 0; - this.nestedDepth = 0; - this.lookahead = 10; - this.accountKey = null; - this.keys = []; + this.wid = 0; + this.id = null; + this.name = null; + this.initialized = false; + this.witness = wdb.options.witness === true; + this.watchOnly = false; + this.type = Account.types.PUBKEYHASH; + this.m = 1; + this.n = 1; + this.accountIndex = 0; + this.receiveDepth = 0; + this.changeDepth = 0; + this.nestedDepth = 0; + this.lookahead = 10; + this.accountKey = null; + this.keys = []; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(options, 'Options are required.'); + assert((options.wid >>> 0) === options.wid); + assert(common.isName(options.id), 'Bad Wallet ID.'); + assert(HD.isHD(options.accountKey), 'Account key is required.'); + assert((options.accountIndex >>> 0) === options.accountIndex, + 'Account index is required.'); + + this.wid = options.wid; + this.id = options.id; + + if (options.name != null) { + assert(common.isName(options.name), 'Bad account name.'); + this.name = options.name; + } + + if (options.initialized != null) { + assert(typeof options.initialized === 'boolean'); + this.initialized = options.initialized; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + + if (options.watchOnly != null) { + assert(typeof options.watchOnly === 'boolean'); + this.watchOnly = options.watchOnly; + } + + if (options.type != null) { + if (typeof options.type === 'string') { + this.type = Account.types[options.type.toUpperCase()]; + assert(this.type != null); + } else { + assert(typeof options.type === 'number'); + this.type = options.type; + assert(Account.typesByVal[this.type]); + } + } + + if (options.m != null) { + assert((options.m & 0xff) === options.m); + this.m = options.m; + } + + if (options.n != null) { + assert((options.n & 0xff) === options.n); + this.n = options.n; + } + + if (options.accountIndex != null) { + assert((options.accountIndex >>> 0) === options.accountIndex); + this.accountIndex = options.accountIndex; + } + + if (options.receiveDepth != null) { + assert((options.receiveDepth >>> 0) === options.receiveDepth); + this.receiveDepth = options.receiveDepth; + } + + if (options.changeDepth != null) { + assert((options.changeDepth >>> 0) === options.changeDepth); + this.changeDepth = options.changeDepth; + } + + if (options.nestedDepth != null) { + assert((options.nestedDepth >>> 0) === options.nestedDepth); + this.nestedDepth = options.nestedDepth; + } + + if (options.lookahead != null) { + assert((options.lookahead >>> 0) === options.lookahead); + assert(options.lookahead >= 0); + assert(options.lookahead <= Account.MAX_LOOKAHEAD); + this.lookahead = options.lookahead; + } + + this.accountKey = options.accountKey; + + if (this.n > 1) + this.type = Account.types.MULTISIG; + + if (!this.name) + this.name = this.accountIndex.toString(10); + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (options.keys) { + assert(Array.isArray(options.keys)); + for (const key of options.keys) + this.pushKey(key); + } + + return this; + } + + /** + * Instantiate account from options. + * @param {WalletDB} wdb + * @param {Object} options + * @returns {Account} + */ + + static fromOptions(wdb, options) { + return new this(wdb).fromOptions(options); + } + + /** + * Attempt to intialize the account (generating + * the first addresses along with the lookahead + * addresses). Called automatically from the + * walletdb. + * @returns {Promise} + */ + + async init(b) { + // Waiting for more keys. + if (this.keys.length !== this.n - 1) { + assert(!this.initialized); + this.save(b); + return; + } + + assert(this.receiveDepth === 0); + assert(this.changeDepth === 0); + assert(this.nestedDepth === 0); + + this.initialized = true; + + await this.initDepth(b); + } + + /** + * 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. + */ + + pushKey(key) { + if (typeof key === 'string') + key = HD.PublicKey.fromBase58(key, this.network); + + if (!HD.isPublic(key)) + throw new Error('Must add HD keys to wallet.'); + + if (!key.isAccount()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + + if (this.type !== Account.types.MULTISIG) + throw new Error('Cannot add keys to non-multisig wallet.'); + + if (key.equals(this.accountKey)) + throw new Error('Cannot add own key.'); + + const index = binary.insert(this.keys, key, cmp, true); + + if (index === -1) + return false; + + if (this.keys.length > this.n - 1) { + binary.remove(this.keys, key, cmp); + throw new Error('Cannot add more keys.'); + } + + 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. + */ + + spliceKey(key) { + if (typeof key === 'string') + key = HD.PublicKey.fromBase58(key, this.network); + + if (!HD.isPublic(key)) + throw new Error('Must add HD keys to wallet.'); + + if (!key.isAccount()) + throw new Error('Must add HD account keys to BIP44 wallet.'); + + if (this.type !== Account.types.MULTISIG) + throw new Error('Cannot remove keys from non-multisig wallet.'); + + if (this.keys.length === this.n - 1) + throw new Error('Cannot remove key.'); + + return binary.remove(this.keys, key, cmp); + } + + /** + * Add a public account key to the account (multisig). + * Saves the key in the wallet database. + * @param {HDPublicKey} key + * @returns {Promise} + */ + + async addSharedKey(b, key) { + const result = this.pushKey(key); + + if (await this.hasDuplicate()) { + this.spliceKey(key); + throw new Error('Cannot add a key from another account.'); + } + + // Try to initialize again. + await this.init(b); + + return result; + } + + /** + * Ensure accounts are not sharing keys. + * @private + * @returns {Promise} + */ + + async hasDuplicate() { + if (this.keys.length !== this.n - 1) + return false; + + const ring = this.deriveReceive(0); + const hash = ring.getScriptHash('hex'); + + return this.wdb.hasPath(this.wid, hash); + } + + /** + * Remove a public account key from the account (multisig). + * Remove the key from the wallet database. + * @param {HDPublicKey} key + * @returns {Promise} + */ + + removeSharedKey(b, key) { + const result = this.spliceKey(key); + + if (!result) + return false; + + this.save(b); + + return true; + } + + /** + * Create a new receiving address (increments receiveDepth). + * @returns {WalletKey} + */ + + createReceive() { + return this.createKey(0); + } + + /** + * Create a new change address (increments receiveDepth). + * @returns {WalletKey} + */ + + createChange() { + return this.createKey(1); + } + + /** + * Create a new change address (increments receiveDepth). + * @returns {WalletKey} + */ + + createNested() { + return this.createKey(2); + } + + /** + * Create a new address (increments depth). + * @param {Boolean} change + * @returns {Promise} - Returns {@link WalletKey}. + */ + + async createKey(b, branch) { + let key, lookahead; + + switch (branch) { + case 0: + key = this.deriveReceive(this.receiveDepth); + lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); + await this.saveKey(b, lookahead); + this.receiveDepth += 1; + this.receive = key; + break; + case 1: + key = this.deriveChange(this.changeDepth); + lookahead = this.deriveReceive(this.changeDepth + this.lookahead); + await this.saveKey(b, lookahead); + this.changeDepth += 1; + this.change = key; + break; + case 2: + key = this.deriveNested(this.nestedDepth); + lookahead = this.deriveNested(this.nestedDepth + this.lookahead); + await this.saveKey(b, lookahead); + this.nestedDepth += 1; + this.nested = key; + break; + default: + throw new Error(`Bad branch: ${branch}.`); + } + + this.save(); + + return key; + } + + /** + * Derive a receiving address at `index`. Do not increment depth. + * @param {Number} index + * @returns {WalletKey} + */ + + deriveReceive(index, master) { + return this.deriveKey(0, index, master); + } + + /** + * Derive a change address at `index`. Do not increment depth. + * @param {Number} index + * @returns {WalletKey} + */ + + deriveChange(index, master) { + return this.deriveKey(1, index, master); + } + + /** + * Derive a nested address at `index`. Do not increment depth. + * @param {Number} index + * @returns {WalletKey} + */ + + deriveNested(index, master) { + if (!this.witness) + throw new Error('Cannot derive nested on non-witness account.'); + + return this.deriveKey(2, index, master); + } + + /** + * Derive an address from `path` object. + * @param {Path} path + * @param {MasterKey} master + * @returns {WalletKey} + */ + + derivePath(path, master) { + switch (path.keyType) { + case Path.types.HD: { + return this.deriveKey(path.branch, path.index, master); + } + case Path.types.KEY: { + assert(this.type === Account.types.PUBKEYHASH); + + let data = path.data; + + if (path.encrypted) { + data = master.decipher(data, path.hash); + if (!data) + return null; + } + + return WalletKey.fromImport(this, data); + } + case Path.types.ADDRESS: { + return null; + } + default: { + throw new Error('Bad key type.'); + } + } + } + + /** + * Derive an address at `index`. Do not increment depth. + * @param {Number} branch - Whether the address on the change branch. + * @param {Number} index + * @returns {WalletKey} + */ + + deriveKey(branch, index, master) { + assert(typeof branch === 'number'); + + const keys = []; + + let key; + if (master && master.key && !this.watchOnly) { + const type = this.network.keyPrefix.coinType; + key = master.key.deriveAccount(44, type, this.accountIndex); + key = key.derive(branch).derive(index); + } else { + key = this.accountKey.derive(branch).derive(index); + } + + const ring = WalletKey.fromHD(this, key, branch, index); + + switch (this.type) { + case Account.types.PUBKEYHASH: + break; + case Account.types.MULTISIG: + keys.push(key.publicKey); + + for (const shared of this.keys) { + const key = shared.derive(branch).derive(index); + keys.push(key.publicKey); + } + + ring.script = Script.fromMultisig(this.m, this.n, keys); + + break; + } + + return ring; + } + + /** + * Save the account to the database. Necessary + * when address depth and keys change. + * @returns {Promise} + */ + + save(b) { + return this.wdb.saveAccount(b, this); + } + + /** + * Save addresses to path map. + * @param {WalletKey[]} rings + * @returns {Promise} + */ + + saveKey(b, ring) { + return this.wdb.saveKey(b, this.wid, ring); + } + + /** + * Save paths to path map. + * @param {Path[]} rings + * @returns {Promise} + */ + + savePath(b, path) { + return this.wdb.savePath(b, this.wid, path); + } + + /** + * Initialize address depths (including lookahead). + * @returns {Promise} + */ + + async initDepth(b) { + // Receive Address + this.receiveDepth = 1; + + for (let i = 0; i <= this.lookahead; i++) { + const key = this.deriveReceive(i); + await this.saveKey(b, key); + } + + // Change Address + this.changeDepth = 1; + + for (let i = 0; i <= this.lookahead; i++) { + const key = this.deriveChange(i); + await this.saveKey(b, key); + } + + // Nested Address + if (this.witness) { + this.nestedDepth = 1; + + for (let i = 0; i <= this.lookahead; i++) { + const key = this.deriveNested(i); + await this.saveKey(b, key); + } + } + + this.save(b); + } + + /** + * Allocate new lookahead addresses if necessary. + * @param {Number} receiveDepth + * @param {Number} changeDepth + * @param {Number} nestedDepth + * @returns {Promise} - Returns {@link WalletKey}. + */ + + async syncDepth(b, receive, change, nested) { + let derived = false; + let result = null; + + if (receive > this.receiveDepth) { + const depth = this.receiveDepth + this.lookahead; + + assert(receive <= depth + 1); + + for (let i = depth; i < receive + this.lookahead; i++) { + const key = this.deriveReceive(i); + await this.saveKey(b, key); + result = key; + } + + this.receiveDepth = receive; + + derived = true; + } + + if (change > this.changeDepth) { + const depth = this.changeDepth + this.lookahead; + + assert(change <= depth + 1); + + for (let i = depth; i < change + this.lookahead; i++) { + const key = this.deriveChange(i); + await this.saveKey(b, key); + } + + this.changeDepth = change; + + derived = true; + } + + if (this.witness && nested > this.nestedDepth) { + const depth = this.nestedDepth + this.lookahead; + + assert(nested <= depth + 1); + + for (let i = depth; i < nested + this.lookahead; i++) { + const key = this.deriveNested(i); + await this.saveKey(b, key); + result = key; + } + + this.nestedDepth = nested; + + derived = true; + result = this.nested; + } + + if (derived) + this.save(b); + + return result; + } + + /** + * Allocate new lookahead addresses. + * @param {Number} lookahead + * @returns {Promise} + */ + + async setLookahead(b, lookahead) { + if (lookahead === this.lookahead) + return; + + if (lookahead < this.lookahead) { + const diff = this.lookahead - lookahead; + + this.receiveDepth += diff; + this.changeDepth += diff; + + if (this.witness) + this.nestedDepth += diff; + + this.lookahead = lookahead; + + this.save(b); + + return; + } + + { + const depth = this.receiveDepth + this.lookahead; + const target = this.receiveDepth + lookahead; + + for (let i = depth; i < target; i++) { + const key = this.deriveReceive(i); + await this.saveKey(b, key); + } + } + + { + const depth = this.changeDepth + this.lookahead; + const target = this.changeDepth + lookahead; + + for (let i = depth; i < target; i++) { + const key = this.deriveChange(i); + await this.saveKey(b, key); + } + } + + if (this.witness) { + const depth = this.nestedDepth + this.lookahead; + const target = this.nestedDepth + lookahead; + + for (let i = depth; i < target; i++) { + const key = this.deriveNested(i); + await this.saveKey(b, key); + } + } + + this.lookahead = lookahead; + this.save(b); + } + + /** + * Get current receive key. + * @returns {WalletKey} + */ + + receiveKey() { + if (!this.initialized) + return null; + + return this.deriveReceive(this.receiveDepth - 1); + } + + /** + * Get current change key. + * @returns {WalletKey} + */ + + changeKey() { + if (!this.initialized) + return null; + + return this.deriveChange(this.changeDepth - 1); + } + + /** + * Get current nested key. + * @returns {WalletKey} + */ + + nestedKey() { + if (!this.initialized) + return null; + + if (!this.witness) + return null; + + return this.deriveNested(this.nestedDepth - 1); + } + + /** + * Get current receive address. + * @returns {Address} + */ + + receiveAddress() { + const key = this.receiveKey(); + + if (!key) + return null; + + return key.getAddress(); + } + + /** + * Get current change address. + * @returns {Address} + */ + + changeAddress() { + const key = this.changeKey(); + + if (!key) + return null; + + return key.getAddress(); + } + + /** + * Get current nested address. + * @returns {Address} + */ + + nestedAddress() { + const key = this.nestedKey(); + + if (!key) + return null; + + return key.getAddress(); + } + + /** + * Convert the account to a more inspection-friendly object. + * @returns {Object} + */ + + inspect() { + const receive = this.receiveAddress(); + const change = this.changeAddress(); + const nested = this.nestedAddress(); + + return { + id: this.id, + wid: this.wid, + name: this.name, + network: this.network.type, + initialized: this.initialized, + witness: this.witness, + watchOnly: this.watchOnly, + type: Account.typesByVal[this.type].toLowerCase(), + m: this.m, + n: this.n, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + nestedDepth: this.nestedDepth, + lookahead: this.lookahead, + receiveAddress: receive ? receive.toString(this.network) : null, + changeAddress: change ? change.toString(this.network) : null, + nestedAddress: nested ? nested.toString(this.network) : null, + accountKey: this.accountKey.toBase58(this.network), + keys: this.keys.map(key => key.toBase58(this.network)) + }; + } + + /** + * Convert the account to an object suitable for + * serialization. + * @returns {Object} + */ + + toJSON(balance) { + const receive = this.receiveAddress(); + const change = this.changeAddress(); + const nested = this.nestedAddress(); + + return { + name: this.name, + initialized: this.initialized, + witness: this.witness, + watchOnly: this.watchOnly, + type: Account.typesByVal[this.type].toLowerCase(), + m: this.m, + n: this.n, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + nestedDepth: this.nestedDepth, + lookahead: this.lookahead, + receiveAddress: receive ? receive.toString(this.network) : null, + changeAddress: change ? change.toString(this.network) : null, + nestedAddress: nested ? nested.toString(this.network) : null, + accountKey: this.accountKey.toBase58(this.network), + keys: this.keys.map(key => key.toBase58(this.network)), + balance: balance ? balance.toJSON(true) : null + }; + } + + /** + * Calculate serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + size += encoding.sizeVarString(this.name, 'ascii'); + size += 105; + size += this.keys.length * 82; + return size; + } + + /** + * Serialize the account. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); + + bw.writeVarString(this.name, 'ascii'); + bw.writeU8(this.initialized ? 1 : 0); + bw.writeU8(this.witness ? 1 : 0); + bw.writeU8(this.type); + bw.writeU8(this.m); + bw.writeU8(this.n); + bw.writeU32(this.accountIndex); + bw.writeU32(this.receiveDepth); + bw.writeU32(this.changeDepth); + bw.writeU32(this.nestedDepth); + bw.writeU8(this.lookahead); + bw.writeBytes(this.accountKey.toRaw(this.network)); + bw.writeU8(this.keys.length); + + for (const key of this.keys) + bw.writeBytes(key.toRaw(this.network)); + + return bw.render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {Object} + */ + + fromRaw(data) { + const br = bio.read(data); + + this.name = br.readVarString('ascii'); + this.initialized = br.readU8() === 1; + this.witness = br.readU8() === 1; + this.type = br.readU8(); + this.m = br.readU8(); + this.n = br.readU8(); + this.accountIndex = br.readU32(); + this.receiveDepth = br.readU32(); + this.changeDepth = br.readU32(); + this.nestedDepth = br.readU32(); + this.lookahead = br.readU8(); + this.accountKey = HD.PublicKey.fromRaw(br.readBytes(82), this.network); + + assert(Account.typesByVal[this.type]); + + const count = br.readU8(); + + for (let i = 0; i < count; i++) { + const key = HD.PublicKey.fromRaw(br.readBytes(82), this.network); + this.pushKey(key); + } + + return this; + } + + /** + * Instantiate a account from serialized data. + * @param {WalletDB} data + * @param {Buffer} data + * @returns {Account} + */ + + static fromRaw(wdb, data) { + return new this(wdb).fromRaw(data); + } + + /** + * Test an object to see if it is a Account. + * @param {Object} obj + * @returns {Boolean} + */ + + static isAccount(obj) { + return obj instanceof Account; + } } /** @@ -85,126 +949,10 @@ Account.types = { * @const {RevMap} */ -Account.typesByVal = { - 0: 'pubkeyhash', - 1: 'multisig' -}; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Account.prototype.fromOptions = function fromOptions(options) { - assert(options, 'Options are required.'); - assert((options.wid >>> 0) === options.wid); - assert(common.isName(options.id), 'Bad Wallet ID.'); - assert(HD.isHD(options.accountKey), 'Account key is required.'); - assert((options.accountIndex >>> 0) === options.accountIndex, - 'Account index is required.'); - - this.wid = options.wid; - this.id = options.id; - - if (options.name != null) { - assert(common.isName(options.name), 'Bad account name.'); - this.name = options.name; - } - - if (options.initialized != null) { - assert(typeof options.initialized === 'boolean'); - this.initialized = options.initialized; - } - - if (options.witness != null) { - assert(typeof options.witness === 'boolean'); - this.witness = options.witness; - } - - if (options.watchOnly != null) { - assert(typeof options.watchOnly === 'boolean'); - this.watchOnly = options.watchOnly; - } - - if (options.type != null) { - if (typeof options.type === 'string') { - this.type = Account.types[options.type.toUpperCase()]; - assert(this.type != null); - } else { - assert(typeof options.type === 'number'); - this.type = options.type; - assert(Account.typesByVal[this.type]); - } - } - - if (options.m != null) { - assert((options.m & 0xff) === options.m); - this.m = options.m; - } - - if (options.n != null) { - assert((options.n & 0xff) === options.n); - this.n = options.n; - } - - if (options.accountIndex != null) { - assert((options.accountIndex >>> 0) === options.accountIndex); - this.accountIndex = options.accountIndex; - } - - if (options.receiveDepth != null) { - assert((options.receiveDepth >>> 0) === options.receiveDepth); - this.receiveDepth = options.receiveDepth; - } - - if (options.changeDepth != null) { - assert((options.changeDepth >>> 0) === options.changeDepth); - this.changeDepth = options.changeDepth; - } - - if (options.nestedDepth != null) { - assert((options.nestedDepth >>> 0) === options.nestedDepth); - this.nestedDepth = options.nestedDepth; - } - - if (options.lookahead != null) { - assert((options.lookahead >>> 0) === options.lookahead); - assert(options.lookahead >= 0); - assert(options.lookahead <= Account.MAX_LOOKAHEAD); - this.lookahead = options.lookahead; - } - - this.accountKey = options.accountKey; - - if (this.n > 1) - this.type = Account.types.MULTISIG; - - if (!this.name) - this.name = this.accountIndex.toString(10); - - if (this.m < 1 || this.m > this.n) - throw new Error('m ranges between 1 and n'); - - if (options.keys) { - assert(Array.isArray(options.keys)); - for (const key of options.keys) - this.pushKey(key); - } - - return this; -}; - -/** - * Instantiate account from options. - * @param {WalletDB} wdb - * @param {Object} options - * @returns {Account} - */ - -Account.fromOptions = function fromOptions(wdb, options) { - return new Account(wdb).fromOptions(options); -}; +Account.typesByVal = [ + 'pubkeyhash', + 'multisig' +]; /* * Default address lookahead. @@ -213,763 +961,6 @@ Account.fromOptions = function fromOptions(wdb, options) { Account.MAX_LOOKAHEAD = 40; -/** - * Attempt to intialize the account (generating - * the first addresses along with the lookahead - * addresses). Called automatically from the - * walletdb. - * @returns {Promise} - */ - -Account.prototype.init = async function init(b) { - // Waiting for more keys. - if (this.keys.length !== this.n - 1) { - assert(!this.initialized); - this.save(b); - return; - } - - assert(this.receiveDepth === 0); - assert(this.changeDepth === 0); - assert(this.nestedDepth === 0); - - this.initialized = true; - - await this.initDepth(b); -}; - -/** - * 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) { - if (typeof key === 'string') - key = HD.PublicKey.fromBase58(key, this.network); - - if (!HD.isPublic(key)) - throw new Error('Must add HD keys to wallet.'); - - if (!key.isAccount()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - - if (this.type !== Account.types.MULTISIG) - throw new Error('Cannot add keys to non-multisig wallet.'); - - if (key.equals(this.accountKey)) - throw new Error('Cannot add own key.'); - - const index = binary.insert(this.keys, key, cmp, true); - - if (index === -1) - return false; - - if (this.keys.length > this.n - 1) { - binary.remove(this.keys, key, cmp); - throw new Error('Cannot add more keys.'); - } - - 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) { - if (typeof key === 'string') - key = HD.PublicKey.fromBase58(key, this.network); - - if (!HD.isPublic(key)) - throw new Error('Must add HD keys to wallet.'); - - if (!key.isAccount()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - - if (this.type !== Account.types.MULTISIG) - throw new Error('Cannot remove keys from non-multisig wallet.'); - - if (this.keys.length === this.n - 1) - throw new Error('Cannot remove key.'); - - return binary.remove(this.keys, key, cmp); -}; - -/** - * Add a public account key to the account (multisig). - * Saves the key in the wallet database. - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Account.prototype.addSharedKey = async function addSharedKey(b, key) { - const result = this.pushKey(key); - - if (await this.hasDuplicate()) { - this.spliceKey(key); - throw new Error('Cannot add a key from another account.'); - } - - // Try to initialize again. - await this.init(b); - - return result; -}; - -/** - * Ensure accounts are not sharing keys. - * @private - * @returns {Promise} - */ - -Account.prototype.hasDuplicate = async function hasDuplicate() { - if (this.keys.length !== this.n - 1) - return false; - - const ring = this.deriveReceive(0); - const hash = ring.getScriptHash('hex'); - - return this.wdb.hasPath(this.wid, hash); -}; - -/** - * Remove a public account key from the account (multisig). - * Remove the key from the wallet database. - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Account.prototype.removeSharedKey = function removeSharedKey(b, key) { - const result = this.spliceKey(key); - - if (!result) - return false; - - this.save(b); - - return true; -}; - -/** - * Create a new receiving address (increments receiveDepth). - * @returns {WalletKey} - */ - -Account.prototype.createReceive = function createReceive() { - return this.createKey(0); -}; - -/** - * Create a new change address (increments receiveDepth). - * @returns {WalletKey} - */ - -Account.prototype.createChange = function createChange() { - return this.createKey(1); -}; - -/** - * Create a new change address (increments receiveDepth). - * @returns {WalletKey} - */ - -Account.prototype.createNested = function createNested() { - return this.createKey(2); -}; - -/** - * Create a new address (increments depth). - * @param {Boolean} change - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Account.prototype.createKey = async function createKey(b, branch) { - let key, lookahead; - - switch (branch) { - case 0: - key = this.deriveReceive(this.receiveDepth); - lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); - await this.saveKey(b, lookahead); - this.receiveDepth += 1; - this.receive = key; - break; - case 1: - key = this.deriveChange(this.changeDepth); - lookahead = this.deriveReceive(this.changeDepth + this.lookahead); - await this.saveKey(b, lookahead); - this.changeDepth += 1; - this.change = key; - break; - case 2: - key = this.deriveNested(this.nestedDepth); - lookahead = this.deriveNested(this.nestedDepth + this.lookahead); - await this.saveKey(b, lookahead); - this.nestedDepth += 1; - this.nested = key; - break; - default: - throw new Error(`Bad branch: ${branch}.`); - } - - this.save(); - - return key; -}; - -/** - * Derive a receiving address at `index`. Do not increment depth. - * @param {Number} index - * @returns {WalletKey} - */ - -Account.prototype.deriveReceive = function deriveReceive(index, master) { - return this.deriveKey(0, index, master); -}; - -/** - * Derive a change address at `index`. Do not increment depth. - * @param {Number} index - * @returns {WalletKey} - */ - -Account.prototype.deriveChange = function deriveChange(index, master) { - return this.deriveKey(1, index, master); -}; - -/** - * Derive a nested address at `index`. Do not increment depth. - * @param {Number} index - * @returns {WalletKey} - */ - -Account.prototype.deriveNested = function deriveNested(index, master) { - if (!this.witness) - throw new Error('Cannot derive nested on non-witness account.'); - - return this.deriveKey(2, index, master); -}; - -/** - * Derive an address from `path` object. - * @param {Path} path - * @param {MasterKey} master - * @returns {WalletKey} - */ - -Account.prototype.derivePath = function derivePath(path, master) { - switch (path.keyType) { - case Path.types.HD: { - return this.deriveKey(path.branch, path.index, master); - } - case Path.types.KEY: { - assert(this.type === Account.types.PUBKEYHASH); - - let data = path.data; - - if (path.encrypted) { - data = master.decipher(data, path.hash); - if (!data) - return null; - } - - return WalletKey.fromImport(this, data); - } - case Path.types.ADDRESS: { - return null; - } - default: { - throw new Error('Bad key type.'); - } - } -}; - -/** - * Derive an address at `index`. Do not increment depth. - * @param {Number} branch - Whether the address on the change branch. - * @param {Number} index - * @returns {WalletKey} - */ - -Account.prototype.deriveKey = function deriveKey(branch, index, master) { - assert(typeof branch === 'number'); - - const keys = []; - - let key; - if (master && master.key && !this.watchOnly) { - const type = this.network.keyPrefix.coinType; - key = master.key.deriveAccount(44, type, this.accountIndex); - key = key.derive(branch).derive(index); - } else { - key = this.accountKey.derive(branch).derive(index); - } - - const ring = WalletKey.fromHD(this, key, branch, index); - - switch (this.type) { - case Account.types.PUBKEYHASH: - break; - case Account.types.MULTISIG: - keys.push(key.publicKey); - - for (const shared of this.keys) { - const key = shared.derive(branch).derive(index); - keys.push(key.publicKey); - } - - ring.script = Script.fromMultisig(this.m, this.n, keys); - - break; - } - - return ring; -}; - -/** - * Save the account to the database. Necessary - * when address depth and keys change. - * @returns {Promise} - */ - -Account.prototype.save = function save(b) { - return this.wdb.saveAccount(b, this); -}; - -/** - * Save addresses to path map. - * @param {WalletKey[]} rings - * @returns {Promise} - */ - -Account.prototype.saveKey = function saveKey(b, ring) { - return this.wdb.saveKey(b, this.wid, ring); -}; - -/** - * Save paths to path map. - * @param {Path[]} rings - * @returns {Promise} - */ - -Account.prototype.savePath = function savePath(b, path) { - return this.wdb.savePath(b, this.wid, path); -}; - -/** - * Initialize address depths (including lookahead). - * @returns {Promise} - */ - -Account.prototype.initDepth = async function initDepth(b) { - // Receive Address - this.receiveDepth = 1; - - for (let i = 0; i <= this.lookahead; i++) { - const key = this.deriveReceive(i); - await this.saveKey(b, key); - } - - // Change Address - this.changeDepth = 1; - - for (let i = 0; i <= this.lookahead; i++) { - const key = this.deriveChange(i); - await this.saveKey(b, key); - } - - // Nested Address - if (this.witness) { - this.nestedDepth = 1; - - for (let i = 0; i <= this.lookahead; i++) { - const key = this.deriveNested(i); - await this.saveKey(b, key); - } - } - - this.save(b); -}; - -/** - * Allocate new lookahead addresses if necessary. - * @param {Number} receiveDepth - * @param {Number} changeDepth - * @param {Number} nestedDepth - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Account.prototype.syncDepth = async function syncDepth(b, receive, change, nested) { - let derived = false; - let result = null; - - if (receive > this.receiveDepth) { - const depth = this.receiveDepth + this.lookahead; - - assert(receive <= depth + 1); - - for (let i = depth; i < receive + this.lookahead; i++) { - const key = this.deriveReceive(i); - await this.saveKey(b, key); - result = key; - } - - this.receiveDepth = receive; - - derived = true; - } - - if (change > this.changeDepth) { - const depth = this.changeDepth + this.lookahead; - - assert(change <= depth + 1); - - for (let i = depth; i < change + this.lookahead; i++) { - const key = this.deriveChange(i); - await this.saveKey(b, key); - } - - this.changeDepth = change; - - derived = true; - } - - if (this.witness && nested > this.nestedDepth) { - const depth = this.nestedDepth + this.lookahead; - - assert(nested <= depth + 1); - - for (let i = depth; i < nested + this.lookahead; i++) { - const key = this.deriveNested(i); - await this.saveKey(b, key); - result = key; - } - - this.nestedDepth = nested; - - derived = true; - result = this.nested; - } - - if (derived) - this.save(b); - - return result; -}; - -/** - * Allocate new lookahead addresses. - * @param {Number} lookahead - * @returns {Promise} - */ - -Account.prototype.setLookahead = async function setLookahead(b, lookahead) { - if (lookahead === this.lookahead) - return; - - if (lookahead < this.lookahead) { - const diff = this.lookahead - lookahead; - - this.receiveDepth += diff; - this.changeDepth += diff; - - if (this.witness) - this.nestedDepth += diff; - - this.lookahead = lookahead; - - this.save(b); - - return; - } - - { - const depth = this.receiveDepth + this.lookahead; - const target = this.receiveDepth + lookahead; - - for (let i = depth; i < target; i++) { - const key = this.deriveReceive(i); - await this.saveKey(b, key); - } - } - - { - const depth = this.changeDepth + this.lookahead; - const target = this.changeDepth + lookahead; - - for (let i = depth; i < target; i++) { - const key = this.deriveChange(i); - await this.saveKey(b, key); - } - } - - if (this.witness) { - const depth = this.nestedDepth + this.lookahead; - const target = this.nestedDepth + lookahead; - - for (let i = depth; i < target; i++) { - const key = this.deriveNested(i); - await this.saveKey(b, key); - } - } - - this.lookahead = lookahead; - this.save(b); -}; - -/** - * Get current receive key. - * @returns {WalletKey} - */ - -Account.prototype.receiveKey = function receiveKey() { - if (!this.initialized) - return null; - - return this.deriveReceive(this.receiveDepth - 1); -}; - -/** - * Get current change key. - * @returns {WalletKey} - */ - -Account.prototype.changeKey = function changeKey() { - if (!this.initialized) - return null; - - return this.deriveChange(this.changeDepth - 1); -}; - -/** - * Get current nested key. - * @returns {WalletKey} - */ - -Account.prototype.nestedKey = function nestedKey() { - if (!this.initialized) - return null; - - if (!this.witness) - return null; - - return this.deriveNested(this.nestedDepth - 1); -}; - -/** - * Get current receive address. - * @returns {Address} - */ - -Account.prototype.receiveAddress = function receiveAddress() { - const key = this.receiveKey(); - - if (!key) - return null; - - return key.getAddress(); -}; - -/** - * Get current change address. - * @returns {Address} - */ - -Account.prototype.changeAddress = function changeAddress() { - const key = this.changeKey(); - - if (!key) - return null; - - return key.getAddress(); -}; - -/** - * Get current nested address. - * @returns {Address} - */ - -Account.prototype.nestedAddress = function nestedAddress() { - const key = this.nestedKey(); - - if (!key) - return null; - - return key.getAddress(); -}; - -/** - * Convert the account to a more inspection-friendly object. - * @returns {Object} - */ - -Account.prototype.inspect = function inspect() { - const receive = this.receiveAddress(); - const change = this.changeAddress(); - const nested = this.nestedAddress(); - - return { - id: this.id, - wid: this.wid, - name: this.name, - network: this.network.type, - initialized: this.initialized, - witness: this.witness, - watchOnly: this.watchOnly, - type: Account.typesByVal[this.type].toLowerCase(), - m: this.m, - n: this.n, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, - nestedDepth: this.nestedDepth, - lookahead: this.lookahead, - receiveAddress: receive ? receive.toString(this.network) : null, - changeAddress: change ? change.toString(this.network) : null, - nestedAddress: nested ? nested.toString(this.network) : null, - accountKey: this.accountKey.toBase58(this.network), - keys: this.keys.map(key => key.toBase58(this.network)) - }; -}; - -/** - * Convert the account to an object suitable for - * serialization. - * @returns {Object} - */ - -Account.prototype.toJSON = function toJSON(balance) { - const receive = this.receiveAddress(); - const change = this.changeAddress(); - const nested = this.nestedAddress(); - - return { - name: this.name, - initialized: this.initialized, - witness: this.witness, - watchOnly: this.watchOnly, - type: Account.typesByVal[this.type].toLowerCase(), - m: this.m, - n: this.n, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, - nestedDepth: this.nestedDepth, - lookahead: this.lookahead, - receiveAddress: receive ? receive.toString(this.network) : null, - changeAddress: change ? change.toString(this.network) : null, - nestedAddress: nested ? nested.toString(this.network) : null, - accountKey: this.accountKey.toBase58(this.network), - keys: this.keys.map(key => key.toBase58(this.network)), - balance: balance ? balance.toJSON(true) : null - }; -}; - -/** - * Calculate serialization size. - * @returns {Number} - */ - -Account.prototype.getSize = function getSize() { - let size = 0; - size += encoding.sizeVarString(this.name, 'ascii'); - size += 105; - size += this.keys.length * 82; - return size; -}; - -/** - * Serialize the account. - * @returns {Buffer} - */ - -Account.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); - - bw.writeVarString(this.name, 'ascii'); - bw.writeU8(this.initialized ? 1 : 0); - bw.writeU8(this.witness ? 1 : 0); - bw.writeU8(this.type); - bw.writeU8(this.m); - bw.writeU8(this.n); - bw.writeU32(this.accountIndex); - bw.writeU32(this.receiveDepth); - bw.writeU32(this.changeDepth); - bw.writeU32(this.nestedDepth); - bw.writeU8(this.lookahead); - bw.writeBytes(this.accountKey.toRaw(this.network)); - bw.writeU8(this.keys.length); - - for (const key of this.keys) - bw.writeBytes(key.toRaw(this.network)); - - return bw.render(); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {Object} - */ - -Account.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - - this.name = br.readVarString('ascii'); - this.initialized = br.readU8() === 1; - this.witness = br.readU8() === 1; - this.type = br.readU8(); - this.m = br.readU8(); - this.n = br.readU8(); - this.accountIndex = br.readU32(); - this.receiveDepth = br.readU32(); - this.changeDepth = br.readU32(); - this.nestedDepth = br.readU32(); - this.lookahead = br.readU8(); - this.accountKey = HD.PublicKey.fromRaw(br.readBytes(82), this.network); - - assert(Account.typesByVal[this.type]); - - const count = br.readU8(); - - for (let i = 0; i < count; i++) { - const key = HD.PublicKey.fromRaw(br.readBytes(82), this.network); - this.pushKey(key); - } - - return this; -}; - -/** - * Instantiate a account from serialized data. - * @param {WalletDB} data - * @param {Buffer} data - * @returns {Account} - */ - -Account.fromRaw = function fromRaw(wdb, data) { - return new Account(wdb).fromRaw(data); -}; - -/** - * Test an object to see if it is a Account. - * @param {Object} obj - * @returns {Boolean} - */ - -Account.isAccount = function isAccount(obj) { - return obj instanceof Account; -}; - /* * Helpers */ diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 2286e151..7924d6bd 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -26,14 +26,16 @@ const HDPrivateKey = require('../hd/private'); const HDPublicKey = require('../hd/public'); const common = require('./common'); +/** + * HTTP + * @alias module:wallet.HTTP + */ + class HTTP extends Server { /** - * HTTP - * @alias module:wallet.HTTP + * Create an http server. * @constructor * @param {Object} options - * @see HTTPBase - * @emits HTTP#socket */ constructor(options) { diff --git a/lib/wallet/masterkey.js b/lib/wallet/masterkey.js index 4244bb45..2d75a1ef 100644 --- a/lib/wallet/masterkey.js +++ b/lib/wallet/masterkey.js @@ -22,36 +22,657 @@ const {encoding} = bio; const {Mnemonic} = HD; /** + * Master Key * Master BIP32 key which can exist * in a timed out encrypted state. * @alias module:wallet.MasterKey - * @constructor - * @param {Object} options */ -function MasterKey(options) { - if (!(this instanceof MasterKey)) - return new MasterKey(options); +class MasterKey { + /** + * Create a master key. + * @constructor + * @param {Object} options + */ - this.encrypted = false; - this.iv = null; - this.ciphertext = null; - this.key = null; - this.mnemonic = null; + constructor(options) { + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + this.key = null; + this.mnemonic = null; - this.alg = MasterKey.alg.PBKDF2; - this.N = 50000; - this.r = 0; - this.p = 0; + this.alg = MasterKey.alg.PBKDF2; + this.N = 50000; + this.r = 0; + this.p = 0; - this.aesKey = null; - this.timer = null; - this.until = 0; - this._onTimeout = this.lock.bind(this); - this.locker = new Lock(); + this.aesKey = null; + this.timer = null; + this.until = 0; + this._onTimeout = this.lock.bind(this); + this.locker = new Lock(); - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ + + fromOptions(options) { + assert(options); + + if (options.network != null) + this.network = Network.get(options.network); + + if (options.encrypted != null) { + assert(typeof options.encrypted === 'boolean'); + this.encrypted = options.encrypted; + } + + if (options.iv) { + assert(Buffer.isBuffer(options.iv)); + this.iv = options.iv; + } + + if (options.ciphertext) { + assert(Buffer.isBuffer(options.ciphertext)); + this.ciphertext = options.ciphertext; + } + + if (options.key) { + assert(HD.isPrivate(options.key)); + this.key = options.key; + } + + if (options.mnemonic) { + assert(options.mnemonic instanceof Mnemonic); + this.mnemonic = options.mnemonic; + } + + if (options.alg != null) { + if (typeof options.alg === 'string') { + this.alg = MasterKey.alg[options.alg.toUpperCase()]; + assert(this.alg != null, 'Unknown algorithm.'); + } else { + assert(typeof options.alg === 'number'); + assert(MasterKey.algByVal[options.alg]); + this.alg = options.alg; + } + } + + if (options.rounds != null) { + assert((options.rounds >>> 0) === options.rounds); + this.N = options.rounds; + } + + if (options.N != null) { + assert((options.N >>> 0) === options.N); + this.N = options.N; + } + + if (options.r != null) { + assert((options.r >>> 0) === options.r); + this.r = options.r; + } + + if (options.p != null) { + assert((options.p >>> 0) === options.p); + this.p = options.p; + } + + assert(this.encrypted ? !this.key : this.key); + + return this; + } + + /** + * Instantiate master key from options. + * @returns {MasterKey} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Decrypt the key and set a timeout to destroy decrypted data. + * @param {Buffer|String} passphrase - Zero this yourself. + * @param {Number} [timeout=60000] timeout in ms. + * @returns {Promise} - Returns {@link HDPrivateKey}. + */ + + async unlock(passphrase, timeout) { + const _unlock = await this.locker.lock(); + try { + return await this._unlock(passphrase, timeout); + } finally { + _unlock(); + } + } + + /** + * Decrypt the key without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @param {Number} [timeout=60000] timeout in ms. + * @returns {Promise} - Returns {@link HDPrivateKey}. + */ + + async _unlock(passphrase, timeout) { + if (this.key) { + if (this.encrypted) { + assert(this.timer != null); + this.start(timeout); + } + return this.key; + } + + if (!passphrase) + throw new Error('No passphrase.'); + + assert(this.encrypted); + + const key = await this.derive(passphrase); + const data = aes.decipher(this.ciphertext, key, this.iv); + + this.readKey(data); + + this.start(timeout); + + this.aesKey = key; + + return this.key; + } + + /** + * Start the destroy timer. + * @private + * @param {Number} [timeout=60000] timeout in ms. + */ + + start(timeout) { + if (!timeout) + timeout = 60; + + this.stop(); + + if (timeout === -1) + return; + + this.until = util.now() + timeout; + this.timer = setTimeout(this._onTimeout, timeout * 1000); + } + + /** + * Stop the destroy timer. + * @private + */ + + stop() { + if (this.timer != null) { + clearTimeout(this.timer); + this.timer = null; + this.until = 0; + } + } + + /** + * Derive an aes key based on params. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + + async derive(passwd) { + const salt = MasterKey.SALT; + const N = this.N; + const r = this.r; + const p = this.p; + + if (typeof passwd === 'string') + passwd = Buffer.from(passwd, 'utf8'); + + switch (this.alg) { + case MasterKey.alg.PBKDF2: + return await pbkdf2.deriveAsync(sha256, passwd, salt, N, 32); + case MasterKey.alg.SCRYPT: + return await scrypt.deriveAsync(passwd, salt, N, r, p, 32); + default: + throw new Error(`Unknown algorithm: ${this.alg}.`); + } + } + + /** + * Encrypt data with in-memory aes key. + * @param {Buffer} data + * @param {Buffer} iv + * @returns {Buffer} + */ + + encipher(data, iv) { + if (!this.aesKey) + return null; + + if (typeof iv === 'string') + iv = Buffer.from(iv, 'hex'); + + return aes.encipher(data, this.aesKey, iv.slice(0, 16)); + } + + /** + * Decrypt data with in-memory aes key. + * @param {Buffer} data + * @param {Buffer} iv + * @returns {Buffer} + */ + + decipher(data, iv) { + if (!this.aesKey) + return null; + + if (typeof iv === 'string') + iv = Buffer.from(iv, 'hex'); + + return aes.decipher(data, this.aesKey, iv.slice(0, 16)); + } + + /** + * Destroy the key by zeroing the + * privateKey and chainCode. Stop + * the timer if there is one. + * @returns {Promise} + */ + + async lock() { + const unlock = await this.locker.lock(); + try { + return await this._lock(); + } finally { + unlock(); + } + } + + /** + * Destroy the key by zeroing the + * privateKey and chainCode. Stop + * the timer if there is one. + */ + + _lock() { + if (!this.encrypted) { + assert(this.timer == null); + assert(this.key); + return; + } + + this.stop(); + + if (this.key) { + this.key.destroy(true); + this.key = null; + } + + if (this.aesKey) { + cleanse(this.aesKey); + this.aesKey = null; + } + } + + /** + * Destroy the key permanently. + */ + + async destroy() { + await this.lock(); + this.locker.destroy(); + } + + /** + * Decrypt the key permanently. + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + + async decrypt(passphrase, clean) { + const unlock = await this.locker.lock(); + try { + return await this._decrypt(passphrase, clean); + } finally { + unlock(); + } + } + + /** + * Decrypt the key permanently without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + + async _decrypt(passphrase, clean) { + if (!this.encrypted) + throw new Error('Master key is not encrypted.'); + + if (!passphrase) + throw new Error('No passphrase provided.'); + + this._lock(); + + const key = await this.derive(passphrase); + const data = aes.decipher(this.ciphertext, key, this.iv); + + this.readKey(data); + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + + if (!clean) { + cleanse(key); + return null; + } + + return key; + } + + /** + * Encrypt the key permanently. + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + + async encrypt(passphrase, clean) { + const unlock = await this.locker.lock(); + try { + return await this._encrypt(passphrase, clean); + } finally { + unlock(); + } + } + + /** + * Encrypt the key permanently without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + + async _encrypt(passphrase, clean) { + if (this.encrypted) + throw new Error('Master key is already encrypted.'); + + if (!passphrase) + throw new Error('No passphrase provided.'); + + const raw = this.writeKey(); + const iv = random.randomBytes(16); + + this.stop(); + + const key = await this.derive(passphrase); + const data = aes.encipher(raw, key, iv); + + this.key = null; + this.mnemonic = null; + this.encrypted = true; + this.iv = iv; + this.ciphertext = data; + + if (!clean) { + cleanse(key); + return null; + } + + return key; + } + + /** + * Calculate key serialization size. + * @returns {Number} + */ + + keySize() { + let size = 0; + + size += this.key.getSize(); + size += 1; + + if (this.mnemonic) + size += this.mnemonic.getSize(); + + return size; + } + + /** + * Serialize key and menmonic to a single buffer. + * @returns {Buffer} + */ + + writeKey() { + const bw = bio.write(this.keySize()); + + this.key.toWriter(bw, this.network); + + if (this.mnemonic) { + bw.writeU8(1); + this.mnemonic.toWriter(bw); + } else { + bw.writeU8(0); + } + + return bw.render(); + } + + /** + * Inject properties from serialized key. + * @param {Buffer} data + */ + + readKey(data) { + const br = bio.read(data); + + this.key = HD.PrivateKey.fromReader(br, this.network); + + if (br.readU8() === 1) + this.mnemonic = Mnemonic.fromReader(br); + + return this; + } + + /** + * Calculate serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + if (this.encrypted) { + size += 1; + size += encoding.sizeVarBytes(this.iv); + size += encoding.sizeVarBytes(this.ciphertext); + size += 13; + return size; + } + + size += 1; + size += encoding.sizeVarlen(this.keySize()); + + return size; + } + + /** + * Serialize the key in the form of: + * `[enc-flag][iv?][ciphertext?][extended-key?]` + * @returns {Buffer} + */ + + toRaw() { + const bw = bio.write(this.getSize()); + + if (this.encrypted) { + bw.writeU8(1); + bw.writeVarBytes(this.iv); + bw.writeVarBytes(this.ciphertext); + + bw.writeU8(this.alg); + bw.writeU32(this.N); + bw.writeU32(this.r); + bw.writeU32(this.p); + + return bw.render(); + } + + bw.writeU8(0); + + // NOTE: useless varint + const size = this.keySize(); + bw.writeVarint(size); + + bw.writeBytes(this.key.toRaw(this.network)); + + if (this.mnemonic) { + bw.writeU8(1); + this.mnemonic.toWriter(bw); + } else { + bw.writeU8(0); + } + + return bw.render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} raw + */ + + fromRaw(raw, network) { + const br = bio.read(raw); + + this.network = Network.get(network); + this.encrypted = br.readU8() === 1; + + if (this.encrypted) { + this.iv = br.readVarBytes(); + this.ciphertext = br.readVarBytes(); + + this.alg = br.readU8(); + + assert(MasterKey.algByVal[this.alg]); + + this.N = br.readU32(); + this.r = br.readU32(); + this.p = br.readU32(); + + return this; + } + + // NOTE: useless varint + br.readVarint(); + + this.key = HD.PrivateKey.fromRaw(br.readBytes(82), this.network); + + if (br.readU8() === 1) + this.mnemonic = Mnemonic.fromReader(br); + + return this; + } + + /** + * Instantiate master key from serialized data. + * @returns {MasterKey} + */ + + static fromRaw(raw, network) { + return new this().fromRaw(raw, network); + } + + /** + * Inject properties from an HDPrivateKey. + * @private + * @param {HDPrivateKey} key + * @param {Mnemonic?} mnemonic + */ + + fromKey(key, mnemonic, network) { + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + this.key = key; + this.mnemonic = mnemonic || null; + this.network = Network.get(network); + return this; + } + + /** + * Instantiate master key from an HDPrivateKey. + * @param {HDPrivateKey} key + * @param {Mnemonic?} mnemonic + * @returns {MasterKey} + */ + + static fromKey(key, mnemonic, network) { + return new this().fromKey(key, mnemonic, network); + } + + /** + * Convert master key to a jsonifiable object. + * @param {Boolean?} unsafe - Whether to include + * the key data in the JSON. + * @returns {Object} + */ + + toJSON(unsafe) { + if (this.encrypted) { + return { + encrypted: true, + until: this.until, + iv: this.iv.toString('hex'), + ciphertext: unsafe ? this.ciphertext.toString('hex') : undefined, + algorithm: MasterKey.algByVal[this.alg].toLowerCase(), + N: this.N, + r: this.r, + p: this.p + }; + } + + return { + encrypted: false, + key: unsafe ? this.key.toJSON(this.network) : undefined, + mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined + }; + } + + /** + * Inspect the key. + * @returns {Object} + */ + + inspect() { + const json = this.toJSON(true); + + if (this.key) + json.key = this.key.toJSON(this.network); + + if (this.mnemonic) + json.mnemonic = this.mnemonic.toJSON(); + + return json; + } + + /** + * Test whether an object is a MasterKey. + * @param {Object} obj + * @returns {Boolean} + */ + + static isMasterKey(obj) { + return obj instanceof MasterKey; + } } /** @@ -79,627 +700,10 @@ MasterKey.alg = { * @default */ -MasterKey.algByVal = { - 0: 'PBKDF2', - 1: 'SCRYPT' -}; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -MasterKey.prototype.fromOptions = function fromOptions(options) { - assert(options); - - if (options.network != null) - this.network = Network.get(options.network); - - if (options.encrypted != null) { - assert(typeof options.encrypted === 'boolean'); - this.encrypted = options.encrypted; - } - - if (options.iv) { - assert(Buffer.isBuffer(options.iv)); - this.iv = options.iv; - } - - if (options.ciphertext) { - assert(Buffer.isBuffer(options.ciphertext)); - this.ciphertext = options.ciphertext; - } - - if (options.key) { - assert(HD.isPrivate(options.key)); - this.key = options.key; - } - - if (options.mnemonic) { - assert(options.mnemonic instanceof Mnemonic); - this.mnemonic = options.mnemonic; - } - - if (options.alg != null) { - if (typeof options.alg === 'string') { - this.alg = MasterKey.alg[options.alg.toUpperCase()]; - assert(this.alg != null, 'Unknown algorithm.'); - } else { - assert(typeof options.alg === 'number'); - assert(MasterKey.algByVal[options.alg]); - this.alg = options.alg; - } - } - - if (options.rounds != null) { - assert((options.rounds >>> 0) === options.rounds); - this.N = options.rounds; - } - - if (options.N != null) { - assert((options.N >>> 0) === options.N); - this.N = options.N; - } - - if (options.r != null) { - assert((options.r >>> 0) === options.r); - this.r = options.r; - } - - if (options.p != null) { - assert((options.p >>> 0) === options.p); - this.p = options.p; - } - - assert(this.encrypted ? !this.key : this.key); - - return this; -}; - -/** - * Instantiate master key from options. - * @returns {MasterKey} - */ - -MasterKey.fromOptions = function fromOptions(options) { - return new MasterKey().fromOptions(options); -}; - -/** - * Decrypt the key and set a timeout to destroy decrypted data. - * @param {Buffer|String} passphrase - Zero this yourself. - * @param {Number} [timeout=60000] timeout in ms. - * @returns {Promise} - Returns {@link HDPrivateKey}. - */ - -MasterKey.prototype.unlock = async function unlock(passphrase, timeout) { - const _unlock = await this.locker.lock(); - try { - return await this._unlock(passphrase, timeout); - } finally { - _unlock(); - } -}; - -/** - * Decrypt the key without a lock. - * @private - * @param {Buffer|String} passphrase - Zero this yourself. - * @param {Number} [timeout=60000] timeout in ms. - * @returns {Promise} - Returns {@link HDPrivateKey}. - */ - -MasterKey.prototype._unlock = async function _unlock(passphrase, timeout) { - if (this.key) { - if (this.encrypted) { - assert(this.timer != null); - this.start(timeout); - } - return this.key; - } - - if (!passphrase) - throw new Error('No passphrase.'); - - assert(this.encrypted); - - const key = await this.derive(passphrase); - const data = aes.decipher(this.ciphertext, key, this.iv); - - this.readKey(data); - - this.start(timeout); - - this.aesKey = key; - - return this.key; -}; - -/** - * Start the destroy timer. - * @private - * @param {Number} [timeout=60000] timeout in ms. - */ - -MasterKey.prototype.start = function start(timeout) { - if (!timeout) - timeout = 60; - - this.stop(); - - if (timeout === -1) - return; - - this.until = util.now() + timeout; - this.timer = setTimeout(this._onTimeout, timeout * 1000); -}; - -/** - * Stop the destroy timer. - * @private - */ - -MasterKey.prototype.stop = function stop() { - if (this.timer != null) { - clearTimeout(this.timer); - this.timer = null; - this.until = 0; - } -}; - -/** - * Derive an aes key based on params. - * @param {String|Buffer} passphrase - * @returns {Promise} - */ - -MasterKey.prototype.derive = async function derive(passwd) { - const salt = MasterKey.SALT; - const N = this.N; - const r = this.r; - const p = this.p; - - if (typeof passwd === 'string') - passwd = Buffer.from(passwd, 'utf8'); - - switch (this.alg) { - case MasterKey.alg.PBKDF2: - return await pbkdf2.deriveAsync(sha256, passwd, salt, N, 32); - case MasterKey.alg.SCRYPT: - return await scrypt.deriveAsync(passwd, salt, N, r, p, 32); - default: - throw new Error(`Unknown algorithm: ${this.alg}.`); - } -}; - -/** - * Encrypt data with in-memory aes key. - * @param {Buffer} data - * @param {Buffer} iv - * @returns {Buffer} - */ - -MasterKey.prototype.encipher = function encipher(data, iv) { - if (!this.aesKey) - return null; - - if (typeof iv === 'string') - iv = Buffer.from(iv, 'hex'); - - return aes.encipher(data, this.aesKey, iv.slice(0, 16)); -}; - -/** - * Decrypt data with in-memory aes key. - * @param {Buffer} data - * @param {Buffer} iv - * @returns {Buffer} - */ - -MasterKey.prototype.decipher = function decipher(data, iv) { - if (!this.aesKey) - return null; - - if (typeof iv === 'string') - iv = Buffer.from(iv, 'hex'); - - return aes.decipher(data, this.aesKey, iv.slice(0, 16)); -}; - -/** - * Destroy the key by zeroing the - * privateKey and chainCode. Stop - * the timer if there is one. - * @returns {Promise} - */ - -MasterKey.prototype.lock = async function lock() { - const unlock = await this.locker.lock(); - try { - return await this._lock(); - } finally { - unlock(); - } -}; - -/** - * Destroy the key by zeroing the - * privateKey and chainCode. Stop - * the timer if there is one. - */ - -MasterKey.prototype._lock = function _lock() { - if (!this.encrypted) { - assert(this.timer == null); - assert(this.key); - return; - } - - this.stop(); - - if (this.key) { - this.key.destroy(true); - this.key = null; - } - - if (this.aesKey) { - cleanse(this.aesKey); - this.aesKey = null; - } -}; - -/** - * Destroy the key permanently. - */ - -MasterKey.prototype.destroy = async function destroy() { - await this.lock(); - this.locker.destroy(); -}; - -/** - * Decrypt the key permanently. - * @param {Buffer|String} passphrase - Zero this yourself. - * @returns {Promise} - */ - -MasterKey.prototype.decrypt = async function decrypt(passphrase, clean) { - const unlock = await this.locker.lock(); - try { - return await this._decrypt(passphrase, clean); - } finally { - unlock(); - } -}; - -/** - * Decrypt the key permanently without a lock. - * @private - * @param {Buffer|String} passphrase - Zero this yourself. - * @returns {Promise} - */ - -MasterKey.prototype._decrypt = async function _decrypt(passphrase, clean) { - if (!this.encrypted) - throw new Error('Master key is not encrypted.'); - - if (!passphrase) - throw new Error('No passphrase provided.'); - - this._lock(); - - const key = await this.derive(passphrase); - const data = aes.decipher(this.ciphertext, key, this.iv); - - this.readKey(data); - this.encrypted = false; - this.iv = null; - this.ciphertext = null; - - if (!clean) { - cleanse(key); - return null; - } - - return key; -}; - -/** - * Encrypt the key permanently. - * @param {Buffer|String} passphrase - Zero this yourself. - * @returns {Promise} - */ - -MasterKey.prototype.encrypt = async function encrypt(passphrase, clean) { - const unlock = await this.locker.lock(); - try { - return await this._encrypt(passphrase, clean); - } finally { - unlock(); - } -}; - -/** - * Encrypt the key permanently without a lock. - * @private - * @param {Buffer|String} passphrase - Zero this yourself. - * @returns {Promise} - */ - -MasterKey.prototype._encrypt = async function _encrypt(passphrase, clean) { - if (this.encrypted) - throw new Error('Master key is already encrypted.'); - - if (!passphrase) - throw new Error('No passphrase provided.'); - - const raw = this.writeKey(); - const iv = random.randomBytes(16); - - this.stop(); - - const key = await this.derive(passphrase); - const data = aes.encipher(raw, key, iv); - - this.key = null; - this.mnemonic = null; - this.encrypted = true; - this.iv = iv; - this.ciphertext = data; - - if (!clean) { - cleanse(key); - return null; - } - - return key; -}; - -/** - * Calculate key serialization size. - * @returns {Number} - */ - -MasterKey.prototype.keySize = function keySize() { - let size = 0; - - size += this.key.getSize(); - size += 1; - - if (this.mnemonic) - size += this.mnemonic.getSize(); - - return size; -}; - -/** - * Serialize key and menmonic to a single buffer. - * @returns {Buffer} - */ - -MasterKey.prototype.writeKey = function writeKey() { - const bw = bio.write(this.keySize()); - - this.key.toWriter(bw, this.network); - - if (this.mnemonic) { - bw.writeU8(1); - this.mnemonic.toWriter(bw); - } else { - bw.writeU8(0); - } - - return bw.render(); -}; - -/** - * Inject properties from serialized key. - * @param {Buffer} data - */ - -MasterKey.prototype.readKey = function readKey(data) { - const br = bio.read(data); - - this.key = HD.PrivateKey.fromReader(br, this.network); - - if (br.readU8() === 1) - this.mnemonic = Mnemonic.fromReader(br); - - return this; -}; - -/** - * Calculate serialization size. - * @returns {Number} - */ - -MasterKey.prototype.getSize = function getSize() { - let size = 0; - - if (this.encrypted) { - size += 1; - size += encoding.sizeVarBytes(this.iv); - size += encoding.sizeVarBytes(this.ciphertext); - size += 13; - return size; - } - - size += 1; - size += encoding.sizeVarlen(this.keySize()); - - return size; -}; - -/** - * Serialize the key in the form of: - * `[enc-flag][iv?][ciphertext?][extended-key?]` - * @returns {Buffer} - */ - -MasterKey.prototype.toRaw = function toRaw() { - const bw = bio.write(this.getSize()); - - if (this.encrypted) { - bw.writeU8(1); - bw.writeVarBytes(this.iv); - bw.writeVarBytes(this.ciphertext); - - bw.writeU8(this.alg); - bw.writeU32(this.N); - bw.writeU32(this.r); - bw.writeU32(this.p); - - return bw.render(); - } - - bw.writeU8(0); - - // NOTE: useless varint - const size = this.keySize(); - bw.writeVarint(size); - - bw.writeBytes(this.key.toRaw(this.network)); - - if (this.mnemonic) { - bw.writeU8(1); - this.mnemonic.toWriter(bw); - } else { - bw.writeU8(0); - } - - return bw.render(); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} raw - */ - -MasterKey.prototype.fromRaw = function fromRaw(raw, network) { - const br = bio.read(raw); - - this.network = Network.get(network); - this.encrypted = br.readU8() === 1; - - if (this.encrypted) { - this.iv = br.readVarBytes(); - this.ciphertext = br.readVarBytes(); - - this.alg = br.readU8(); - - assert(MasterKey.algByVal[this.alg]); - - this.N = br.readU32(); - this.r = br.readU32(); - this.p = br.readU32(); - - return this; - } - - // NOTE: useless varint - br.readVarint(); - - this.key = HD.PrivateKey.fromRaw(br.readBytes(82), this.network); - - if (br.readU8() === 1) - this.mnemonic = Mnemonic.fromReader(br); - - return this; -}; - -/** - * Instantiate master key from serialized data. - * @returns {MasterKey} - */ - -MasterKey.fromRaw = function fromRaw(raw, network) { - return new MasterKey().fromRaw(raw, network); -}; - -/** - * Inject properties from an HDPrivateKey. - * @private - * @param {HDPrivateKey} key - * @param {Mnemonic?} mnemonic - */ - -MasterKey.prototype.fromKey = function fromKey(key, mnemonic, network) { - this.encrypted = false; - this.iv = null; - this.ciphertext = null; - this.key = key; - this.mnemonic = mnemonic || null; - this.network = Network.get(network); - return this; -}; - -/** - * Instantiate master key from an HDPrivateKey. - * @param {HDPrivateKey} key - * @param {Mnemonic?} mnemonic - * @returns {MasterKey} - */ - -MasterKey.fromKey = function fromKey(key, mnemonic, network) { - return new MasterKey().fromKey(key, mnemonic, network); -}; - -/** - * Convert master key to a jsonifiable object. - * @param {Boolean?} unsafe - Whether to include - * the key data in the JSON. - * @returns {Object} - */ - -MasterKey.prototype.toJSON = function toJSON(unsafe) { - if (this.encrypted) { - return { - encrypted: true, - until: this.until, - iv: this.iv.toString('hex'), - ciphertext: unsafe ? this.ciphertext.toString('hex') : undefined, - algorithm: MasterKey.algByVal[this.alg].toLowerCase(), - N: this.N, - r: this.r, - p: this.p - }; - } - - return { - encrypted: false, - key: unsafe ? this.key.toJSON(this.network) : undefined, - mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined - }; -}; - -/** - * Inspect the key. - * @returns {Object} - */ - -MasterKey.prototype.inspect = function inspect() { - const json = this.toJSON(true); - - if (this.key) - json.key = this.key.toJSON(this.network); - - if (this.mnemonic) - json.mnemonic = this.mnemonic.toJSON(); - - return json; -}; - -/** - * Test whether an object is a MasterKey. - * @param {Object} obj - * @returns {Boolean} - */ - -MasterKey.isMasterKey = function isMasterKey(obj) { - return obj instanceof MasterKey; -}; +MasterKey.algByVal = [ + 'PBKDF2', + 'SCRYPT' +]; /* * Expose diff --git a/lib/wallet/nullclient.js b/lib/wallet/nullclient.js index 0229ae1a..1ef3ee2a 100644 --- a/lib/wallet/nullclient.js +++ b/lib/wallet/nullclient.js @@ -10,159 +10,160 @@ const assert = require('assert'); const EventEmitter = require('events'); /** - * NullClient + * Null Client * Sort of a fake local client for separation of concerns. * @alias module:node.NullClient - * @constructor */ -function NullClient(wdb) { - if (!(this instanceof NullClient)) - return new NullClient(wdb); +class NullClient extends EventEmitter { + /** + * Create a client. + * @constructor + */ - EventEmitter.call(this); + constructor(wdb) { + super(); - this.wdb = wdb; - this.network = wdb.network; - this.opened = false; + this.wdb = wdb; + this.network = wdb.network; + this.opened = false; + } + + /** + * Open the client. + * @returns {Promise} + */ + + async open(options) { + assert(!this.opened, 'NullClient is already open.'); + this.opened = true; + setImmediate(() => this.emit('connect')); + } + + /** + * Close the client. + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'NullClient is not open.'); + this.opened = false; + setImmediate(() => this.emit('disconnect')); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + bind(type, handler) { + return this.on(type, handler); + } + + /** + * Add a listener. + * @param {String} type + * @param {Function} handler + */ + + hook(type, handler) { + return this.on(type, handler); + } + + /** + * Get chain tip. + * @returns {Promise} + */ + + async getTip() { + const {hash, height, time} = this.network.genesis; + return { hash, height, time }; + } + + /** + * Get chain entry. + * @param {Hash} hash + * @returns {Promise} + */ + + async getEntry(hash) { + return { hash, height: 0, time: 0 }; + } + + /** + * Send a transaction. Do not wait for promise. + * @param {TX} tx + * @returns {Promise} + */ + + async send(tx) { + this.wdb.emit('send', tx); + } + + /** + * Set bloom filter. + * @param {Bloom} filter + * @returns {Promise} + */ + + async setFilter(filter) { + this.wdb.emit('set filter', filter); + } + + /** + * Add data to filter. + * @param {Buffer} data + * @returns {Promise} + */ + + async addFilter(data) { + this.wdb.emit('add filter', data); + } + + /** + * Reset filter. + * @returns {Promise} + */ + + async resetFilter() { + this.wdb.emit('reset filter'); + } + + /** + * Esimate smart fee. + * @param {Number?} blocks + * @returns {Promise} + */ + + async estimateFee(blocks) { + return this.network.feeRate; + } + + /** + * Get hash range. + * @param {Number} start + * @param {Number} end + * @returns {Promise} + */ + + async getHashes(start = -1, end = -1) { + return [this.network.genesis.hash]; + } + + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + async rescan(start) { + ; + } } -Object.setPrototypeOf(NullClient.prototype, EventEmitter.prototype); - -/** - * Open the client. - * @returns {Promise} - */ - -NullClient.prototype.open = async function open(options) { - assert(!this.opened, 'NullClient is already open.'); - this.opened = true; - setImmediate(() => this.emit('connect')); -}; - -/** - * Close the client. - * @returns {Promise} - */ - -NullClient.prototype.close = async function close() { - assert(this.opened, 'NullClient is not open.'); - this.opened = false; - setImmediate(() => this.emit('disconnect')); -}; - -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -NullClient.prototype.bind = function bind(type, handler) { - return this.on(type, handler); -}; - -/** - * Add a listener. - * @param {String} type - * @param {Function} handler - */ - -NullClient.prototype.hook = function hook(type, handler) { - return this.on(type, handler); -}; - -/** - * Get chain tip. - * @returns {Promise} - */ - -NullClient.prototype.getTip = async function getTip() { - const {hash, height, time} = this.network.genesis; - return { hash, height, time }; -}; - -/** - * Get chain entry. - * @param {Hash} hash - * @returns {Promise} - */ - -NullClient.prototype.getEntry = async function getEntry(hash) { - return { hash, height: 0, time: 0 }; -}; - -/** - * Send a transaction. Do not wait for promise. - * @param {TX} tx - * @returns {Promise} - */ - -NullClient.prototype.send = async function send(tx) { - this.wdb.emit('send', tx); -}; - -/** - * Set bloom filter. - * @param {Bloom} filter - * @returns {Promise} - */ - -NullClient.prototype.setFilter = async function setFilter(filter) { - this.wdb.emit('set filter', filter); -}; - -/** - * Add data to filter. - * @param {Buffer} data - * @returns {Promise} - */ - -NullClient.prototype.addFilter = async function addFilter(data) { - this.wdb.emit('add filter', data); -}; - -/** - * Reset filter. - * @returns {Promise} - */ - -NullClient.prototype.resetFilter = async function resetFilter() { - this.wdb.emit('reset filter'); -}; - -/** - * Esimate smart fee. - * @param {Number?} blocks - * @returns {Promise} - */ - -NullClient.prototype.estimateFee = async function estimateFee(blocks) { - return this.network.feeRate; -}; - -/** - * Get hash range. - * @param {Number} start - * @param {Number} end - * @returns {Promise} - */ - -NullClient.prototype.getHashes = async function getHashes(start = -1, end = -1) { - return [this.network.genesis.hash]; -}; - -/** - * Rescan for any missed transactions. - * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - -NullClient.prototype.rescan = async function rescan(start) { - ; -}; - /* * Expose */ diff --git a/lib/wallet/path.js b/lib/wallet/path.js index 0c32d429..b734fc64 100644 --- a/lib/wallet/path.js +++ b/lib/wallet/path.js @@ -14,36 +14,283 @@ const {encoding} = bio; /** * Path * @alias module:wallet.Path - * @constructor - * @property {WalletID} wid * @property {String} name - Account name. * @property {Number} account - Account index. * @property {Number} branch - Branch index. * @property {Number} index - Address index. - * @property {Address|null} address */ -function Path(options) { - if (!(this instanceof Path)) - return new Path(options); +class Path { + /** + * Create a path. + * @constructor + * @param {Object?} options + */ - this.keyType = Path.types.HD; + constructor(options) { + this.keyType = Path.types.HD; - this.name = null; // Passed in by caller. - this.account = 0; - this.branch = -1; - this.index = -1; + this.name = null; // Passed in by caller. + this.account = 0; + this.branch = -1; + this.index = -1; - this.encrypted = false; - this.data = null; + this.encrypted = false; + this.data = null; - // Currently unused. - this.type = Address.types.PUBKEYHASH; - this.version = -1; - this.hash = null; // Passed in by caller. + // Currently unused. + this.type = Address.types.PUBKEYHASH; + this.version = -1; + this.hash = null; // Passed in by caller. - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Instantiate path from options object. + * @private + * @param {Object} options + * @returns {Path} + */ + + fromOptions(options) { + this.keyType = options.keyType; + + this.name = options.name; + this.account = options.account; + this.branch = options.branch; + this.index = options.index; + + this.encrypted = options.encrypted; + this.data = options.data; + + this.type = options.type; + this.version = options.version; + this.hash = options.hash; + + return this; + } + + /** + * Instantiate path from options object. + * @param {Object} options + * @returns {Path} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } + + /** + * Clone the path object. + * @returns {Path} + */ + + clone() { + const path = new this.constructor(); + + path.keyType = this.keyType; + + path.name = this.name; + path.account = this.account; + path.branch = this.branch; + path.index = this.index; + + path.encrypted = this.encrypted; + path.data = this.data; + + path.type = this.type; + path.version = this.version; + path.hash = this.hash; + + return path; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = bio.read(data); + + this.account = br.readU32(); + this.keyType = br.readU8(); + + switch (this.keyType) { + case Path.types.HD: + this.branch = br.readU32(); + this.index = br.readU32(); + break; + case Path.types.KEY: + this.encrypted = br.readU8() === 1; + this.data = br.readVarBytes(); + break; + case Path.types.ADDRESS: + // Hash will be passed in by caller. + break; + default: + assert(false); + break; + } + + this.version = br.readI8(); + this.type = br.readU8(); + + if (this.type === 129 || this.type === 130) + this.type = 4; + + return this; + } + + /** + * Instantiate path from serialized data. + * @param {Buffer} data + * @returns {Path} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Calculate serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + size += 5; + + switch (this.keyType) { + case Path.types.HD: + size += 8; + break; + case Path.types.KEY: + size += 1; + size += encoding.sizeVarBytes(this.data); + break; + } + + size += 2; + + return size; + } + + /** + * Serialize path. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); + + bw.writeU32(this.account); + bw.writeU8(this.keyType); + + switch (this.keyType) { + case Path.types.HD: + assert(!this.data); + assert(this.index !== -1); + bw.writeU32(this.branch); + bw.writeU32(this.index); + break; + case Path.types.KEY: + assert(this.data); + assert(this.index === -1); + bw.writeU8(this.encrypted ? 1 : 0); + bw.writeVarBytes(this.data); + break; + case Path.types.ADDRESS: + assert(!this.data); + assert(this.index === -1); + break; + default: + assert(false); + break; + } + + bw.writeI8(this.version); + bw.writeU8(this.type); + + return bw.render(); + } + + /** + * Inject properties from address. + * @private + * @param {Account} account + * @param {Address} address + */ + + fromAddress(account, address) { + this.keyType = Path.types.ADDRESS; + this.name = account.name; + this.account = account.accountIndex; + this.version = address.version; + this.type = address.type; + this.hash = address.getHash('hex'); + return this; + } + + /** + * Instantiate path from address. + * @param {Account} account + * @param {Address} address + * @returns {Path} + */ + + static fromAddress(account, address) { + return new this().fromAddress(account, address); + } + + /** + * Convert path object to string derivation path. + * @returns {String} + */ + + toPath() { + if (this.keyType !== Path.types.HD) + return null; + + return `m/${this.account}'/${this.branch}/${this.index}`; + } + + /** + * Convert path object to an address (currently unused). + * @returns {Address} + */ + + toAddress() { + return Address.fromHash(this.hash, this.type, this.version); + } + + /** + * Convert path to a json-friendly object. + * @returns {Object} + */ + + toJSON() { + return { + name: this.name, + account: this.account, + change: this.branch === 1, + derivation: this.toPath() + }; + } + + /** + * Inspect the path. + * @returns {String} + */ + + inspect() { + return ``; + } } /** @@ -59,249 +306,16 @@ Path.types = { }; /** - * Instantiate path from options object. - * @private - * @param {Object} options - * @returns {Path} + * Path types. + * @enum {Number} + * @default */ -Path.prototype.fromOptions = function fromOptions(options) { - this.keyType = options.keyType; - - this.name = options.name; - this.account = options.account; - this.branch = options.branch; - this.index = options.index; - - this.encrypted = options.encrypted; - this.data = options.data; - - this.type = options.type; - this.version = options.version; - this.hash = options.hash; - - return this; -}; - -/** - * Instantiate path from options object. - * @param {Object} options - * @returns {Path} - */ - -Path.fromOptions = function fromOptions(options) { - return new Path().fromOptions(options); -}; - -/** - * Clone the path object. - * @returns {Path} - */ - -Path.prototype.clone = function clone() { - const path = new Path(); - - path.keyType = this.keyType; - - path.name = this.name; - path.account = this.account; - path.branch = this.branch; - path.index = this.index; - - path.encrypted = this.encrypted; - path.data = this.data; - - path.type = this.type; - path.version = this.version; - path.hash = this.hash; - - return path; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Path.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - - this.account = br.readU32(); - this.keyType = br.readU8(); - - switch (this.keyType) { - case Path.types.HD: - this.branch = br.readU32(); - this.index = br.readU32(); - break; - case Path.types.KEY: - this.encrypted = br.readU8() === 1; - this.data = br.readVarBytes(); - break; - case Path.types.ADDRESS: - // Hash will be passed in by caller. - break; - default: - assert(false); - break; - } - - this.version = br.readI8(); - this.type = br.readU8(); - - if (this.type === 129 || this.type === 130) - this.type = 4; - - return this; -}; - -/** - * Instantiate path from serialized data. - * @param {Buffer} data - * @returns {Path} - */ - -Path.fromRaw = function fromRaw(data) { - return new Path().fromRaw(data); -}; - -/** - * Calculate serialization size. - * @returns {Number} - */ - -Path.prototype.getSize = function getSize() { - let size = 0; - - size += 5; - - switch (this.keyType) { - case Path.types.HD: - size += 8; - break; - case Path.types.KEY: - size += 1; - size += encoding.sizeVarBytes(this.data); - break; - } - - size += 2; - - return size; -}; - -/** - * Serialize path. - * @returns {Buffer} - */ - -Path.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); - - bw.writeU32(this.account); - bw.writeU8(this.keyType); - - switch (this.keyType) { - case Path.types.HD: - assert(!this.data); - assert(this.index !== -1); - bw.writeU32(this.branch); - bw.writeU32(this.index); - break; - case Path.types.KEY: - assert(this.data); - assert(this.index === -1); - bw.writeU8(this.encrypted ? 1 : 0); - bw.writeVarBytes(this.data); - break; - case Path.types.ADDRESS: - assert(!this.data); - assert(this.index === -1); - break; - default: - assert(false); - break; - } - - bw.writeI8(this.version); - bw.writeU8(this.type); - - return bw.render(); -}; - -/** - * Inject properties from address. - * @private - * @param {Account} account - * @param {Address} address - */ - -Path.prototype.fromAddress = function fromAddress(account, address) { - this.keyType = Path.types.ADDRESS; - this.name = account.name; - this.account = account.accountIndex; - this.version = address.version; - this.type = address.type; - this.hash = address.getHash('hex'); - return this; -}; - -/** - * Instantiate path from address. - * @param {Account} account - * @param {Address} address - * @returns {Path} - */ - -Path.fromAddress = function fromAddress(account, address) { - return new Path().fromAddress(account, address); -}; - -/** - * Convert path object to string derivation path. - * @returns {String} - */ - -Path.prototype.toPath = function toPath() { - if (this.keyType !== Path.types.HD) - return null; - - return `m/${this.account}'/${this.branch}/${this.index}`; -}; - -/** - * Convert path object to an address (currently unused). - * @returns {Address} - */ - -Path.prototype.toAddress = function toAddress() { - return Address.fromHash(this.hash, this.type, this.version); -}; - -/** - * Convert path to a json-friendly object. - * @returns {Object} - */ - -Path.prototype.toJSON = function toJSON() { - return { - name: this.name, - account: this.account, - change: this.branch === 1, - derivation: this.toPath() - }; -}; - -/** - * Inspect the path. - * @returns {String} - */ - -Path.prototype.inspect = function inspect() { - return ``; -}; +Path.typesByVal = [ + 'HD', + 'KEY', + 'ADDRESS' +]; /** * Expose diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 98286bd8..47f31cd8 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -20,71 +20,75 @@ const plugin = exports; /** * Plugin - * @constructor - * @param {Node} node + * @extends EventEmitter */ -function Plugin(node) { - if (!(this instanceof Plugin)) - return new Plugin(node); +class Plugin extends EventEmitter { + /** + * Create a plugin. + * @constructor + * @param {Node} node + */ - const config = node.config; + constructor(node) { + super(); - this.network = node.network; - this.logger = node.logger; + const config = node.config; - this.client = new NodeClient(node); - this.plugin = true; + this.network = node.network; + this.logger = node.logger; - this.wdb = new WalletDB({ - network: node.network, - logger: node.logger, - workers: node.workers, - client: this.client, - prefix: config.prefix, - db: config.str(['wallet-db', 'db']), - maxFiles: config.uint('wallet-max-files'), - cacheSize: config.mb('wallet-cache-size'), - witness: config.bool('wallet-witness'), - checkpoints: config.bool('wallet-checkpoints'), - startHeight: config.uint('wallet-start-height'), - wipeNoReally: config.bool('wallet-wipe-no-really'), - spv: node.spv - }); + this.client = new NodeClient(node); + this.plugin = true; - this.rpc = new RPC(this); + this.wdb = new WalletDB({ + network: node.network, + logger: node.logger, + workers: node.workers, + client: this.client, + prefix: config.prefix, + db: config.str(['wallet-db', 'db']), + maxFiles: config.uint('wallet-max-files'), + cacheSize: config.mb('wallet-cache-size'), + witness: config.bool('wallet-witness'), + checkpoints: config.bool('wallet-checkpoints'), + startHeight: config.uint('wallet-start-height'), + wipeNoReally: config.bool('wallet-wipe-no-really'), + spv: node.spv + }); - this.http = new HTTP({ - network: node.network, - logger: node.logger, - node: this, - apiKey: config.str(['wallet-api-key', 'api-key']), - walletAuth: config.bool('wallet-auth'), - noAuth: config.bool(['wallet-no-auth', 'no-auth']) - }); + this.rpc = new RPC(this); - this.http.attach('/wallet', node.http); + this.http = new HTTP({ + network: node.network, + logger: node.logger, + node: this, + apiKey: config.str(['wallet-api-key', 'api-key']), + walletAuth: config.bool('wallet-auth'), + noAuth: config.bool(['wallet-no-auth', 'no-auth']) + }); - this.init(); + this.http.attach('/wallet', node.http); + + this.init(); + } + + init() { + this.wdb.on('error', err => this.emit('error', err)); + this.http.on('error', err => this.emit('error', err)); + } + + async open() { + await this.wdb.open(); + this.rpc.wallet = this.wdb.primary; + } + + async close() { + this.rpc.wallet = null; + await this.wdb.close(); + } } -Object.setPrototypeOf(Plugin.prototype, EventEmitter.prototype); - -Plugin.prototype.init = function init() { - this.wdb.on('error', err => this.emit('error', err)); - this.http.on('error', err => this.emit('error', err)); -}; - -Plugin.prototype.open = async function open() { - await this.wdb.open(); - this.rpc.wallet = this.wdb.primary; -}; - -Plugin.prototype.close = async function close() { - this.rpc.wallet = this.wdb.primary; - await this.wdb.close(); -}; - /** * Plugin name. * @const {String} diff --git a/lib/wallet/records.js b/lib/wallet/records.js index c3ce6b71..4bab3ae4 100644 --- a/lib/wallet/records.js +++ b/lib/wallet/records.js @@ -18,464 +18,479 @@ const {encoding} = bio; /** * Chain State - * @constructor */ -function ChainState() { - if (!(this instanceof ChainState)) - return new ChainState(); +class ChainState { + /** + * Create a chain state. + * @constructor + */ - this.startHeight = 0; - this.startHash = encoding.NULL_HASH; - this.height = 0; - this.marked = false; + constructor() { + this.startHeight = 0; + this.startHash = encoding.NULL_HASH; + this.height = 0; + this.marked = false; + } + + /** + * Clone the state. + * @returns {ChainState} + */ + + clone() { + const state = new ChainState(); + state.startHeight = this.startHeight; + state.startHash = this.startHash; + state.height = this.height; + state.marked = this.marked; + return state; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = bio.read(data); + + this.startHeight = br.readU32(); + this.startHash = br.readHash('hex'); + this.height = br.readU32(); + this.marked = br.readU8() === 1; + + return this; + } + + /** + * Instantiate chain state from serialized data. + * @param {Hash} hash + * @param {Buffer} data + * @returns {ChainState} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Serialize the chain state. + * @returns {Buffer} + */ + + toRaw() { + const bw = bio.write(41); + + bw.writeU32(this.startHeight); + bw.writeHash(this.startHash); + bw.writeU32(this.height); + bw.writeU8(this.marked ? 1 : 0); + + return bw.render(); + } } -/** - * Clone the state. - * @returns {ChainState} - */ - -ChainState.prototype.clone = function clone() { - const state = new ChainState(); - state.startHeight = this.startHeight; - state.startHash = this.startHash; - state.height = this.height; - state.marked = this.marked; - return state; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -ChainState.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - - this.startHeight = br.readU32(); - this.startHash = br.readHash('hex'); - this.height = br.readU32(); - this.marked = br.readU8() === 1; - - return this; -}; - -/** - * Instantiate chain state from serialized data. - * @param {Hash} hash - * @param {Buffer} data - * @returns {ChainState} - */ - -ChainState.fromRaw = function fromRaw(data) { - return new ChainState().fromRaw(data); -}; - -/** - * Serialize the chain state. - * @returns {Buffer} - */ - -ChainState.prototype.toRaw = function toRaw() { - const bw = bio.write(41); - - bw.writeU32(this.startHeight); - bw.writeHash(this.startHash); - bw.writeU32(this.height); - bw.writeU8(this.marked ? 1 : 0); - - return bw.render(); -}; - /** * Block Meta - * @constructor - * @param {Hash} hash - * @param {Number} height - * @param {Number} time */ -function BlockMeta(hash, height, time) { - if (!(this instanceof BlockMeta)) - return new BlockMeta(hash, height, time); +class BlockMeta { + /** + * Create block meta. + * @constructor + * @param {Hash} hash + * @param {Number} height + * @param {Number} time + */ - this.hash = hash || encoding.NULL_HASH; - this.height = height != null ? height : -1; - this.time = time || 0; -} - -/** - * Clone the block. - * @returns {BlockMeta} - */ - -BlockMeta.prototype.clone = function clone() { - return new BlockMeta(this.hash, this.height, this.time); -}; - -/** - * Get block meta hash as a buffer. - * @returns {Buffer} - */ - -BlockMeta.prototype.toHash = function toHash() { - return Buffer.from(this.hash, 'hex'); -}; - -/** - * Instantiate block meta from chain entry. - * @private - * @param {ChainEntry} entry - */ - -BlockMeta.prototype.fromEntry = function fromEntry(entry) { - this.hash = entry.hash; - this.height = entry.height; - this.time = entry.time; - return this; -}; - -/** - * Instantiate block meta from json object. - * @private - * @param {Object} json - */ - -BlockMeta.prototype.fromJSON = function fromJSON(json) { - this.hash = encoding.revHex(json.hash); - this.height = json.height; - this.time = json.time; - return this; -}; - -/** - * Instantiate block meta from serialized tip data. - * @private - * @param {Buffer} data - */ - -BlockMeta.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - this.hash = br.readHash('hex'); - this.height = br.readU32(); - this.time = br.readU32(); - return this; -}; - -/** - * Instantiate block meta from chain entry. - * @param {ChainEntry} entry - * @returns {BlockMeta} - */ - -BlockMeta.fromEntry = function fromEntry(entry) { - return new BlockMeta().fromEntry(entry); -}; - -/** - * Instantiate block meta from json object. - * @param {Object} json - * @returns {BlockMeta} - */ - -BlockMeta.fromJSON = function fromJSON(json) { - return new BlockMeta().fromJSON(json); -}; - -/** - * Instantiate block meta from serialized data. - * @param {Hash} hash - * @param {Buffer} data - * @returns {BlockMeta} - */ - -BlockMeta.fromRaw = function fromRaw(data) { - return new BlockMeta().fromRaw(data); -}; - -/** - * Serialize the block meta. - * @returns {Buffer} - */ - -BlockMeta.prototype.toRaw = function toRaw() { - const bw = bio.write(42); - bw.writeHash(this.hash); - bw.writeU32(this.height); - bw.writeU32(this.time); - return bw.render(); -}; - -/** - * Convert the block meta to a more json-friendly object. - * @returns {Object} - */ - -BlockMeta.prototype.toJSON = function toJSON() { - return { - hash: encoding.revHex(this.hash), - height: this.height, - time: this.time - }; -}; - -/** - * TXRecord - * @constructor - * @param {TX} tx - * @param {BlockMeta?} block - */ - -function TXRecord(tx, block) { - if (!(this instanceof TXRecord)) - return new TXRecord(tx, block); - - this.tx = null; - this.hash = null; - this.mtime = util.now(); - this.height = -1; - this.block = null; - this.index = -1; - this.time = 0; - - if (tx) - this.fromTX(tx, block); -} - -/** - * Inject properties from tx and block. - * @private - * @param {TX} tx - * @param {Block?} block - * @returns {TXRecord} - */ - -TXRecord.prototype.fromTX = function fromTX(tx, block) { - this.tx = tx; - this.hash = tx.hash('hex'); - - if (block) - this.setBlock(block); - - return this; -}; - -/** - * Instantiate tx record from tx and block. - * @param {TX} tx - * @param {Block?} block - * @returns {TXRecord} - */ - -TXRecord.fromTX = function fromTX(tx, block) { - return new TXRecord().fromTX(tx, block); -}; - -/** - * Set block data (confirm). - * @param {BlockMeta} block - */ - -TXRecord.prototype.setBlock = function setBlock(block) { - this.height = block.height; - this.block = block.hash; - this.time = block.time; -}; - -/** - * Unset block (unconfirm). - */ - -TXRecord.prototype.unsetBlock = function unsetBlock() { - this.height = -1; - this.block = null; - this.time = 0; -}; - -/** - * Convert tx record to a block meta. - * @returns {BlockMeta} - */ - -TXRecord.prototype.getBlock = function getBlock() { - if (this.height === -1) - return null; - - return new BlockMeta(this.block, this.height, this.time); -}; - -/** - * Calculate current number of transaction confirmations. - * @param {Number} height - Current chain height. - * @returns {Number} confirmations - */ - -TXRecord.prototype.getDepth = function getDepth(height) { - assert(typeof height === 'number', 'Must pass in height.'); - - if (this.height === -1) - return 0; - - if (height < this.height) - return 0; - - return height - this.height + 1; -}; - -/** - * Get serialization size. - * @returns {Number} - */ - -TXRecord.prototype.getSize = function getSize() { - let size = 0; - - size += this.tx.getSize(); - size += 4; - - if (this.block) { - size += 1; - size += 32; - size += 4 * 3; - } else { - size += 1; + constructor(hash, height, time) { + this.hash = hash || encoding.NULL_HASH; + this.height = height != null ? height : -1; + this.time = time || 0; } - return size; -}; + /** + * Clone the block. + * @returns {BlockMeta} + */ -/** - * Serialize a transaction to "extended format". - * @returns {Buffer} - */ - -TXRecord.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); - - let index = this.index; - - this.tx.toWriter(bw); - - bw.writeU32(this.mtime); - - if (this.block) { - if (index === -1) - index = 0x7fffffff; - - bw.writeU8(1); - bw.writeHash(this.block); - bw.writeU32(this.height); - bw.writeU32(this.time); - bw.writeU32(index); - } else { - bw.writeU8(0); + clone() { + return new this.constructor(this.hash, this.height, this.time); } - return bw.render(); -}; + /** + * Get block meta hash as a buffer. + * @returns {Buffer} + */ -/** - * Inject properties from "extended" format. - * @private - * @param {Buffer} data - */ + toHash() { + return Buffer.from(this.hash, 'hex'); + } -TXRecord.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); + /** + * Instantiate block meta from chain entry. + * @private + * @param {ChainEntry} entry + */ - this.tx = new TX(); - this.tx.fromReader(br); + fromEntry(entry) { + this.hash = entry.hash; + this.height = entry.height; + this.time = entry.time; + return this; + } - this.hash = this.tx.hash('hex'); - this.mtime = br.readU32(); + /** + * Instantiate block meta from json object. + * @private + * @param {Object} json + */ - if (br.readU8() === 1) { - this.block = br.readHash('hex'); + fromJSON(json) { + this.hash = encoding.revHex(json.hash); + this.height = json.height; + this.time = json.time; + return this; + } + + /** + * Instantiate block meta from serialized tip data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = bio.read(data); + this.hash = br.readHash('hex'); this.height = br.readU32(); this.time = br.readU32(); - this.index = br.readU32(); - if (this.index === 0x7fffffff) - this.index = -1; + return this; } - return this; -}; + /** + * Instantiate block meta from chain entry. + * @param {ChainEntry} entry + * @returns {BlockMeta} + */ + + static fromEntry(entry) { + return new this().fromEntry(entry); + } + + /** + * Instantiate block meta from json object. + * @param {Object} json + * @returns {BlockMeta} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Instantiate block meta from serialized data. + * @param {Hash} hash + * @param {Buffer} data + * @returns {BlockMeta} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Serialize the block meta. + * @returns {Buffer} + */ + + toRaw() { + const bw = bio.write(42); + bw.writeHash(this.hash); + bw.writeU32(this.height); + bw.writeU32(this.time); + return bw.render(); + } + + /** + * Convert the block meta to a more json-friendly object. + * @returns {Object} + */ + + toJSON() { + return { + hash: encoding.revHex(this.hash), + height: this.height, + time: this.time + }; + } +} /** - * Instantiate a transaction from a buffer - * in "extended" serialization format. - * @param {Buffer} data - * @returns {TX} + * TX Record */ -TXRecord.fromRaw = function fromRaw(data) { - return new TXRecord().fromRaw(data); -}; +class TXRecord { + /** + * Create tx record. + * @constructor + * @param {TX} tx + * @param {BlockMeta?} block + */ + + constructor(tx, block) { + this.tx = null; + this.hash = null; + this.mtime = util.now(); + this.height = -1; + this.block = null; + this.index = -1; + this.time = 0; + + if (tx) + this.fromTX(tx, block); + } + + /** + * Inject properties from tx and block. + * @private + * @param {TX} tx + * @param {Block?} block + * @returns {TXRecord} + */ + + fromTX(tx, block) { + this.tx = tx; + this.hash = tx.hash('hex'); + + if (block) + this.setBlock(block); + + return this; + } + + /** + * Instantiate tx record from tx and block. + * @param {TX} tx + * @param {Block?} block + * @returns {TXRecord} + */ + + static fromTX(tx, block) { + return new this().fromTX(tx, block); + } + + /** + * Set block data (confirm). + * @param {BlockMeta} block + */ + + setBlock(block) { + this.height = block.height; + this.block = block.hash; + this.time = block.time; + } + + /** + * Unset block (unconfirm). + */ + + unsetBlock() { + this.height = -1; + this.block = null; + this.time = 0; + } + + /** + * Convert tx record to a block meta. + * @returns {BlockMeta} + */ + + getBlock() { + if (this.height === -1) + return null; + + return new BlockMeta(this.block, this.height, this.time); + } + + /** + * Calculate current number of transaction confirmations. + * @param {Number} height - Current chain height. + * @returns {Number} confirmations + */ + + getDepth(height) { + assert(typeof height === 'number', 'Must pass in height.'); + + if (this.height === -1) + return 0; + + if (height < this.height) + return 0; + + return height - this.height + 1; + } + + /** + * Get serialization size. + * @returns {Number} + */ + + getSize() { + let size = 0; + + size += this.tx.getSize(); + size += 4; + + if (this.block) { + size += 1; + size += 32; + size += 4 * 3; + } else { + size += 1; + } + + return size; + } + + /** + * Serialize a transaction to "extended format". + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); + + let index = this.index; + + this.tx.toWriter(bw); + + bw.writeU32(this.mtime); + + if (this.block) { + if (index === -1) + index = 0x7fffffff; + + bw.writeU8(1); + bw.writeHash(this.block); + bw.writeU32(this.height); + bw.writeU32(this.time); + bw.writeU32(index); + } else { + bw.writeU8(0); + } + + return bw.render(); + } + + /** + * Inject properties from "extended" format. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = bio.read(data); + + this.tx = new TX(); + this.tx.fromReader(br); + + this.hash = this.tx.hash('hex'); + this.mtime = br.readU32(); + + if (br.readU8() === 1) { + this.block = br.readHash('hex'); + this.height = br.readU32(); + this.time = br.readU32(); + this.index = br.readU32(); + if (this.index === 0x7fffffff) + this.index = -1; + } + + return this; + } + + /** + * Instantiate a transaction from a buffer + * in "extended" serialization format. + * @param {Buffer} data + * @returns {TX} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } +} /** * Map Record - * @constructor */ -function MapRecord() { - this.wids = new Set(); +class MapRecord { + /** + * Create map record. + * @constructor + */ + + constructor() { + this.wids = new Set(); + } + + add(wid) { + if (this.wids.has(wid)) + return false; + + this.wids.add(wid); + + return true; + } + + remove(wid) { + return this.wids.delete(wid); + } + + toWriter(bw) { + bw.writeU32(this.wids.size); + + for (const wid of this.wids) + bw.writeU32(wid); + + return bw; + } + + getSize() { + return 4 + this.wids.size * 4; + } + + toRaw() { + const size = this.getSize(); + return this.toWriter(bio.write(size)).render(); + } + + fromReader(br) { + const count = br.readU32(); + + for (let i = 0; i < count; i++) + this.wids.add(br.readU32()); + + return this; + } + + fromRaw(data) { + return this.fromReader(bio.read(data)); + } + + static fromReader(br) { + return new this().fromReader(br); + } + + static fromRaw(data) { + return new this().fromRaw(data); + } } -MapRecord.prototype.add = function add(wid) { - if (this.wids.has(wid)) - return false; - - this.wids.add(wid); - - return true; -}; - -MapRecord.prototype.remove = function remove(wid) { - return this.wids.delete(wid); -}; - -MapRecord.prototype.toWriter = function toWriter(bw) { - bw.writeU32(this.wids.size); - - for (const wid of this.wids) - bw.writeU32(wid); - - return bw; -}; - -MapRecord.prototype.getSize = function getSize() { - return 4 + this.wids.size * 4; -}; - -MapRecord.prototype.toRaw = function toRaw() { - const size = this.getSize(); - return this.toWriter(bio.write(size)).render(); -}; - -MapRecord.prototype.fromReader = function fromReader(br) { - const count = br.readU32(); - - for (let i = 0; i < count; i++) - this.wids.add(br.readU32()); - - return this; -}; - -MapRecord.prototype.fromRaw = function fromRaw(data) { - return this.fromReader(bio.read(data)); -}; - -MapRecord.fromReader = function fromReader(br) { - return new MapRecord().fromReader(br); -}; - -MapRecord.fromRaw = function fromRaw(data) { - return new MapRecord().fromRaw(data); -}; - /* * Expose */ diff --git a/lib/wallet/server.js b/lib/wallet/server.js index 20bdbbd3..a252c5a9 100644 --- a/lib/wallet/server.js +++ b/lib/wallet/server.js @@ -16,121 +16,123 @@ const RPC = require('./rpc'); /** * Wallet Node * @extends Node - * @constructor */ -function WalletNode(options) { - if (!(this instanceof WalletNode)) - return new WalletNode(options); +class WalletNode extends Node { + /** + * Create a wallet node. + * @constructor + * @param {Object?} options + */ - Node.call(this, 'bcoin', 'wallet.conf', 'wallet.log', options); + constructor(options) { + super('bcoin', 'wallet.conf', 'wallet.log', options); - this.opened = false; + this.opened = false; - this.client = new Client({ - network: this.network, - url: this.config.str('node-url'), - host: this.config.str('node-host'), - port: this.config.str('node-port', this.network.rpcPort), - ssl: this.config.str('node-ssl'), - apiKey: this.config.str('node-api-key') - }); + this.client = new Client({ + network: this.network, + url: this.config.str('node-url'), + host: this.config.str('node-host'), + port: this.config.str('node-port', this.network.rpcPort), + ssl: this.config.str('node-ssl'), + apiKey: this.config.str('node-api-key') + }); - this.wdb = new WalletDB({ - network: this.network, - logger: this.logger, - workers: this.workers, - client: this.client, - prefix: this.config.prefix, - db: this.config.str('db'), - maxFiles: this.config.uint('max-files'), - cacheSize: this.config.mb('cache-size'), - witness: this.config.bool('witness'), - checkpoints: this.config.bool('checkpoints'), - startHeight: this.config.uint('start-height'), - wipeNoReally: this.config.bool('wipe-no-really'), - spv: this.config.bool('spv') - }); + this.wdb = new WalletDB({ + network: this.network, + logger: this.logger, + workers: this.workers, + client: this.client, + prefix: this.config.prefix, + db: this.config.str('db'), + maxFiles: this.config.uint('max-files'), + cacheSize: this.config.mb('cache-size'), + witness: this.config.bool('witness'), + checkpoints: this.config.bool('checkpoints'), + startHeight: this.config.uint('start-height'), + wipeNoReally: this.config.bool('wipe-no-really'), + spv: this.config.bool('spv') + }); - this.rpc = new RPC(this); + this.rpc = new RPC(this); - this.http = new HTTP({ - network: this.network, - logger: this.logger, - node: this, - prefix: this.config.prefix, - ssl: this.config.bool('ssl'), - keyFile: this.config.path('ssl-key'), - certFile: this.config.path('ssl-cert'), - host: this.config.str('http-host'), - port: this.config.uint('http-port'), - apiKey: this.config.str('api-key'), - noAuth: this.config.bool('no-auth'), - walletAuth: this.config.bool('wallet-auth') - }); + this.http = new HTTP({ + network: this.network, + logger: this.logger, + node: this, + prefix: this.config.prefix, + ssl: this.config.bool('ssl'), + keyFile: this.config.path('ssl-key'), + certFile: this.config.path('ssl-cert'), + host: this.config.str('http-host'), + port: this.config.uint('http-port'), + apiKey: this.config.str('api-key'), + noAuth: this.config.bool('no-auth'), + walletAuth: this.config.bool('wallet-auth') + }); - this.init(); + this.init(); + } + + /** + * Initialize the node. + * @private + */ + + init() { + this.wdb.on('error', err => this.error(err)); + this.http.on('error', err => this.error(err)); + + this.loadPlugins(); + } + + /** + * Open the node and all its child objects, + * wait for the database to load. + * @alias WalletNode#open + * @returns {Promise} + */ + + async open() { + assert(!this.opened, 'WalletNode is already open.'); + this.opened = true; + + await this.handlePreopen(); + await this.wdb.open(); + + this.rpc.wallet = this.wdb.primary; + + await this.openPlugins(); + + await this.http.open(); + await this.handleOpen(); + + this.logger.info('Wallet node is loaded.'); + } + + /** + * Close the node, wait for the database to close. + * @alias WalletNode#close + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'WalletNode is not open.'); + this.opened = false; + + await this.handlePreclose(); + await this.http.close(); + + await this.closePlugins(); + + this.rpc.wallet = null; + + await this.wdb.close(); + await this.handleClose(); + } } -Object.setPrototypeOf(WalletNode.prototype, Node.prototype); - -/** - * Initialize the node. - * @private - */ - -WalletNode.prototype.init = function init() { - this.wdb.on('error', err => this.error(err)); - this.http.on('error', err => this.error(err)); - - this.loadPlugins(); -}; - -/** - * Open the node and all its child objects, - * wait for the database to load. - * @alias WalletNode#open - * @returns {Promise} - */ - -WalletNode.prototype.open = async function open() { - assert(!this.opened, 'WalletNode is already open.'); - this.opened = true; - - await this.handlePreopen(); - await this.wdb.open(); - - this.rpc.wallet = this.wdb.primary; - - await this.openPlugins(); - - await this.http.open(); - await this.handleOpen(); - - this.logger.info('Wallet node is loaded.'); -}; - -/** - * Close the node, wait for the database to close. - * @alias WalletNode#close - * @returns {Promise} - */ - -WalletNode.prototype.close = async function close() { - assert(this.opened, 'WalletNode is not open.'); - this.opened = false; - - await this.handlePreclose(); - await this.http.close(); - - await this.closePlugins(); - - this.rpc.wallet = null; - - await this.wdb.close(); - await this.handleClose(); -}; - /* * Expose */ diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index bb933259..f25c6f19 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -23,22 +23,2083 @@ const {TXRecord} = records; /** * TXDB * @alias module:wallet.TXDB - * @constructor - * @param {WalletDB} wdb */ -function TXDB(wdb, wid) { - if (!(this instanceof TXDB)) - return new TXDB(wdb); +class TXDB { + /** + * Create a TXDB. + * @constructor + * @param {WalletDB} wdb + */ - this.wdb = wdb; - this.db = wdb.db; - this.logger = wdb.logger; + constructor(wdb, wid) { + this.wdb = wdb; + this.db = wdb.db; + this.logger = wdb.logger; - this.wid = wid || 0; - this.prefix = layout.prefix(this.wid); - this.wallet = null; - this.locked = new Set(); + this.wid = wid || 0; + this.prefix = layout.prefix(this.wid); + this.wallet = null; + this.locked = new Set(); + } + + /** + * Open TXDB. + * @returns {Promise} + */ + + async open(wallet) { + this.wid = wallet.wid; + this.prefix = layout.prefix(this.wid); + this.wallet = wallet; + } + + /** + * Emit transaction event. + * @private + * @param {String} event + * @param {Object} data + * @param {Details} details + */ + + emit(event, data, details) { + this.wdb.emit(event, this.wallet, data, details); + this.wallet.emit(event, data, details); + } + + /** + * Bucket + * @returns {Bucket} + */ + + bucket() { + return this.db.bucket(this.prefix); + } + + /** + * Get. + * @param {String} key + */ + + get(key) { + return this.bucket().get(key); + } + + /** + * Has. + * @param {String} key + */ + + has(key) { + return this.bucket().has(key); + } + + /** + * Iterate. + * @param {Object} options + * @returns {Promise} + */ + + range(options) { + return this.bucket().range(options); + } + + /** + * Iterate. + * @param {Object} options + * @returns {Promise} + */ + + keys(options) { + return this.bucket().keys(options); + } + + /** + * Iterate. + * @param {Object} options + * @returns {Promise} + */ + + values(options) { + return this.bucket().values(options); + } + + /** + * Get wallet path for output. + * @param {Output} output + * @returns {Promise} - Returns {@link Path}. + */ + + getPath(output) { + const hash = output.getHash('hex'); + + if (!hash) + return null; + + return this.wdb.getPath(this.wid, hash); + } + + /** + * Test whether path exists for output. + * @param {Output} output + * @returns {Promise} - Returns Boolean. + */ + + hasPath(output) { + const hash = output.getHash('hex'); + + if (!hash) + return false; + + return this.wdb.hasPath(this.wid, hash); + } + + /** + * Save credit. + * @param {Credit} credit + * @param {Path} path + */ + + async saveCredit(b, credit, path) { + const {coin} = credit; + + b.put(layout.c(coin.hash, coin.index), credit.toRaw()); + b.put(layout.C(path.account, coin.hash, coin.index), null); + + return this.addOutpointMap(b, coin.hash, coin.index); + } + + /** + * Remove credit. + * @param {Credit} credit + * @param {Path} path + */ + + async removeCredit(b, credit, path) { + const {coin} = credit; + + b.del(layout.c(coin.hash, coin.index)); + b.del(layout.C(path.account, coin.hash, coin.index)); + + return this.removeOutpointMap(b, coin.hash, coin.index); + } + + /** + * Spend credit. + * @param {Credit} credit + * @param {TX} tx + * @param {Number} index + */ + + spendCredit(b, credit, tx, index) { + const prevout = tx.inputs[index].prevout; + const spender = Outpoint.fromTX(tx, index); + b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); + b.put(layout.d(spender.hash, spender.index), credit.coin.toRaw()); + } + + /** + * Unspend credit. + * @param {TX} tx + * @param {Number} index + */ + + unspendCredit(b, tx, index) { + const prevout = tx.inputs[index].prevout; + const spender = Outpoint.fromTX(tx, index); + b.del(layout.s(prevout.hash, prevout.index)); + b.del(layout.d(spender.hash, spender.index)); + } + + /** + * Write input record. + * @param {TX} tx + * @param {Number} index + */ + + async writeInput(b, tx, index) { + const prevout = tx.inputs[index].prevout; + const spender = Outpoint.fromTX(tx, index); + b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); + return this.addOutpointMap(b, prevout.hash, prevout.index); + } + + /** + * Remove input record. + * @param {TX} tx + * @param {Number} index + */ + + async removeInput(b, tx, index) { + const prevout = tx.inputs[index].prevout; + b.del(layout.s(prevout.hash, prevout.index)); + return this.removeOutpointMap(b, prevout.hash, prevout.index); + } + + /** + * Update wallet balance. + * @param {BalanceDelta} state + */ + + async updateBalance(b, state) { + const balance = await this.getWalletBalance(); + state.applyTo(balance); + b.put(layout.R, balance.toRaw()); + return balance; + } + + /** + * Update account balance. + * @param {Number} acct + * @param {Balance} delta + */ + + async updateAccountBalance(b, acct, delta) { + const balance = await this.getAccountBalance(acct); + delta.applyTo(balance); + b.put(layout.r(acct), balance.toRaw()); + return balance; + } + + /** + * Test a whether a coin has been spent. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns Boolean. + */ + + async getSpent(hash, index) { + const data = await this.get(layout.s(hash, index)); + + if (!data) + return null; + + return Outpoint.fromRaw(data); + } + + /** + * Test a whether a coin has been spent. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns Boolean. + */ + + isSpent(hash, index) { + return this.has(layout.s(hash, index)); + } + + /** + * Append to global map. + * @param {Number} height + * @returns {Promise} + */ + + addBlockMap(b, height) { + return this.wdb.addBlockMap(b.batch, height, this.wid); + } + + /** + * Remove from global map. + * @param {Number} height + * @returns {Promise} + */ + + removeBlockMap(b, height) { + return this.wdb.removeBlockMap(b.batch, height, this.wid); + } + + /** + * Append to global map. + * @param {Hash} hash + * @returns {Promise} + */ + + addTXMap(b, hash) { + return this.wdb.addTXMap(b.batch, hash, this.wid); + } + + /** + * Remove from global map. + * @param {Hash} hash + * @returns {Promise} + */ + + removeTXMap(b, hash) { + return this.wdb.removeTXMap(b.batch, hash, this.wid); + } + + /** + * Append to global map. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} + */ + + addOutpointMap(b, hash, index) { + return this.wdb.addOutpointMap(b.batch, hash, index, this.wid); + } + + /** + * Remove from global map. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} + */ + + removeOutpointMap(b, hash, index) { + return this.wdb.removeOutpointMap(b.batch, hash, index, this.wid); + } + + /** + * List block records. + * @returns {Promise} + */ + + getBlocks() { + return this.keys({ + gte: layout.b(0), + lte: layout.b(0xffffffff), + parse: key => layout.bb(key) + }); + } + + /** + * Get block record. + * @param {Number} height + * @returns {Promise} + */ + + async getBlock(height) { + const data = await this.get(layout.b(height)); + + if (!data) + return null; + + return BlockRecord.fromRaw(data); + } + + /** + * Append to the global block record. + * @param {Hash} hash + * @param {BlockMeta} block + * @returns {Promise} + */ + + async addBlock(b, hash, block) { + const key = layout.b(block.height); + const data = await this.get(key); + + if (!data) { + const blk = BlockRecord.fromMeta(block); + blk.add(hash); + b.put(key, blk.toRaw()); + return; + } + + const raw = Buffer.allocUnsafe(data.length + 32); + data.copy(raw, 0); + + const size = raw.readUInt32LE(40, true); + raw.writeUInt32LE(size + 1, 40, true); + hash.copy(raw, data.length); + + b.put(key, raw); + } + + /** + * Remove from the global block record. + * @param {Hash} hash + * @param {Number} height + * @returns {Promise} + */ + + async removeBlock(b, hash, height) { + const key = layout.b(height); + const data = await this.get(key); + + if (!data) + return; + + const size = data.readUInt32LE(40, true); + + assert(size > 0); + assert(data.slice(-32).equals(hash)); + + if (size === 1) { + b.del(key); + return; + } + + const raw = data.slice(0, -32); + raw.writeUInt32LE(size - 1, 40, true); + + b.put(key, raw); + } + + /** + * Remove from the global block record. + * @param {Hash} hash + * @param {Number} height + * @returns {Promise} + */ + + async spliceBlock(b, hash, height) { + const block = await this.getBlock(height); + + if (!block) + return; + + if (!block.remove(hash)) + return; + + if (block.hashes.size === 0) { + b.del(layout.b(height)); + return; + } + + b.put(layout.b(height), block.toRaw()); + } + + /** + * Add transaction without a batch. + * @private + * @param {TX} tx + * @returns {Promise} + */ + + async add(tx, block) { + const hash = tx.hash('hex'); + const existing = await this.getTX(hash); + + assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); + + if (existing) { + // Existing tx is already confirmed. Ignore. + if (existing.height !== -1) + return null; + + // The incoming tx won't confirm the + // existing one anyway. Ignore. + if (!block) + return null; + + // Confirm transaction. + return this.confirm(existing, block); + } + + const wtx = TXRecord.fromTX(tx, block); + + if (!block) { + // Potentially remove double-spenders. + // Only remove if they're not confirmed. + if (!await this.removeConflicts(tx, true)) + return null; + } else { + // Potentially remove double-spenders. + await this.removeConflicts(tx, false); + } + + // Finally we can do a regular insertion. + return this.insert(wtx, block); + } + + /** + * Insert transaction. + * @private + * @param {TXRecord} wtx + * @param {BlockMeta} block + * @returns {Promise} + */ + + async insert(wtx, block) { + const b = this.bucket(); + const {tx, hash} = wtx; + const height = block ? block.height : -1; + const details = new Details(wtx, block); + const state = new BalanceDelta(); + + let own = false; + + if (!tx.isCoinbase()) { + // We need to potentially spend some coins here. + for (let i = 0; i < tx.inputs.length; i++) { + const input = tx.inputs[i]; + const {hash, index} = input.prevout; + const credit = await this.getCredit(hash, index); + + if (!credit) { + // Watch all inputs for incoming txs. + // This allows us to check for double spends. + if (!block) + await this.writeInput(b, tx, i); + continue; + } + + const coin = credit.coin; + const path = await this.getPath(coin); + assert(path); + + // Build the tx details object + // as we go, for speed. + details.setInput(i, path, coin); + + // Write an undo coin for the credit + // and add it to the stxo set. + this.spendCredit(b, credit, tx, i); + + // Unconfirmed balance should always + // be updated as it reflects the on-chain + // balance _and_ mempool balance assuming + // everything in the mempool were to confirm. + state.tx(path, 1); + state.coin(path, -1); + state.unconfirmed(path, -coin.value); + + if (!block) { + // If the tx is not mined, we do not + // disconnect the coin, we simply mark + // a `spent` flag on the credit. This + // effectively prevents the mempool + // from altering our utxo state + // permanently. It also makes it + // possible to compare the on-chain + // state vs. the mempool state. + credit.spent = true; + await this.saveCredit(b, credit, path); + } else { + // If the tx is mined, we can safely + // remove the coin being spent. This + // coin will be indexed as an undo + // coin so it can be reconnected + // later during a reorg. + state.confirmed(path, -coin.value); + await this.removeCredit(b, credit, path); + } + + own = true; + } + } + + // Potentially add coins to the utxo set. + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const path = await this.getPath(output); + + if (!path) + continue; + + details.setOutput(i, path); + + const credit = Credit.fromTX(tx, i, height); + credit.own = own; + + state.tx(path, 1); + state.coin(path, 1); + state.unconfirmed(path, output.value); + + if (block) + state.confirmed(path, output.value); + + await this.saveCredit(b, credit, path); + } + + // If this didn't update any coins, + // it's not our transaction. + if (!state.updated()) + return null; + + // Save and index the transaction record. + b.put(layout.t(hash), wtx.toRaw()); + b.put(layout.m(wtx.mtime, hash), null); + + if (!block) + b.put(layout.p(hash), null); + else + b.put(layout.h(height, hash), null); + + // Do some secondary indexing for account-based + // queries. This saves us a lot of time for + // queries later. + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + + b.put(layout.T(acct, hash), null); + b.put(layout.M(acct, wtx.mtime, hash), null); + + if (!block) + b.put(layout.P(acct, hash), null); + else + b.put(layout.H(acct, height, hash), null); + } + + // Update block records. + if (block) { + await this.addBlockMap(b, height); + await this.addBlock(b, tx.hash(), block); + } else { + await this.addTXMap(b, hash); + } + + // Commit the new state. + const balance = await this.updateBalance(b, state); + + await b.write(); + + // This transaction may unlock some + // coins now that we've seen it. + this.unlockTX(tx); + + // Emit events for potential local and + // websocket listeners. Note that these + // will only be emitted if the batch is + // successfully written to disk. + this.emit('tx', tx, details); + this.emit('balance', balance); + + return details; + } + + /** + * Attempt to confirm a transaction. + * @private + * @param {TXRecord} wtx + * @param {BlockMeta} block + * @returns {Promise} + */ + + async confirm(wtx, block) { + const b = this.bucket(); + const {tx, hash} = wtx; + const height = block.height; + const details = new Details(wtx, block); + const state = new BalanceDelta(); + + wtx.setBlock(block); + + if (!tx.isCoinbase()) { + const credits = await this.getSpentCredits(tx); + + // Potentially spend coins. Now that the tx + // is mined, we can actually _remove_ coins + // from the utxo state. + for (let i = 0; i < tx.inputs.length; i++) { + const input = tx.inputs[i]; + const {hash, index} = input.prevout; + + let resolved = false; + + // There may be new credits available + // that we haven't seen yet. + if (!credits[i]) { + await this.removeInput(b, tx, i); + + const credit = await this.getCredit(hash, index); + + if (!credit) + continue; + + // Add a spend record and undo coin + // for the coin we now know is ours. + // We don't need to remove the coin + // since it was never added in the + // first place. + this.spendCredit(b, credit, tx, i); + + credits[i] = credit; + resolved = true; + } + + const credit = credits[i]; + const coin = credit.coin; + + assert(coin.height !== -1); + + const path = await this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + if (resolved) { + state.coin(path, -1); + state.unconfirmed(path, -coin.value); + } + + // We can now safely remove the credit + // entirely, now that we know it's also + // been removed on-chain. + state.confirmed(path, -coin.value); + + await this.removeCredit(b, credit, path); + } + } + + // Update credit heights, including undo coins. + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const path = await this.getPath(output); + + if (!path) + continue; + + details.setOutput(i, path); + + const credit = await this.getCredit(hash, i); + assert(credit); + + // Credits spent in the mempool add an + // undo coin for ease. If this credit is + // spent in the mempool, we need to + // update the undo coin's height. + if (credit.spent) + await this.updateSpentCoin(b, tx, i, height); + + // Update coin height and confirmed + // balance. Save once again. + state.confirmed(path, output.value); + credit.coin.height = height; + + await this.saveCredit(b, credit, path); + } + + // Save the new serialized transaction as + // the block-related properties have been + // updated. Also reindex for height. + b.put(layout.t(hash), wtx.toRaw()); + b.del(layout.p(hash)); + b.put(layout.h(height, hash), null); + + // Secondary indexing also needs to change. + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + b.del(layout.P(acct, hash)); + b.put(layout.H(acct, height, hash), null); + } + + await this.removeTXMap(b, hash); + await this.addBlockMap(b, height); + await this.addBlock(b, tx.hash(), block); + + // Commit the new state. The balance has updated. + const balance = await this.updateBalance(b, state); + + await b.write(); + + this.unlockTX(tx); + + this.emit('confirmed', tx, details); + this.emit('balance', balance); + + return details; + } + + /** + * Recursively remove a transaction + * from the database. + * @param {Hash} hash + * @returns {Promise} + */ + + async remove(hash) { + const wtx = await this.getTX(hash); + + if (!wtx) + return null; + + return this.removeRecursive(wtx); + } + + /** + * Remove a transaction from the + * database. Disconnect inputs. + * @private + * @param {TXRecord} wtx + * @returns {Promise} + */ + + async erase(wtx, block) { + const b = this.bucket(); + const {tx, hash} = wtx; + const height = block ? block.height : -1; + const details = new Details(wtx, block); + const state = new BalanceDelta(); + + if (!tx.isCoinbase()) { + // We need to undo every part of the + // state this transaction ever touched. + // Start by getting the undo coins. + const credits = await this.getSpentCredits(tx); + + for (let i = 0; i < tx.inputs.length; i++) { + const credit = credits[i]; + + if (!credit) { + if (!block) + await this.removeInput(b, tx, i); + continue; + } + + const coin = credit.coin; + const path = await this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + // Recalculate the balance, remove + // from stxo set, remove the undo + // coin, and resave the credit. + state.tx(path, -1); + state.coin(path, 1); + state.unconfirmed(path, coin.value); + + if (block) + state.confirmed(path, coin.value); + + this.unspendCredit(b, tx, i); + + credit.spent = false; + await this.saveCredit(b, credit, path); + } + } + + // We need to remove all credits + // this transaction created. + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const path = await this.getPath(output); + + if (!path) + continue; + + details.setOutput(i, path); + + const credit = Credit.fromTX(tx, i, height); + + state.tx(path, -1); + state.coin(path, -1); + state.unconfirmed(path, -output.value); + + if (block) + state.confirmed(path, -output.value); + + await this.removeCredit(b, credit, path); + } + + // Remove the transaction data + // itself as well as unindex. + b.del(layout.t(hash)); + b.del(layout.m(wtx.mtime, hash)); + + if (!block) + b.del(layout.p(hash)); + else + b.del(layout.h(height, hash)); + + // Remove all secondary indexing. + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + + b.del(layout.T(acct, hash)); + b.del(layout.M(acct, wtx.mtime, hash)); + + if (!block) + b.del(layout.P(acct, hash)); + else + b.del(layout.H(acct, height, hash)); + } + + // Update block records. + if (block) { + await this.removeBlockMap(b, height); + await this.spliceBlock(b, hash, height); + } else { + await this.removeTXMap(b, hash); + } + + // Update the transaction counter + // and commit new state due to + // balance change. + const balance = await this.updateBalance(b, state); + + await b.write(); + + this.emit('remove tx', tx, details); + this.emit('balance', balance); + + return details; + } + + /** + * Remove a transaction and recursively + * remove all of its spenders. + * @private + * @param {TXRecord} wtx + * @returns {Promise} + */ + + async removeRecursive(wtx) { + const {tx, hash} = wtx; + + for (let i = 0; i < tx.outputs.length; i++) { + const spent = await this.getSpent(hash, i); + + if (!spent) + continue; + + // Remove all of the spender's spenders first. + const stx = await this.getTX(spent.hash); + + assert(stx); + + await this.removeRecursive(stx); + } + + // Remove the spender. + return this.erase(wtx, wtx.getBlock()); + } + + /** + * Revert a block. + * @param {Number} height + * @returns {Promise} + */ + + async revert(height) { + const block = await this.getBlock(height); + + if (!block) + return 0; + + const hashes = block.toArray(); + + for (let i = hashes.length - 1; i >= 0; i--) { + const hash = hashes[i]; + await this.unconfirm(hash); + } + + return hashes.length; + } + + /** + * Unconfirm a transaction without a batch. + * @private + * @param {Hash} hash + * @returns {Promise} + */ + + async unconfirm(hash) { + const wtx = await this.getTX(hash); + + if (!wtx) + return null; + + if (wtx.height === -1) + return null; + + return this.disconnect(wtx, wtx.getBlock()); + } + + /** + * Unconfirm a transaction. Necessary after a reorg. + * @param {TXRecord} wtx + * @returns {Promise} + */ + + async disconnect(wtx, block) { + const b = this.bucket(); + const {tx, hash, height} = wtx; + const details = new Details(wtx, block); + const state = new BalanceDelta(); + + assert(block); + + wtx.unsetBlock(); + + if (!tx.isCoinbase()) { + // We need to reconnect the coins. Start + // by getting all of the undo coins we know + // about. + const credits = await this.getSpentCredits(tx); + + for (let i = 0; i < tx.inputs.length; i++) { + const credit = credits[i]; + + if (!credit) { + await this.writeInput(b, tx, i); + continue; + } + + const coin = credit.coin; + + assert(coin.height !== -1); + + const path = await this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + state.confirmed(path, coin.value); + + // Resave the credit and mark it + // as spent in the mempool instead. + credit.spent = true; + await this.saveCredit(b, credit, path); + } + } + + // We need to remove heights on + // the credits and undo coins. + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const path = await this.getPath(output); + + if (!path) + continue; + + const credit = await this.getCredit(hash, i); + + // Potentially update undo coin height. + if (!credit) { + await this.updateSpentCoin(b, tx, i, height); + continue; + } + + if (credit.spent) + await this.updateSpentCoin(b, tx, i, height); + + details.setOutput(i, path); + + // Update coin height and confirmed + // balance. Save once again. + credit.coin.height = -1; + + state.confirmed(path, -output.value); + + await this.saveCredit(b, credit, path); + } + + await this.addTXMap(b, hash); + await this.removeBlockMap(b, height); + await this.removeBlock(b, tx.hash(), height); + + // We need to update the now-removed + // block properties and reindex due + // to the height change. + b.put(layout.t(hash), wtx.toRaw()); + b.put(layout.p(hash), null); + b.del(layout.h(height, hash)); + + // Secondary indexing also needs to change. + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + b.put(layout.P(acct, hash), null); + b.del(layout.H(acct, height, hash)); + } + + // Commit state due to unconfirmed + // vs. confirmed balance change. + const balance = await this.updateBalance(b, state); + + await b.write(); + + this.emit('unconfirmed', tx, details); + this.emit('balance', balance); + + return details; + } + + /** + * Remove spenders that have not been confirmed. We do this in the + * odd case of stuck transactions or when a coin is double-spent + * by a newer transaction. All previously-spending transactions + * of that coin that are _not_ confirmed will be removed from + * the database. + * @private + * @param {Hash} hash + * @param {TX} ref - Reference tx, the tx that double-spent. + * @returns {Promise} - Returns Boolean. + */ + + async removeConflict(wtx) { + const tx = wtx.tx; + + this.logger.warning('Handling conflicting tx: %s.', tx.txid()); + + const details = await this.removeRecursive(wtx); + + this.logger.warning('Removed conflict: %s.', tx.txid()); + + // Emit the _removed_ transaction. + this.emit('conflict', tx, details); + + return details; + } + + /** + * Retrieve coins for own inputs, remove + * double spenders, and verify inputs. + * @private + * @param {TX} tx + * @returns {Promise} + */ + + async removeConflicts(tx, conf) { + if (tx.isCoinbase()) + return true; + + const txid = tx.hash('hex'); + const spends = []; + + // Gather all spent records first. + for (const {prevout} of tx.inputs) { + const {hash, index} = prevout; + + // Is it already spent? + const spent = await this.getSpent(hash, index); + + if (!spent) + continue; + + // Did _we_ spend it? + if (spent.hash === txid) + continue; + + const spender = await this.getTX(spent.hash); + assert(spender); + + if (conf && spender.height !== -1) + return false; + + spends.push(spender); + } + + // Once we know we're not going to + // screw things up, remove the double + // spenders. + for (const spender of spends) { + // Remove the double spender. + await this.removeConflict(spender); + } + + return true; + } + + /** + * Lock all coins in a transaction. + * @param {TX} tx + */ + + lockTX(tx) { + if (tx.isCoinbase()) + return; + + for (const input of tx.inputs) + this.lockCoin(input.prevout); + } + + /** + * Unlock all coins in a transaction. + * @param {TX} tx + */ + + unlockTX(tx) { + if (tx.isCoinbase()) + return; + + for (const input of tx.inputs) + this.unlockCoin(input.prevout); + } + + /** + * Lock a single coin. + * @param {Coin|Outpoint} coin + */ + + lockCoin(coin) { + const key = coin.toKey(); + this.locked.add(key); + } + + /** + * Unlock a single coin. + * @param {Coin|Outpoint} coin + */ + + unlockCoin(coin) { + const key = coin.toKey(); + return this.locked.delete(key); + } + + /** + * Test locked status of a single coin. + * @param {Coin|Outpoint} coin + */ + + isLocked(coin) { + const key = coin.toKey(); + return this.locked.has(key); + } + + /** + * Filter array of coins or outpoints + * for only unlocked ones. + * @param {Coin[]|Outpoint[]} + * @returns {Array} + */ + + filterLocked(coins) { + const out = []; + + for (const coin of coins) { + if (!this.isLocked(coin)) + out.push(coin); + } + + return out; + } + + /** + * Return an array of all locked outpoints. + * @returns {Outpoint[]} + */ + + getLocked() { + const outpoints = []; + + for (const key of this.locked.keys()) + outpoints.push(Outpoint.fromKey(key)); + + return outpoints; + } + + /** + * Get hashes of all transactions in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getAccountHistoryHashes(acct) { + assert(typeof acct === 'number'); + return this.keys({ + gte: layout.T(acct, encoding.NULL_HASH), + lte: layout.T(acct, encoding.HIGH_HASH), + parse: (key) => { + const [, hash] = layout.Tt(key); + return hash; + } + }); + } + + /** + * Get hashes of all transactions in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getHistoryHashes(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountHistoryHashes(acct); + + return this.keys({ + gte: layout.t(encoding.NULL_HASH), + lte: layout.t(encoding.HIGH_HASH), + parse: key => layout.tt(key) + }); + } + + /** + * Get hashes of all unconfirmed transactions in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getAccountPendingHashes(acct) { + assert(typeof acct === 'number'); + return this.keys({ + gte: layout.P(acct, encoding.NULL_HASH), + lte: layout.P(acct, encoding.HIGH_HASH), + parse: (key) => { + const [, hash] = layout.Pp(key); + return hash; + } + }); + } + + /** + * Get hashes of all unconfirmed transactions in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getPendingHashes(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountPendingHashes(acct); + + return this.keys({ + gte: layout.p(encoding.NULL_HASH), + lte: layout.p(encoding.HIGH_HASH), + parse: key => layout.pp(key) + }); + } + + /** + * Get all coin hashes in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getAccountOutpoints(acct) { + assert(typeof acct === 'number'); + return this.keys({ + gte: layout.C(acct, encoding.NULL_HASH, 0), + lte: layout.C(acct, encoding.HIGH_HASH, 0xffffffff), + parse: (key) => { + const [, hash, index] = layout.Cc(key); + return new Outpoint(hash, index); + } + }); + } + + /** + * Get all coin hashes in the database. + * @param {Number} acct + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getOutpoints(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountOutpoints(acct); + + return this.keys({ + gte: layout.c(encoding.NULL_HASH, 0), + lte: layout.c(encoding.HIGH_HASH, 0xffffffff), + parse: (key) => { + const [hash, index] = layout.cc(key); + return new Outpoint(hash, index); + } + }); + } + + /** + * Get TX hashes by height range. + * @param {Number} acct + * @param {Object} options + * @param {Number} options.start - Start height. + * @param {Number} options.end - End height. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getAccountHeightRangeHashes(acct, options) { + assert(typeof acct === 'number'); + + const start = options.start || 0; + const end = options.end || 0xffffffff; + + return this.keys({ + gte: layout.H(acct, start, encoding.NULL_HASH), + lte: layout.H(acct, end, encoding.HIGH_HASH), + limit: options.limit, + reverse: options.reverse, + parse: (key) => { + const [,, hash] = layout.Hh(key); + return hash; + } + }); + } + + /** + * Get TX hashes by height range. + * @param {Number} acct + * @param {Object} options + * @param {Number} options.start - Start height. + * @param {Number} options.end - End height. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getHeightRangeHashes(acct, options) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountHeightRangeHashes(acct, options); + + const start = options.start || 0; + const end = options.end || 0xffffffff; + + return this.keys({ + gte: layout.h(start, encoding.NULL_HASH), + lte: layout.h(end, encoding.HIGH_HASH), + limit: options.limit, + reverse: options.reverse, + parse: (key) => { + const [, hash] = layout.hh(key); + return hash; + } + }); + } + + /** + * Get TX hashes by height. + * @param {Number} height + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getHeightHashes(height) { + return this.getHeightRangeHashes({ start: height, end: height }); + } + + /** + * Get TX hashes by timestamp range. + * @param {Number} acct + * @param {Object} options + * @param {Number} options.start - Start height. + * @param {Number} options.end - End height. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getAccountRangeHashes(acct, options) { + assert(typeof acct === 'number'); + + const start = options.start || 0; + const end = options.end || 0xffffffff; + + return this.keys({ + gte: layout.M(acct, start, encoding.NULL_HASH), + lte: layout.M(acct, end, encoding.HIGH_HASH), + limit: options.limit, + reverse: options.reverse, + parse: (key) => { + const [,, hash] = layout.Mm(key); + return hash; + } + }); + } + + /** + * Get TX hashes by timestamp range. + * @param {Number} acct + * @param {Object} options + * @param {Number} options.start - Start height. + * @param {Number} options.end - End height. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getRangeHashes(acct, options) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountRangeHashes(acct, options); + + const start = options.start || 0; + const end = options.end || 0xffffffff; + + return this.keys({ + gte: layout.m(start, encoding.NULL_HASH), + lte: layout.m(end, encoding.HIGH_HASH), + limit: options.limit, + reverse: options.reverse, + parse: (key) => { + const [, hash] = layout.mm(key); + return hash; + } + }); + } + + /** + * Get transactions by timestamp range. + * @param {Number} acct + * @param {Object} options + * @param {Number} options.start - Start time. + * @param {Number} options.end - End time. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} - Returns {@link TX}[]. + */ + + async getRange(acct, options) { + const hashes = await this.getRangeHashes(acct, options); + const txs = []; + + for (const hash of hashes) { + const tx = await this.getTX(hash); + assert(tx); + txs.push(tx); + } + + return txs; + } + + /** + * Get last N transactions. + * @param {Number} acct + * @param {Number} limit - Max number of transactions. + * @returns {Promise} - Returns {@link TX}[]. + */ + + getLast(acct, limit) { + return this.getRange(acct, { + start: 0, + end: 0xffffffff, + reverse: true, + limit: limit || 10 + }); + } + + /** + * Get all transactions. + * @param {Number} acct + * @returns {Promise} - Returns {@link TX}[]. + */ + + getHistory(acct) { + assert(typeof acct === 'number'); + + // Slow case + if (acct !== -1) + return this.getAccountHistory(acct); + + // Fast case + return this.values({ + gte: layout.t(encoding.NULL_HASH), + lte: layout.t(encoding.HIGH_HASH), + parse: data => TXRecord.fromRaw(data) + }); + } + + /** + * Get all acct transactions. + * @param {Number} acct + * @returns {Promise} - Returns {@link TX}[]. + */ + + async getAccountHistory(acct) { + const hashes = await this.getHistoryHashes(acct); + const txs = []; + + for (const hash of hashes) { + const tx = await this.getTX(hash); + assert(tx); + txs.push(tx); + } + + return txs; + } + + /** + * Get unconfirmed transactions. + * @param {Number} acct + * @returns {Promise} - Returns {@link TX}[]. + */ + + async getPending(acct) { + const hashes = await this.getPendingHashes(acct); + const txs = []; + + for (const hash of hashes) { + const tx = await this.getTX(hash); + assert(tx); + txs.push(tx); + } + + return txs; + } + + /** + * Get coins. + * @param {Number} acct + * @returns {Promise} - Returns {@link Coin}[]. + */ + + getCredits(acct) { + assert(typeof acct === 'number'); + + // Slow case + if (acct !== -1) + return this.getAccountCredits(acct); + + // Fast case + return this.range({ + gte: layout.c(encoding.NULL_HASH, 0x00000000), + lte: layout.c(encoding.HIGH_HASH, 0xffffffff), + parse: (key, value) => { + const [hash, index] = layout.cc(key); + const credit = Credit.fromRaw(value); + credit.coin.hash = hash; + credit.coin.index = index; + return credit; + } + }); + } + + /** + * Get coins by account. + * @param {Number} acct + * @returns {Promise} - Returns {@link Coin}[]. + */ + + async getAccountCredits(acct) { + const outpoints = await this.getOutpoints(acct); + const credits = []; + + for (const {hash, index} of outpoints) { + const credit = await this.getCredit(hash, index); + assert(credit); + credits.push(credit); + } + + return credits; + } + + /** + * Fill a transaction with coins (all historical coins). + * @param {TX} tx + * @returns {Promise} - Returns {@link TX}. + */ + + async getSpentCredits(tx) { + if (tx.isCoinbase()) + return []; + + const hash = tx.hash('hex'); + const credits = []; + + for (let i = 0; i < tx.inputs.length; i++) + credits.push(null); + + await this.range({ + gte: layout.d(hash, 0x00000000), + lte: layout.d(hash, 0xffffffff), + parse: (key, value) => { + const [, index] = layout.dd(key); + const coin = Coin.fromRaw(value); + const input = tx.inputs[index]; + assert(input); + coin.hash = input.prevout.hash; + coin.index = input.prevout.index; + credits[index] = new Credit(coin); + } + }); + + return credits; + } + + /** + * Get coins. + * @param {Number} acct + * @returns {Promise} - Returns {@link Coin}[]. + */ + + async getCoins(acct) { + const credits = await this.getCredits(acct); + const coins = []; + + for (const credit of credits) { + if (credit.spent) + continue; + + coins.push(credit.coin); + } + + return coins; + } + + /** + * Get coins by account. + * @param {Number} acct + * @returns {Promise} - Returns {@link Coin}[]. + */ + + async getAccountCoins(acct) { + const credits = await this.getAccountCredits(acct); + const coins = []; + + for (const credit of credits) { + if (credit.spent) + continue; + + coins.push(credit.coin); + } + + return coins; + } + + /** + * Get historical coins for a transaction. + * @param {TX} tx + * @returns {Promise} - Returns {@link TX}. + */ + + async getSpentCoins(tx) { + if (tx.isCoinbase()) + return []; + + const credits = await this.getSpentCredits(tx); + const coins = []; + + for (const credit of credits) { + if (!credit) { + coins.push(null); + continue; + } + + coins.push(credit.coin); + } + + return coins; + } + + /** + * Get a coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getCoinView(tx) { + const view = new CoinView(); + + if (tx.isCoinbase()) + return view; + + for (const {prevout} of tx.inputs) { + const {hash, index} = prevout; + const coin = await this.getCoin(hash, index); + + if (!coin) + continue; + + view.addCoin(coin); + } + + return view; + } + + /** + * Get historical coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getSpentView(tx) { + const view = new CoinView(); + + if (tx.isCoinbase()) + return view; + + const coins = await this.getSpentCoins(tx); + + for (const coin of coins) { + if (!coin) + continue; + + view.addCoin(coin); + } + + return view; + } + + /** + * Get transaction. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + async getTX(hash) { + const raw = await this.get(layout.t(hash)); + + if (!raw) + return null; + + return TXRecord.fromRaw(raw); + } + + /** + * Get transaction details. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TXDetails}. + */ + + async getDetails(hash) { + const wtx = await this.getTX(hash); + + if (!wtx) + return null; + + return this.toDetails(wtx); + } + + /** + * Convert transaction to transaction details. + * @param {TXRecord[]} wtxs + * @returns {Promise} + */ + + async toDetails(wtxs) { + const out = []; + + if (!Array.isArray(wtxs)) + return this._toDetails(wtxs); + + for (const wtx of wtxs) { + const details = await this._toDetails(wtx); + + if (!details) + continue; + + out.push(details); + } + + return out; + } + + /** + * Convert transaction to transaction details. + * @private + * @param {TXRecord} wtx + * @returns {Promise} + */ + + async _toDetails(wtx) { + const tx = wtx.tx; + const block = wtx.getBlock(); + const details = new Details(wtx, block); + const coins = await this.getSpentCoins(tx); + + for (let i = 0; i < tx.inputs.length; i++) { + const coin = coins[i]; + let path = null; + + if (coin) + path = await this.getPath(coin); + + details.setInput(i, path, coin); + } + + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const path = await this.getPath(output); + details.setOutput(i, path); + } + + return details; + } + + /** + * Test whether the database has a transaction. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + hasTX(hash) { + return this.has(layout.t(hash)); + } + + /** + * Get coin. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + async getCoin(hash, index) { + const credit = await this.getCredit(hash, index); + + if (!credit) + return null; + + return credit.coin; + } + + /** + * Get coin. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + async getCredit(hash, index) { + const data = await this.get(layout.c(hash, index)); + + if (!data) + return null; + + const credit = Credit.fromRaw(data); + credit.coin.hash = hash; + credit.coin.index = index; + + return credit; + } + + /** + * Get spender coin. + * @param {Outpoint} spent + * @param {Outpoint} prevout + * @returns {Promise} - Returns {@link Coin}. + */ + + async getSpentCoin(spent, prevout) { + const data = await this.get(layout.d(spent.hash, spent.index)); + + if (!data) + return null; + + const coin = Coin.fromRaw(data); + coin.hash = prevout.hash; + coin.index = prevout.index; + + return coin; + } + + /** + * Test whether the database has a spent coin. + * @param {Outpoint} spent + * @returns {Promise} - Returns {@link Coin}. + */ + + hasSpentCoin(spent) { + return this.has(layout.d(spent.hash, spent.index)); + } + + /** + * Update spent coin height in storage. + * @param {TX} tx - Sending transaction. + * @param {Number} index + * @param {Number} height + * @returns {Promise} + */ + + async updateSpentCoin(b, tx, index, height) { + const prevout = Outpoint.fromTX(tx, index); + const spent = await this.getSpent(prevout.hash, prevout.index); + + if (!spent) + return; + + const coin = await this.getSpentCoin(spent, prevout); + + if (!coin) + return; + + coin.height = height; + + b.put(layout.d(spent.hash, spent.index), coin.toRaw()); + } + + /** + * Test whether the database has a transaction. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async hasCoin(hash, index) { + return this.has(layout.c(hash, index)); + } + + /** + * Calculate balance. + * @param {Number?} account + * @returns {Promise} - Returns {@link Balance}. + */ + + async getBalance(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountBalance(acct); + + return this.getWalletBalance(); + } + + /** + * Calculate balance. + * @returns {Promise} - Returns {@link Balance}. + */ + + async getWalletBalance() { + const data = await this.get(layout.R); + + if (!data) + return new Balance(); + + return Balance.fromRaw(-1, data); + } + + /** + * Calculate balance by account. + * @param {Number} acct + * @returns {Promise} - Returns {@link Balance}. + */ + + async getAccountBalance(acct) { + const data = await this.get(layout.r(acct)); + + if (!data) + return new Balance(acct); + + return Balance.fromRaw(acct, data); + } + + /** + * Zap pending transactions older than `age`. + * @param {Number} acct + * @param {Number} age - Age delta (delete transactions older than `now - age`). + * @returns {Promise} + */ + + async zap(acct, age) { + assert((age >>> 0) === age); + + const now = util.now(); + + const txs = await this.getRange(acct, { + start: 0, + end: now - age + }); + + const hashes = []; + + for (const wtx of txs) { + if (wtx.height !== -1) + continue; + + assert(now - wtx.mtime >= age); + + this.logger.debug('Zapping TX: %s (%d)', + wtx.tx.txid(), this.wid); + + await this.remove(wtx.hash); + + hashes.push(wtx.hash); + } + + return hashes; + } + + /** + * Abandon transaction. + * @param {Hash} hash + * @returns {Promise} + */ + + async abandon(hash) { + const result = await this.has(layout.p(hash)); + + if (!result) + throw new Error('TX not eligible.'); + + return this.remove(hash); + } } /** @@ -48,2688 +2109,650 @@ function TXDB(wdb, wid) { TXDB.layout = layout; -/** - * Open TXDB. - * @returns {Promise} - */ - -TXDB.prototype.open = async function open(wallet) { - this.wid = wallet.wid; - this.prefix = layout.prefix(this.wid); - this.wallet = wallet; -}; - -/** - * Emit transaction event. - * @private - * @param {String} event - * @param {Object} data - * @param {Details} details - */ - -TXDB.prototype.emit = function emit(event, data, details) { - this.wdb.emit(event, this.wallet, data, details); - this.wallet.emit(event, data, details); -}; - -/** - * Bucket - * @returns {Bucket} - */ - -TXDB.prototype.bucket = function bucket() { - return this.db.bucket(this.prefix); -}; - -/** - * Get. - * @param {String} key - */ - -TXDB.prototype.get = function get(key) { - return this.bucket().get(key); -}; - -/** - * Has. - * @param {String} key - */ - -TXDB.prototype.has = function has(key) { - return this.bucket().has(key); -}; - -/** - * Iterate. - * @param {Object} options - * @returns {Promise} - */ - -TXDB.prototype.range = function range(options) { - return this.bucket().range(options); -}; - -/** - * Iterate. - * @param {Object} options - * @returns {Promise} - */ - -TXDB.prototype.keys = function keys(options) { - return this.bucket().keys(options); -}; - -/** - * Iterate. - * @param {Object} options - * @returns {Promise} - */ - -TXDB.prototype.values = function values(options) { - return this.bucket().values(options); -}; - -/** - * Get wallet path for output. - * @param {Output} output - * @returns {Promise} - Returns {@link Path}. - */ - -TXDB.prototype.getPath = function getPath(output) { - const hash = output.getHash('hex'); - - if (!hash) - return null; - - return this.wdb.getPath(this.wid, hash); -}; - -/** - * Test whether path exists for output. - * @param {Output} output - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.hasPath = function hasPath(output) { - const hash = output.getHash('hex'); - - if (!hash) - return false; - - return this.wdb.hasPath(this.wid, hash); -}; - -/** - * Save credit. - * @param {Credit} credit - * @param {Path} path - */ - -TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) { - const {coin} = credit; - - b.put(layout.c(coin.hash, coin.index), credit.toRaw()); - b.put(layout.C(path.account, coin.hash, coin.index), null); - - return this.addOutpointMap(b, coin.hash, coin.index); -}; - -/** - * Remove credit. - * @param {Credit} credit - * @param {Path} path - */ - -TXDB.prototype.removeCredit = async function removeCredit(b, credit, path) { - const {coin} = credit; - - b.del(layout.c(coin.hash, coin.index)); - b.del(layout.C(path.account, coin.hash, coin.index)); - - return this.removeOutpointMap(b, coin.hash, coin.index); -}; - -/** - * Spend credit. - * @param {Credit} credit - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.spendCredit = function spendCredit(b, credit, tx, index) { - const prevout = tx.inputs[index].prevout; - const spender = Outpoint.fromTX(tx, index); - b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); - b.put(layout.d(spender.hash, spender.index), credit.coin.toRaw()); -}; - -/** - * Unspend credit. - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.unspendCredit = function unspendCredit(b, tx, index) { - const prevout = tx.inputs[index].prevout; - const spender = Outpoint.fromTX(tx, index); - b.del(layout.s(prevout.hash, prevout.index)); - b.del(layout.d(spender.hash, spender.index)); -}; - -/** - * Write input record. - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.writeInput = async function writeInput(b, tx, index) { - const prevout = tx.inputs[index].prevout; - const spender = Outpoint.fromTX(tx, index); - b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); - return this.addOutpointMap(b, prevout.hash, prevout.index); -}; - -/** - * Remove input record. - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.removeInput = async function removeInput(b, tx, index) { - const prevout = tx.inputs[index].prevout; - b.del(layout.s(prevout.hash, prevout.index)); - return this.removeOutpointMap(b, prevout.hash, prevout.index); -}; - -/** - * Update wallet balance. - * @param {BalanceDelta} state - */ - -TXDB.prototype.updateBalance = async function updateBalance(b, state) { - const balance = await this.getWalletBalance(); - state.applyTo(balance); - b.put(layout.R, balance.toRaw()); - return balance; -}; - -/** - * Update account balance. - * @param {Number} acct - * @param {Balance} delta - */ - -TXDB.prototype.updateAccountBalance = async function updateAccountBalance(b, acct, delta) { - const balance = await this.getAccountBalance(acct); - delta.applyTo(balance); - b.put(layout.r(acct), balance.toRaw()); - return balance; -}; - -/** - * Test a whether a coin has been spent. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.getSpent = async function getSpent(hash, index) { - const data = await this.get(layout.s(hash, index)); - - if (!data) - return null; - - return Outpoint.fromRaw(data); -}; - -/** - * Test a whether a coin has been spent. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.isSpent = function isSpent(hash, index) { - return this.has(layout.s(hash, index)); -}; - -/** - * Append to global map. - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.addBlockMap = function addBlockMap(b, height) { - return this.wdb.addBlockMap(b.batch, height, this.wid); -}; - -/** - * Remove from global map. - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.removeBlockMap = function removeBlockMap(b, height) { - return this.wdb.removeBlockMap(b.batch, height, this.wid); -}; - -/** - * Append to global map. - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.addTXMap = function addTXMap(b, hash) { - return this.wdb.addTXMap(b.batch, hash, this.wid); -}; - -/** - * Remove from global map. - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.removeTXMap = function removeTXMap(b, hash) { - return this.wdb.removeTXMap(b.batch, hash, this.wid); -}; - -/** - * Append to global map. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - */ - -TXDB.prototype.addOutpointMap = function addOutpointMap(b, hash, index) { - return this.wdb.addOutpointMap(b.batch, hash, index, this.wid); -}; - -/** - * Remove from global map. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - */ - -TXDB.prototype.removeOutpointMap = function removeOutpointMap(b, hash, index) { - return this.wdb.removeOutpointMap(b.batch, hash, index, this.wid); -}; - -/** - * List block records. - * @returns {Promise} - */ - -TXDB.prototype.getBlocks = function getBlocks() { - return this.keys({ - gte: layout.b(0), - lte: layout.b(0xffffffff), - parse: key => layout.bb(key) - }); -}; - -/** - * Get block record. - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.getBlock = async function getBlock(height) { - const data = await this.get(layout.b(height)); - - if (!data) - return null; - - return BlockRecord.fromRaw(data); -}; - -/** - * Append to the global block record. - * @param {Hash} hash - * @param {BlockMeta} block - * @returns {Promise} - */ - -TXDB.prototype.addBlock = async function addBlock(b, hash, block) { - const key = layout.b(block.height); - const data = await this.get(key); - - if (!data) { - const blk = BlockRecord.fromMeta(block); - blk.add(hash); - b.put(key, blk.toRaw()); - return; - } - - const raw = Buffer.allocUnsafe(data.length + 32); - data.copy(raw, 0); - - const size = raw.readUInt32LE(40, true); - raw.writeUInt32LE(size + 1, 40, true); - hash.copy(raw, data.length); - - b.put(key, raw); -}; - -/** - * Remove from the global block record. - * @param {Hash} hash - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.removeBlock = async function removeBlock(b, hash, height) { - const key = layout.b(height); - const data = await this.get(key); - - if (!data) - return; - - const size = data.readUInt32LE(40, true); - - assert(size > 0); - assert(data.slice(-32).equals(hash)); - - if (size === 1) { - b.del(key); - return; - } - - const raw = data.slice(0, -32); - raw.writeUInt32LE(size - 1, 40, true); - - b.put(key, raw); -}; - -/** - * Remove from the global block record. - * @param {Hash} hash - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.spliceBlock = async function spliceBlock(b, hash, height) { - const block = await this.getBlock(height); - - if (!block) - return; - - if (!block.remove(hash)) - return; - - if (block.hashes.size === 0) { - b.del(layout.b(height)); - return; - } - - b.put(layout.b(height), block.toRaw()); -}; - -/** - * Add transaction without a batch. - * @private - * @param {TX} tx - * @returns {Promise} - */ - -TXDB.prototype.add = async function add(tx, block) { - const hash = tx.hash('hex'); - const existing = await this.getTX(hash); - - assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); - - if (existing) { - // Existing tx is already confirmed. Ignore. - if (existing.height !== -1) - return null; - - // The incoming tx won't confirm the - // existing one anyway. Ignore. - if (!block) - return null; - - // Confirm transaction. - return this.confirm(existing, block); - } - - const wtx = TXRecord.fromTX(tx, block); - - if (!block) { - // Potentially remove double-spenders. - // Only remove if they're not confirmed. - if (!await this.removeConflicts(tx, true)) - return null; - } else { - // Potentially remove double-spenders. - await this.removeConflicts(tx, false); - } - - // Finally we can do a regular insertion. - return this.insert(wtx, block); -}; - -/** - * Insert transaction. - * @private - * @param {TXRecord} wtx - * @param {BlockMeta} block - * @returns {Promise} - */ - -TXDB.prototype.insert = async function insert(wtx, block) { - const b = this.bucket(); - const {tx, hash} = wtx; - const height = block ? block.height : -1; - const details = new Details(wtx, block); - const state = new BalanceDelta(); - - let own = false; - - if (!tx.isCoinbase()) { - // We need to potentially spend some coins here. - for (let i = 0; i < tx.inputs.length; i++) { - const input = tx.inputs[i]; - const {hash, index} = input.prevout; - const credit = await this.getCredit(hash, index); - - if (!credit) { - // Watch all inputs for incoming txs. - // This allows us to check for double spends. - if (!block) - await this.writeInput(b, tx, i); - continue; - } - - const coin = credit.coin; - const path = await this.getPath(coin); - assert(path); - - // Build the tx details object - // as we go, for speed. - details.setInput(i, path, coin); - - // Write an undo coin for the credit - // and add it to the stxo set. - this.spendCredit(b, credit, tx, i); - - // Unconfirmed balance should always - // be updated as it reflects the on-chain - // balance _and_ mempool balance assuming - // everything in the mempool were to confirm. - state.tx(path, 1); - state.coin(path, -1); - state.unconfirmed(path, -coin.value); - - if (!block) { - // If the tx is not mined, we do not - // disconnect the coin, we simply mark - // a `spent` flag on the credit. This - // effectively prevents the mempool - // from altering our utxo state - // permanently. It also makes it - // possible to compare the on-chain - // state vs. the mempool state. - credit.spent = true; - await this.saveCredit(b, credit, path); - } else { - // If the tx is mined, we can safely - // remove the coin being spent. This - // coin will be indexed as an undo - // coin so it can be reconnected - // later during a reorg. - state.confirmed(path, -coin.value); - await this.removeCredit(b, credit, path); - } - - own = true; - } - } - - // Potentially add coins to the utxo set. - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const path = await this.getPath(output); - - if (!path) - continue; - - details.setOutput(i, path); - - const credit = Credit.fromTX(tx, i, height); - credit.own = own; - - state.tx(path, 1); - state.coin(path, 1); - state.unconfirmed(path, output.value); - - if (block) - state.confirmed(path, output.value); - - await this.saveCredit(b, credit, path); - } - - // If this didn't update any coins, - // it's not our transaction. - if (!state.updated()) - return null; - - // Save and index the transaction record. - b.put(layout.t(hash), wtx.toRaw()); - b.put(layout.m(wtx.mtime, hash), null); - - if (!block) - b.put(layout.p(hash), null); - else - b.put(layout.h(height, hash), null); - - // Do some secondary indexing for account-based - // queries. This saves us a lot of time for - // queries later. - for (const [acct, delta] of state.accounts) { - await this.updateAccountBalance(b, acct, delta); - - b.put(layout.T(acct, hash), null); - b.put(layout.M(acct, wtx.mtime, hash), null); - - if (!block) - b.put(layout.P(acct, hash), null); - else - b.put(layout.H(acct, height, hash), null); - } - - // Update block records. - if (block) { - await this.addBlockMap(b, height); - await this.addBlock(b, tx.hash(), block); - } else { - await this.addTXMap(b, hash); - } - - // Commit the new state. - const balance = await this.updateBalance(b, state); - - await b.write(); - - // This transaction may unlock some - // coins now that we've seen it. - this.unlockTX(tx); - - // Emit events for potential local and - // websocket listeners. Note that these - // will only be emitted if the batch is - // successfully written to disk. - this.emit('tx', tx, details); - this.emit('balance', balance); - - return details; -}; - -/** - * Attempt to confirm a transaction. - * @private - * @param {TXRecord} wtx - * @param {BlockMeta} block - * @returns {Promise} - */ - -TXDB.prototype.confirm = async function confirm(wtx, block) { - const b = this.bucket(); - const {tx, hash} = wtx; - const height = block.height; - const details = new Details(wtx, block); - const state = new BalanceDelta(); - - wtx.setBlock(block); - - if (!tx.isCoinbase()) { - const credits = await this.getSpentCredits(tx); - - // Potentially spend coins. Now that the tx - // is mined, we can actually _remove_ coins - // from the utxo state. - for (let i = 0; i < tx.inputs.length; i++) { - const input = tx.inputs[i]; - const {hash, index} = input.prevout; - - let resolved = false; - - // There may be new credits available - // that we haven't seen yet. - if (!credits[i]) { - await this.removeInput(b, tx, i); - - const credit = await this.getCredit(hash, index); - - if (!credit) - continue; - - // Add a spend record and undo coin - // for the coin we now know is ours. - // We don't need to remove the coin - // since it was never added in the - // first place. - this.spendCredit(b, credit, tx, i); - - credits[i] = credit; - resolved = true; - } - - const credit = credits[i]; - const coin = credit.coin; - - assert(coin.height !== -1); - - const path = await this.getPath(coin); - assert(path); - - details.setInput(i, path, coin); - - if (resolved) { - state.coin(path, -1); - state.unconfirmed(path, -coin.value); - } - - // We can now safely remove the credit - // entirely, now that we know it's also - // been removed on-chain. - state.confirmed(path, -coin.value); - - await this.removeCredit(b, credit, path); - } - } - - // Update credit heights, including undo coins. - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const path = await this.getPath(output); - - if (!path) - continue; - - details.setOutput(i, path); - - const credit = await this.getCredit(hash, i); - assert(credit); - - // Credits spent in the mempool add an - // undo coin for ease. If this credit is - // spent in the mempool, we need to - // update the undo coin's height. - if (credit.spent) - await this.updateSpentCoin(b, tx, i, height); - - // Update coin height and confirmed - // balance. Save once again. - state.confirmed(path, output.value); - credit.coin.height = height; - - await this.saveCredit(b, credit, path); - } - - // Save the new serialized transaction as - // the block-related properties have been - // updated. Also reindex for height. - b.put(layout.t(hash), wtx.toRaw()); - b.del(layout.p(hash)); - b.put(layout.h(height, hash), null); - - // Secondary indexing also needs to change. - for (const [acct, delta] of state.accounts) { - await this.updateAccountBalance(b, acct, delta); - b.del(layout.P(acct, hash)); - b.put(layout.H(acct, height, hash), null); - } - - await this.removeTXMap(b, hash); - await this.addBlockMap(b, height); - await this.addBlock(b, tx.hash(), block); - - // Commit the new state. The balance has updated. - const balance = await this.updateBalance(b, state); - - await b.write(); - - this.unlockTX(tx); - - this.emit('confirmed', tx, details); - this.emit('balance', balance); - - return details; -}; - -/** - * Recursively remove a transaction - * from the database. - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.remove = async function remove(hash) { - const wtx = await this.getTX(hash); - - if (!wtx) - return null; - - return this.removeRecursive(wtx); -}; - -/** - * Remove a transaction from the - * database. Disconnect inputs. - * @private - * @param {TXRecord} wtx - * @returns {Promise} - */ - -TXDB.prototype.erase = async function erase(wtx, block) { - const b = this.bucket(); - const {tx, hash} = wtx; - const height = block ? block.height : -1; - const details = new Details(wtx, block); - const state = new BalanceDelta(); - - if (!tx.isCoinbase()) { - // We need to undo every part of the - // state this transaction ever touched. - // Start by getting the undo coins. - const credits = await this.getSpentCredits(tx); - - for (let i = 0; i < tx.inputs.length; i++) { - const credit = credits[i]; - - if (!credit) { - if (!block) - await this.removeInput(b, tx, i); - continue; - } - - const coin = credit.coin; - const path = await this.getPath(coin); - assert(path); - - details.setInput(i, path, coin); - - // Recalculate the balance, remove - // from stxo set, remove the undo - // coin, and resave the credit. - state.tx(path, -1); - state.coin(path, 1); - state.unconfirmed(path, coin.value); - - if (block) - state.confirmed(path, coin.value); - - this.unspendCredit(b, tx, i); - - credit.spent = false; - await this.saveCredit(b, credit, path); - } - } - - // We need to remove all credits - // this transaction created. - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const path = await this.getPath(output); - - if (!path) - continue; - - details.setOutput(i, path); - - const credit = Credit.fromTX(tx, i, height); - - state.tx(path, -1); - state.coin(path, -1); - state.unconfirmed(path, -output.value); - - if (block) - state.confirmed(path, -output.value); - - await this.removeCredit(b, credit, path); - } - - // Remove the transaction data - // itself as well as unindex. - b.del(layout.t(hash)); - b.del(layout.m(wtx.mtime, hash)); - - if (!block) - b.del(layout.p(hash)); - else - b.del(layout.h(height, hash)); - - // Remove all secondary indexing. - for (const [acct, delta] of state.accounts) { - await this.updateAccountBalance(b, acct, delta); - - b.del(layout.T(acct, hash)); - b.del(layout.M(acct, wtx.mtime, hash)); - - if (!block) - b.del(layout.P(acct, hash)); - else - b.del(layout.H(acct, height, hash)); - } - - // Update block records. - if (block) { - await this.removeBlockMap(b, height); - await this.spliceBlock(b, hash, height); - } else { - await this.removeTXMap(b, hash); - } - - // Update the transaction counter - // and commit new state due to - // balance change. - const balance = await this.updateBalance(b, state); - - await b.write(); - - this.emit('remove tx', tx, details); - this.emit('balance', balance); - - return details; -}; - -/** - * Remove a transaction and recursively - * remove all of its spenders. - * @private - * @param {TXRecord} wtx - * @returns {Promise} - */ - -TXDB.prototype.removeRecursive = async function removeRecursive(wtx) { - const {tx, hash} = wtx; - - for (let i = 0; i < tx.outputs.length; i++) { - const spent = await this.getSpent(hash, i); - - if (!spent) - continue; - - // Remove all of the spender's spenders first. - const stx = await this.getTX(spent.hash); - - assert(stx); - - await this.removeRecursive(stx); - } - - // Remove the spender. - return this.erase(wtx, wtx.getBlock()); -}; - -/** - * Revert a block. - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.revert = async function revert(height) { - const block = await this.getBlock(height); - - if (!block) - return 0; - - const hashes = block.toArray(); - - for (let i = hashes.length - 1; i >= 0; i--) { - const hash = hashes[i]; - await this.unconfirm(hash); - } - - return hashes.length; -}; - -/** - * Unconfirm a transaction without a batch. - * @private - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.unconfirm = async function unconfirm(hash) { - const wtx = await this.getTX(hash); - - if (!wtx) - return null; - - if (wtx.height === -1) - return null; - - return this.disconnect(wtx, wtx.getBlock()); -}; - -/** - * Unconfirm a transaction. Necessary after a reorg. - * @param {TXRecord} wtx - * @returns {Promise} - */ - -TXDB.prototype.disconnect = async function disconnect(wtx, block) { - const b = this.bucket(); - const {tx, hash, height} = wtx; - const details = new Details(wtx, block); - const state = new BalanceDelta(); - - assert(block); - - wtx.unsetBlock(); - - if (!tx.isCoinbase()) { - // We need to reconnect the coins. Start - // by getting all of the undo coins we know - // about. - const credits = await this.getSpentCredits(tx); - - for (let i = 0; i < tx.inputs.length; i++) { - const credit = credits[i]; - - if (!credit) { - await this.writeInput(b, tx, i); - continue; - } - - const coin = credit.coin; - - assert(coin.height !== -1); - - const path = await this.getPath(coin); - assert(path); - - details.setInput(i, path, coin); - - state.confirmed(path, coin.value); - - // Resave the credit and mark it - // as spent in the mempool instead. - credit.spent = true; - await this.saveCredit(b, credit, path); - } - } - - // We need to remove heights on - // the credits and undo coins. - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const path = await this.getPath(output); - - if (!path) - continue; - - const credit = await this.getCredit(hash, i); - - // Potentially update undo coin height. - if (!credit) { - await this.updateSpentCoin(b, tx, i, height); - continue; - } - - if (credit.spent) - await this.updateSpentCoin(b, tx, i, height); - - details.setOutput(i, path); - - // Update coin height and confirmed - // balance. Save once again. - credit.coin.height = -1; - - state.confirmed(path, -output.value); - - await this.saveCredit(b, credit, path); - } - - await this.addTXMap(b, hash); - await this.removeBlockMap(b, height); - await this.removeBlock(b, tx.hash(), height); - - // We need to update the now-removed - // block properties and reindex due - // to the height change. - b.put(layout.t(hash), wtx.toRaw()); - b.put(layout.p(hash), null); - b.del(layout.h(height, hash)); - - // Secondary indexing also needs to change. - for (const [acct, delta] of state.accounts) { - await this.updateAccountBalance(b, acct, delta); - b.put(layout.P(acct, hash), null); - b.del(layout.H(acct, height, hash)); - } - - // Commit state due to unconfirmed - // vs. confirmed balance change. - const balance = await this.updateBalance(b, state); - - await b.write(); - - this.emit('unconfirmed', tx, details); - this.emit('balance', balance); - - return details; -}; - -/** - * Remove spenders that have not been confirmed. We do this in the - * odd case of stuck transactions or when a coin is double-spent - * by a newer transaction. All previously-spending transactions - * of that coin that are _not_ confirmed will be removed from - * the database. - * @private - * @param {Hash} hash - * @param {TX} ref - Reference tx, the tx that double-spent. - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.removeConflict = async function removeConflict(wtx) { - const tx = wtx.tx; - - this.logger.warning('Handling conflicting tx: %s.', tx.txid()); - - const details = await this.removeRecursive(wtx); - - this.logger.warning('Removed conflict: %s.', tx.txid()); - - // Emit the _removed_ transaction. - this.emit('conflict', tx, details); - - return details; -}; - -/** - * Retrieve coins for own inputs, remove - * double spenders, and verify inputs. - * @private - * @param {TX} tx - * @returns {Promise} - */ - -TXDB.prototype.removeConflicts = async function removeConflicts(tx, conf) { - if (tx.isCoinbase()) - return true; - - const txid = tx.hash('hex'); - const spends = []; - - // Gather all spent records first. - for (const {prevout} of tx.inputs) { - const {hash, index} = prevout; - - // Is it already spent? - const spent = await this.getSpent(hash, index); - - if (!spent) - continue; - - // Did _we_ spend it? - if (spent.hash === txid) - continue; - - const spender = await this.getTX(spent.hash); - assert(spender); - - if (conf && spender.height !== -1) - return false; - - spends.push(spender); - } - - // Once we know we're not going to - // screw things up, remove the double - // spenders. - for (const spender of spends) { - // Remove the double spender. - await this.removeConflict(spender); - } - - return true; -}; - -/** - * Lock all coins in a transaction. - * @param {TX} tx - */ - -TXDB.prototype.lockTX = function lockTX(tx) { - if (tx.isCoinbase()) - return; - - for (const input of tx.inputs) - this.lockCoin(input.prevout); -}; - -/** - * Unlock all coins in a transaction. - * @param {TX} tx - */ - -TXDB.prototype.unlockTX = function unlockTX(tx) { - if (tx.isCoinbase()) - return; - - for (const input of tx.inputs) - this.unlockCoin(input.prevout); -}; - -/** - * Lock a single coin. - * @param {Coin|Outpoint} coin - */ - -TXDB.prototype.lockCoin = function lockCoin(coin) { - const key = coin.toKey(); - this.locked.add(key); -}; - -/** - * Unlock a single coin. - * @param {Coin|Outpoint} coin - */ - -TXDB.prototype.unlockCoin = function unlockCoin(coin) { - const key = coin.toKey(); - return this.locked.delete(key); -}; - -/** - * Test locked status of a single coin. - * @param {Coin|Outpoint} coin - */ - -TXDB.prototype.isLocked = function isLocked(coin) { - const key = coin.toKey(); - return this.locked.has(key); -}; - -/** - * Filter array of coins or outpoints - * for only unlocked ones. - * @param {Coin[]|Outpoint[]} - * @returns {Array} - */ - -TXDB.prototype.filterLocked = function filterLocked(coins) { - const out = []; - - for (const coin of coins) { - if (!this.isLocked(coin)) - out.push(coin); - } - - return out; -}; - -/** - * Return an array of all locked outpoints. - * @returns {Outpoint[]} - */ - -TXDB.prototype.getLocked = function getLocked() { - const outpoints = []; - - for (const key of this.locked.keys()) - outpoints.push(Outpoint.fromKey(key)); - - return outpoints; -}; - -/** - * Get hashes of all transactions in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(acct) { - assert(typeof acct === 'number'); - return this.keys({ - gte: layout.T(acct, encoding.NULL_HASH), - lte: layout.T(acct, encoding.HIGH_HASH), - parse: (key) => { - const [, hash] = layout.Tt(key); - return hash; - } - }); -}; - -/** - * Get hashes of all transactions in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getHistoryHashes = function getHistoryHashes(acct) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountHistoryHashes(acct); - - return this.keys({ - gte: layout.t(encoding.NULL_HASH), - lte: layout.t(encoding.HIGH_HASH), - parse: key => layout.tt(key) - }); -}; - -/** - * Get hashes of all unconfirmed transactions in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(acct) { - assert(typeof acct === 'number'); - return this.keys({ - gte: layout.P(acct, encoding.NULL_HASH), - lte: layout.P(acct, encoding.HIGH_HASH), - parse: (key) => { - const [, hash] = layout.Pp(key); - return hash; - } - }); -}; - -/** - * Get hashes of all unconfirmed transactions in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getPendingHashes = function getPendingHashes(acct) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountPendingHashes(acct); - - return this.keys({ - gte: layout.p(encoding.NULL_HASH), - lte: layout.p(encoding.HIGH_HASH), - parse: key => layout.pp(key) - }); -}; - -/** - * Get all coin hashes in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(acct) { - assert(typeof acct === 'number'); - return this.keys({ - gte: layout.C(acct, encoding.NULL_HASH, 0), - lte: layout.C(acct, encoding.HIGH_HASH, 0xffffffff), - parse: (key) => { - const [, hash, index] = layout.Cc(key); - return new Outpoint(hash, index); - } - }); -}; - -/** - * Get all coin hashes in the database. - * @param {Number} acct - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getOutpoints = function getOutpoints(acct) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountOutpoints(acct); - - return this.keys({ - gte: layout.c(encoding.NULL_HASH, 0), - lte: layout.c(encoding.HIGH_HASH, 0xffffffff), - parse: (key) => { - const [hash, index] = layout.cc(key); - return new Outpoint(hash, index); - } - }); -}; - -/** - * Get TX hashes by height range. - * @param {Number} acct - * @param {Object} options - * @param {Number} options.start - Start height. - * @param {Number} options.end - End height. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(acct, options) { - assert(typeof acct === 'number'); - - const start = options.start || 0; - const end = options.end || 0xffffffff; - - return this.keys({ - gte: layout.H(acct, start, encoding.NULL_HASH), - lte: layout.H(acct, end, encoding.HIGH_HASH), - limit: options.limit, - reverse: options.reverse, - parse: (key) => { - const [,, hash] = layout.Hh(key); - return hash; - } - }); -}; - -/** - * Get TX hashes by height range. - * @param {Number} acct - * @param {Object} options - * @param {Number} options.start - Start height. - * @param {Number} options.end - End height. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(acct, options) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountHeightRangeHashes(acct, options); - - const start = options.start || 0; - const end = options.end || 0xffffffff; - - return this.keys({ - gte: layout.h(start, encoding.NULL_HASH), - lte: layout.h(end, encoding.HIGH_HASH), - limit: options.limit, - reverse: options.reverse, - parse: (key) => { - const [, hash] = layout.hh(key); - return hash; - } - }); -}; - -/** - * Get TX hashes by height. - * @param {Number} height - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getHeightHashes = function getHeightHashes(height) { - return this.getHeightRangeHashes({ start: height, end: height }); -}; - -/** - * Get TX hashes by timestamp range. - * @param {Number} acct - * @param {Object} options - * @param {Number} options.start - Start height. - * @param {Number} options.end - End height. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(acct, options) { - assert(typeof acct === 'number'); - - const start = options.start || 0; - const end = options.end || 0xffffffff; - - return this.keys({ - gte: layout.M(acct, start, encoding.NULL_HASH), - lte: layout.M(acct, end, encoding.HIGH_HASH), - limit: options.limit, - reverse: options.reverse, - parse: (key) => { - const [,, hash] = layout.Mm(key); - return hash; - } - }); -}; - -/** - * Get TX hashes by timestamp range. - * @param {Number} acct - * @param {Object} options - * @param {Number} options.start - Start height. - * @param {Number} options.end - End height. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -TXDB.prototype.getRangeHashes = function getRangeHashes(acct, options) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountRangeHashes(acct, options); - - const start = options.start || 0; - const end = options.end || 0xffffffff; - - return this.keys({ - gte: layout.m(start, encoding.NULL_HASH), - lte: layout.m(end, encoding.HIGH_HASH), - limit: options.limit, - reverse: options.reverse, - parse: (key) => { - const [, hash] = layout.mm(key); - return hash; - } - }); -}; - -/** - * Get transactions by timestamp range. - * @param {Number} acct - * @param {Object} options - * @param {Number} options.start - Start time. - * @param {Number} options.end - End time. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - Returns {@link TX}[]. - */ - -TXDB.prototype.getRange = async function getRange(acct, options) { - const hashes = await this.getRangeHashes(acct, options); - const txs = []; - - for (const hash of hashes) { - const tx = await this.getTX(hash); - assert(tx); - txs.push(tx); - } - - return txs; -}; - -/** - * Get last N transactions. - * @param {Number} acct - * @param {Number} limit - Max number of transactions. - * @returns {Promise} - Returns {@link TX}[]. - */ - -TXDB.prototype.getLast = function getLast(acct, limit) { - return this.getRange(acct, { - start: 0, - end: 0xffffffff, - reverse: true, - limit: limit || 10 - }); -}; - -/** - * Get all transactions. - * @param {Number} acct - * @returns {Promise} - Returns {@link TX}[]. - */ - -TXDB.prototype.getHistory = function getHistory(acct) { - assert(typeof acct === 'number'); - - // Slow case - if (acct !== -1) - return this.getAccountHistory(acct); - - // Fast case - return this.values({ - gte: layout.t(encoding.NULL_HASH), - lte: layout.t(encoding.HIGH_HASH), - parse: TXRecord.fromRaw - }); -}; - -/** - * Get all acct transactions. - * @param {Number} acct - * @returns {Promise} - Returns {@link TX}[]. - */ - -TXDB.prototype.getAccountHistory = async function getAccountHistory(acct) { - const hashes = await this.getHistoryHashes(acct); - const txs = []; - - for (const hash of hashes) { - const tx = await this.getTX(hash); - assert(tx); - txs.push(tx); - } - - return txs; -}; - -/** - * Get unconfirmed transactions. - * @param {Number} acct - * @returns {Promise} - Returns {@link TX}[]. - */ - -TXDB.prototype.getPending = async function getPending(acct) { - const hashes = await this.getPendingHashes(acct); - const txs = []; - - for (const hash of hashes) { - const tx = await this.getTX(hash); - assert(tx); - txs.push(tx); - } - - return txs; -}; - -/** - * Get coins. - * @param {Number} acct - * @returns {Promise} - Returns {@link Coin}[]. - */ - -TXDB.prototype.getCredits = function getCredits(acct) { - assert(typeof acct === 'number'); - - // Slow case - if (acct !== -1) - return this.getAccountCredits(acct); - - // Fast case - return this.range({ - gte: layout.c(encoding.NULL_HASH, 0x00000000), - lte: layout.c(encoding.HIGH_HASH, 0xffffffff), - parse: (key, value) => { - const [hash, index] = layout.cc(key); - const credit = Credit.fromRaw(value); - credit.coin.hash = hash; - credit.coin.index = index; - return credit; - } - }); -}; - -/** - * Get coins by account. - * @param {Number} acct - * @returns {Promise} - Returns {@link Coin}[]. - */ - -TXDB.prototype.getAccountCredits = async function getAccountCredits(acct) { - const outpoints = await this.getOutpoints(acct); - const credits = []; - - for (const {hash, index} of outpoints) { - const credit = await this.getCredit(hash, index); - assert(credit); - credits.push(credit); - } - - return credits; -}; - -/** - * Fill a transaction with coins (all historical coins). - * @param {TX} tx - * @returns {Promise} - Returns {@link TX}. - */ - -TXDB.prototype.getSpentCredits = async function getSpentCredits(tx) { - if (tx.isCoinbase()) - return []; - - const hash = tx.hash('hex'); - const credits = []; - - for (let i = 0; i < tx.inputs.length; i++) - credits.push(null); - - await this.range({ - gte: layout.d(hash, 0x00000000), - lte: layout.d(hash, 0xffffffff), - parse: (key, value) => { - const [, index] = layout.dd(key); - const coin = Coin.fromRaw(value); - const input = tx.inputs[index]; - assert(input); - coin.hash = input.prevout.hash; - coin.index = input.prevout.index; - credits[index] = new Credit(coin); - } - }); - - return credits; -}; - -/** - * Get coins. - * @param {Number} acct - * @returns {Promise} - Returns {@link Coin}[]. - */ - -TXDB.prototype.getCoins = async function getCoins(acct) { - const credits = await this.getCredits(acct); - const coins = []; - - for (const credit of credits) { - if (credit.spent) - continue; - - coins.push(credit.coin); - } - - return coins; -}; - -/** - * Get coins by account. - * @param {Number} acct - * @returns {Promise} - Returns {@link Coin}[]. - */ - -TXDB.prototype.getAccountCoins = async function getAccountCoins(acct) { - const credits = await this.getAccountCredits(acct); - const coins = []; - - for (const credit of credits) { - if (credit.spent) - continue; - - coins.push(credit.coin); - } - - return coins; -}; - -/** - * Get historical coins for a transaction. - * @param {TX} tx - * @returns {Promise} - Returns {@link TX}. - */ - -TXDB.prototype.getSpentCoins = async function getSpentCoins(tx) { - if (tx.isCoinbase()) - return []; - - const credits = await this.getSpentCredits(tx); - const coins = []; - - for (const credit of credits) { - if (!credit) { - coins.push(null); - continue; - } - - coins.push(credit.coin); - } - - return coins; -}; - -/** - * Get a coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -TXDB.prototype.getCoinView = async function getCoinView(tx) { - const view = new CoinView(); - - if (tx.isCoinbase()) - return view; - - for (const {prevout} of tx.inputs) { - const {hash, index} = prevout; - const coin = await this.getCoin(hash, index); - - if (!coin) - continue; - - view.addCoin(coin); - } - - return view; -}; - -/** - * Get historical coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -TXDB.prototype.getSpentView = async function getSpentView(tx) { - const view = new CoinView(); - - if (tx.isCoinbase()) - return view; - - const coins = await this.getSpentCoins(tx); - - for (const coin of coins) { - if (!coin) - continue; - - view.addCoin(coin); - } - - return view; -}; - -/** - * Get transaction. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ - -TXDB.prototype.getTX = async function getTX(hash) { - const raw = await this.get(layout.t(hash)); - - if (!raw) - return null; - - return TXRecord.fromRaw(raw); -}; - -/** - * Get transaction details. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TXDetails}. - */ - -TXDB.prototype.getDetails = async function getDetails(hash) { - const wtx = await this.getTX(hash); - - if (!wtx) - return null; - - return this.toDetails(wtx); -}; - -/** - * Convert transaction to transaction details. - * @param {TXRecord[]} wtxs - * @returns {Promise} - */ - -TXDB.prototype.toDetails = async function toDetails(wtxs) { - const out = []; - - if (!Array.isArray(wtxs)) - return this._toDetails(wtxs); - - for (const wtx of wtxs) { - const details = await this._toDetails(wtx); - - if (!details) - continue; - - out.push(details); - } - - return out; -}; - -/** - * Convert transaction to transaction details. - * @private - * @param {TXRecord} wtx - * @returns {Promise} - */ - -TXDB.prototype._toDetails = async function _toDetails(wtx) { - const tx = wtx.tx; - const block = wtx.getBlock(); - const details = new Details(wtx, block); - const coins = await this.getSpentCoins(tx); - - for (let i = 0; i < tx.inputs.length; i++) { - const coin = coins[i]; - let path = null; - - if (coin) - path = await this.getPath(coin); - - details.setInput(i, path, coin); - } - - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const path = await this.getPath(output); - details.setOutput(i, path); - } - - return details; -}; - -/** - * Test whether the database has a transaction. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.hasTX = function hasTX(hash) { - return this.has(layout.t(hash)); -}; - -/** - * Get coin. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -TXDB.prototype.getCoin = async function getCoin(hash, index) { - const credit = await this.getCredit(hash, index); - - if (!credit) - return null; - - return credit.coin; -}; - -/** - * Get coin. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -TXDB.prototype.getCredit = async function getCredit(hash, index) { - const data = await this.get(layout.c(hash, index)); - - if (!data) - return null; - - const credit = Credit.fromRaw(data); - credit.coin.hash = hash; - credit.coin.index = index; - - return credit; -}; - -/** - * Get spender coin. - * @param {Outpoint} spent - * @param {Outpoint} prevout - * @returns {Promise} - Returns {@link Coin}. - */ - -TXDB.prototype.getSpentCoin = async function getSpentCoin(spent, prevout) { - const data = await this.get(layout.d(spent.hash, spent.index)); - - if (!data) - return null; - - const coin = Coin.fromRaw(data); - coin.hash = prevout.hash; - coin.index = prevout.index; - - return coin; -}; - -/** - * Test whether the database has a spent coin. - * @param {Outpoint} spent - * @returns {Promise} - Returns {@link Coin}. - */ - -TXDB.prototype.hasSpentCoin = function hasSpentCoin(spent) { - return this.has(layout.d(spent.hash, spent.index)); -}; - -/** - * Update spent coin height in storage. - * @param {TX} tx - Sending transaction. - * @param {Number} index - * @param {Number} height - * @returns {Promise} - */ - -TXDB.prototype.updateSpentCoin = async function updateSpentCoin(b, tx, index, height) { - const prevout = Outpoint.fromTX(tx, index); - const spent = await this.getSpent(prevout.hash, prevout.index); - - if (!spent) - return; - - const coin = await this.getSpentCoin(spent, prevout); - - if (!coin) - return; - - coin.height = height; - - b.put(layout.d(spent.hash, spent.index), coin.toRaw()); -}; - -/** - * Test whether the database has a transaction. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.hasCoin = async function hasCoin(hash, index) { - return this.has(layout.c(hash, index)); -}; - -/** - * Calculate balance. - * @param {Number?} account - * @returns {Promise} - Returns {@link Balance}. - */ - -TXDB.prototype.getBalance = async function getBalance(acct) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountBalance(acct); - - return this.getWalletBalance(); -}; - -/** - * Calculate balance. - * @returns {Promise} - Returns {@link Balance}. - */ - -TXDB.prototype.getWalletBalance = async function getWalletBalance() { - const data = await this.get(layout.R); - - if (!data) - return new Balance(); - - return Balance.fromRaw(-1, data); -}; - -/** - * Calculate balance by account. - * @param {Number} acct - * @returns {Promise} - Returns {@link Balance}. - */ - -TXDB.prototype.getAccountBalance = async function getAccountBalance(acct) { - const data = await this.get(layout.r(acct)); - - if (!data) - return new Balance(acct); - - return Balance.fromRaw(acct, data); -}; - -/** - * Zap pending transactions older than `age`. - * @param {Number} acct - * @param {Number} age - Age delta (delete transactions older than `now - age`). - * @returns {Promise} - */ - -TXDB.prototype.zap = async function zap(acct, age) { - assert((age >>> 0) === age); - - const now = util.now(); - - const txs = await this.getRange(acct, { - start: 0, - end: now - age - }); - - const hashes = []; - - for (const wtx of txs) { - if (wtx.height !== -1) - continue; - - assert(now - wtx.mtime >= age); - - this.logger.debug('Zapping TX: %s (%d)', - wtx.tx.txid(), this.wid); - - await this.remove(wtx.hash); - - hashes.push(wtx.hash); - } - - return hashes; -}; - -/** - * Abandon transaction. - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.abandon = async function abandon(hash) { - const result = await this.has(layout.p(hash)); - - if (!result) - throw new Error('TX not eligible.'); - - return this.remove(hash); -}; - /** * Balance * @alias module:wallet.Balance - * @constructor - * @param {Number} account */ -function Balance(acct = -1) { - if (!(this instanceof Balance)) - return new Balance(acct); +class Balance { + /** + * Create a balance. + * @constructor + * @param {Number} account + */ - assert(typeof acct === 'number'); + constructor(acct = -1) { + assert(typeof acct === 'number'); - this.account = acct; - this.tx = 0; - this.coin = 0; - this.unconfirmed = 0; - this.confirmed = 0; + this.account = acct; + this.tx = 0; + this.coin = 0; + this.unconfirmed = 0; + this.confirmed = 0; + } + + /** + * Apply delta. + * @param {Balance} balance + */ + + applyTo(balance) { + balance.tx += this.tx; + balance.coin += this.coin; + balance.unconfirmed += this.unconfirmed; + balance.confirmed += this.confirmed; + + assert(balance.tx >= 0); + assert(balance.coin >= 0); + assert(balance.unconfirmed >= 0); + assert(balance.confirmed >= 0); + } + + /** + * Serialize balance. + * @returns {Buffer} + */ + + toRaw() { + const bw = bio.write(32); + + bw.writeU64(this.tx); + bw.writeU64(this.coin); + bw.writeU64(this.unconfirmed); + bw.writeU64(this.confirmed); + + return bw.render(); + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {TXDBState} + */ + + fromRaw(data) { + const br = bio.read(data); + this.tx = br.readU64(); + this.coin = br.readU64(); + this.unconfirmed = br.readU64(); + this.confirmed = br.readU64(); + return this; + } + + /** + * Instantiate balance from serialized data. + * @param {Number} acct + * @param {Buffer} data + * @returns {TXDBState} + */ + + static fromRaw(acct, data) { + return new this(acct).fromRaw(data); + } + + /** + * Convert balance to a more json-friendly object. + * @param {Boolean?} minimal + * @returns {Object} + */ + + toJSON(minimal) { + return { + account: !minimal ? this.account : undefined, + tx: this.tx, + coin: this.coin, + unconfirmed: this.unconfirmed, + confirmed: this.confirmed + }; + } + + /** + * Inspect balance. + * @param {String} + */ + + inspect() { + return ''; + } } -/** - * Apply delta. - * @param {Balance} balance - */ - -Balance.prototype.applyTo = function applyTo(balance) { - balance.tx += this.tx; - balance.coin += this.coin; - balance.unconfirmed += this.unconfirmed; - balance.confirmed += this.confirmed; - - assert(balance.tx >= 0); - assert(balance.coin >= 0); - assert(balance.unconfirmed >= 0); - assert(balance.confirmed >= 0); -}; - -/** - * Serialize balance. - * @returns {Buffer} - */ - -Balance.prototype.toRaw = function toRaw() { - const bw = bio.write(32); - - bw.writeU64(this.tx); - bw.writeU64(this.coin); - bw.writeU64(this.unconfirmed); - bw.writeU64(this.confirmed); - - return bw.render(); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {TXDBState} - */ - -Balance.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - this.tx = br.readU64(); - this.coin = br.readU64(); - this.unconfirmed = br.readU64(); - this.confirmed = br.readU64(); - return this; -}; - -/** - * Instantiate balance from serialized data. - * @param {Number} acct - * @param {Buffer} data - * @returns {TXDBState} - */ - -Balance.fromRaw = function fromRaw(acct, data) { - return new Balance(acct).fromRaw(data); -}; - -/** - * Convert balance to a more json-friendly object. - * @param {Boolean?} minimal - * @returns {Object} - */ - -Balance.prototype.toJSON = function toJSON(minimal) { - return { - account: !minimal ? this.account : undefined, - tx: this.tx, - coin: this.coin, - unconfirmed: this.unconfirmed, - confirmed: this.confirmed - }; -}; - -/** - * Inspect balance. - * @param {String} - */ - -Balance.prototype.inspect = function inspect() { - return ''; -}; - /** * Balance Delta - * @constructor * @ignore */ -function BalanceDelta() { - this.wallet = new Balance(); - this.accounts = new Map(); +class BalanceDelta { + /** + * Create a balance delta. + * @constructor + */ + + constructor() { + this.wallet = new Balance(); + this.accounts = new Map(); + } + + updated() { + return this.wallet.tx !== 0; + } + + applyTo(balance) { + this.wallet.applyTo(balance); + } + + get(path) { + if (!this.accounts.has(path.account)) + this.accounts.set(path.account, new Balance()); + + return this.accounts.get(path.account); + } + + tx(path, value) { + const account = this.get(path); + account.tx = value; + this.wallet.tx = value; + } + + coin(path, value) { + const account = this.get(path); + account.coin += value; + this.wallet.coin += value; + } + + unconfirmed(path, value) { + const account = this.get(path); + account.unconfirmed += value; + this.wallet.unconfirmed += value; + } + + confirmed(path, value) { + const account = this.get(path); + account.confirmed += value; + this.wallet.confirmed += value; + } } -BalanceDelta.prototype.updated = function updated() { - return this.wallet.tx !== 0; -}; - -BalanceDelta.prototype.applyTo = function applyTo(balance) { - this.wallet.applyTo(balance); -}; - -BalanceDelta.prototype.get = function get(path) { - if (!this.accounts.has(path.account)) - this.accounts.set(path.account, new Balance()); - - return this.accounts.get(path.account); -}; - -BalanceDelta.prototype.tx = function tx(path, value) { - const account = this.get(path); - account.tx = value; - this.wallet.tx = value; -}; - -BalanceDelta.prototype.coin = function coin(path, value) { - const account = this.get(path); - account.coin += value; - this.wallet.coin += value; -}; - -BalanceDelta.prototype.unconfirmed = function unconfirmed(path, value) { - const account = this.get(path); - account.unconfirmed += value; - this.wallet.unconfirmed += value; -}; - -BalanceDelta.prototype.confirmed = function confirmed(path, value) { - const account = this.get(path); - account.confirmed += value; - this.wallet.confirmed += value; -}; - /** * Credit (wrapped coin) * @alias module:wallet.Credit - * @constructor - * @param {Coin} coin - * @param {Boolean?} spent * @property {Coin} coin * @property {Boolean} spent */ -function Credit(coin, spent) { - if (!(this instanceof Credit)) - return new Credit(coin, spent); +class Credit { + /** + * Create a credit. + * @constructor + * @param {Coin} coin + * @param {Boolean?} spent + */ - this.coin = coin || new Coin(); - this.spent = spent || false; - this.own = false; + constructor(coin, spent) { + this.coin = coin || new Coin(); + this.spent = spent || false; + this.own = false; + } + + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + + fromRaw(data) { + const br = bio.read(data); + this.coin.fromReader(br); + this.spent = br.readU8() === 1; + this.own = br.readU8() === 1; + return this; + } + + /** + * Instantiate credit from serialized data. + * @param {Buffer} data + * @returns {Credit} + */ + + static fromRaw(data) { + return new this().fromRaw(data); + } + + /** + * Get serialization size. + * @returns {Number} + */ + + getSize() { + return this.coin.getSize() + 2; + } + + /** + * Serialize credit. + * @returns {Buffer} + */ + + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); + this.coin.toWriter(bw); + bw.writeU8(this.spent ? 1 : 0); + bw.writeU8(this.own ? 1 : 0); + return bw.render(); + } + + /** + * Inject properties from tx object. + * @private + * @param {TX} tx + * @param {Number} index + * @returns {Credit} + */ + + fromTX(tx, index, height) { + this.coin.fromTX(tx, index, height); + this.spent = false; + this.own = false; + return this; + } + + /** + * Instantiate credit from transaction. + * @param {TX} tx + * @param {Number} index + * @returns {Credit} + */ + + static fromTX(tx, index, height) { + return new this().fromTX(tx, index, height); + } } -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Credit.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - this.coin.fromReader(br); - this.spent = br.readU8() === 1; - this.own = br.readU8() === 1; - return this; -}; - -/** - * Instantiate credit from serialized data. - * @param {Buffer} data - * @returns {Credit} - */ - -Credit.fromRaw = function fromRaw(data) { - return new Credit().fromRaw(data); -}; - -/** - * Get serialization size. - * @returns {Number} - */ - -Credit.prototype.getSize = function getSize() { - return this.coin.getSize() + 2; -}; - -/** - * Serialize credit. - * @returns {Buffer} - */ - -Credit.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); - this.coin.toWriter(bw); - bw.writeU8(this.spent ? 1 : 0); - bw.writeU8(this.own ? 1 : 0); - return bw.render(); -}; - -/** - * Inject properties from tx object. - * @private - * @param {TX} tx - * @param {Number} index - * @returns {Credit} - */ - -Credit.prototype.fromTX = function fromTX(tx, index, height) { - this.coin.fromTX(tx, index, height); - this.spent = false; - this.own = false; - return this; -}; - -/** - * Instantiate credit from transaction. - * @param {TX} tx - * @param {Number} index - * @returns {Credit} - */ - -Credit.fromTX = function fromTX(tx, index, height) { - return new Credit().fromTX(tx, index, height); -}; - /** * Transaction Details * @alias module:wallet.Details - * @constructor - * @param {TXDB} txdb - * @param {TX} tx */ -function Details(wtx, block) { - if (!(this instanceof Details)) - return new Details(wtx, block); +class Details { + /** + * Create transaction details. + * @constructor + * @param {TXRecord} wtx + * @param {BlockMeta} block + */ - this.hash = wtx.hash; - this.tx = wtx.tx; - this.mtime = wtx.mtime; - this.size = this.tx.getSize(); - this.vsize = this.tx.getVirtualSize(); + constructor(wtx, block) { + this.hash = wtx.hash; + this.tx = wtx.tx; + this.mtime = wtx.mtime; + this.size = this.tx.getSize(); + this.vsize = this.tx.getVirtualSize(); - this.block = null; - this.height = -1; - this.time = 0; + this.block = null; + this.height = -1; + this.time = 0; - if (block) { - this.block = block.hash; - this.height = block.height; - this.time = block.time; + if (block) { + this.block = block.hash; + this.height = block.height; + this.time = block.time; + } + + this.inputs = []; + this.outputs = []; + + this.init(); } - this.inputs = []; - this.outputs = []; + /** + * Initialize transaction details. + * @private + */ - this.init(); + init() { + for (const input of this.tx.inputs) { + const member = new DetailsMember(); + member.address = input.getAddress(); + this.inputs.push(member); + } + + for (const output of this.tx.outputs) { + const member = new DetailsMember(); + member.value = output.value; + member.address = output.getAddress(); + this.outputs.push(member); + } + } + + /** + * Add necessary info to input member. + * @param {Number} i + * @param {Path} path + * @param {Coin} coin + */ + + setInput(i, path, coin) { + const member = this.inputs[i]; + + if (coin) { + member.value = coin.value; + member.address = coin.getAddress(); + } + + if (path) + member.path = path; + } + + /** + * Add necessary info to output member. + * @param {Number} i + * @param {Path} path + */ + + setOutput(i, path) { + const member = this.outputs[i]; + + if (path) + member.path = path; + } + + /** + * Calculate confirmations. + * @returns {Number} + */ + + getDepth(height) { + if (this.height === -1) + return 0; + + if (height == null) + return 0; + + const depth = height - this.height; + + if (depth < 0) + return 0; + + return depth + 1; + } + + /** + * Calculate fee. Only works if wallet + * owns all inputs. Returns 0 otherwise. + * @returns {Amount} + */ + + getFee() { + let inputValue = 0; + let outputValue = 0; + + for (const input of this.inputs) { + if (!input.path) + return 0; + + inputValue += input.value; + } + + for (const output of this.outputs) + outputValue += output.value; + + return inputValue - outputValue; + } + + /** + * Calculate fee rate. Only works if wallet + * owns all inputs. Returns 0 otherwise. + * @param {Amount} fee + * @returns {Rate} + */ + + getRate(fee) { + return policy.getRate(this.vsize, fee); + } + + /** + * Convert details to a more json-friendly object. + * @returns {Object} + */ + + toJSON(network, height) { + const fee = this.getFee(); + const rate = this.getRate(fee); + + return { + hash: encoding.revHex(this.hash), + height: this.height, + block: this.block ? encoding.revHex(this.block) : null, + time: this.time, + mtime: this.mtime, + date: util.date(this.time), + mdate: util.date(this.mtime), + size: this.size, + virtualSize: this.vsize, + fee: fee, + rate: rate, + confirmations: this.getDepth(height), + inputs: this.inputs.map((input) => { + return input.getJSON(network); + }), + outputs: this.outputs.map((output) => { + return output.getJSON(network); + }), + tx: this.tx.toRaw().toString('hex') + }; + } } -/** - * Initialize transaction details. - * @private - */ - -Details.prototype.init = function init() { - for (const input of this.tx.inputs) { - const member = new DetailsMember(); - member.address = input.getAddress(); - this.inputs.push(member); - } - - for (const output of this.tx.outputs) { - const member = new DetailsMember(); - member.value = output.value; - member.address = output.getAddress(); - this.outputs.push(member); - } -}; - -/** - * Add necessary info to input member. - * @param {Number} i - * @param {Path} path - * @param {Coin} coin - */ - -Details.prototype.setInput = function setInput(i, path, coin) { - const member = this.inputs[i]; - - if (coin) { - member.value = coin.value; - member.address = coin.getAddress(); - } - - if (path) - member.path = path; -}; - -/** - * Add necessary info to output member. - * @param {Number} i - * @param {Path} path - */ - -Details.prototype.setOutput = function setOutput(i, path) { - const member = this.outputs[i]; - - if (path) - member.path = path; -}; - -/** - * Calculate confirmations. - * @returns {Number} - */ - -Details.prototype.getDepth = function getDepth(height) { - if (this.height === -1) - return 0; - - if (height == null) - return 0; - - const depth = height - this.height; - - if (depth < 0) - return 0; - - return depth + 1; -}; - -/** - * Calculate fee. Only works if wallet - * owns all inputs. Returns 0 otherwise. - * @returns {Amount} - */ - -Details.prototype.getFee = function getFee() { - let inputValue = 0; - let outputValue = 0; - - for (const input of this.inputs) { - if (!input.path) - return 0; - - inputValue += input.value; - } - - for (const output of this.outputs) - outputValue += output.value; - - return inputValue - outputValue; -}; - -/** - * Calculate fee rate. Only works if wallet - * owns all inputs. Returns 0 otherwise. - * @param {Amount} fee - * @returns {Rate} - */ - -Details.prototype.getRate = function getRate(fee) { - return policy.getRate(this.vsize, fee); -}; - -/** - * Convert details to a more json-friendly object. - * @returns {Object} - */ - -Details.prototype.toJSON = function toJSON(network, height) { - const fee = this.getFee(); - const rate = this.getRate(fee); - - return { - hash: encoding.revHex(this.hash), - height: this.height, - block: this.block ? encoding.revHex(this.block) : null, - time: this.time, - mtime: this.mtime, - date: util.date(this.time), - mdate: util.date(this.mtime), - size: this.size, - virtualSize: this.vsize, - fee: fee, - rate: rate, - confirmations: this.getDepth(height), - inputs: this.inputs.map((input) => { - return input.getJSON(network); - }), - outputs: this.outputs.map((output) => { - return output.getJSON(network); - }), - tx: this.tx.toRaw().toString('hex') - }; -}; - /** * Transaction Details Member - * @alias module:wallet.DetailsMember - * @constructor * @property {Number} value * @property {Address} address * @property {Path} path */ -function DetailsMember() { - if (!(this instanceof DetailsMember)) - return new DetailsMember(); +class DetailsMember { + /** + * Create details member. + * @constructor + */ - this.value = 0; - this.address = null; - this.path = null; + constructor() { + this.value = 0; + this.address = null; + this.path = null; + } + + /** + * Convert the member to a more json-friendly object. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Convert the member to a more json-friendly object. + * @param {Network} network + * @returns {Object} + */ + + getJSON(network) { + return { + value: this.value, + address: this.address + ? this.address.toString(network) + : null, + path: this.path + ? this.path.toJSON() + : null + }; + } } -/** - * Convert the member to a more json-friendly object. - * @returns {Object} - */ - -DetailsMember.prototype.toJSON = function toJSON() { - return this.getJSON(); -}; - -/** - * Convert the member to a more json-friendly object. - * @param {Network} network - * @returns {Object} - */ - -DetailsMember.prototype.getJSON = function getJSON(network) { - return { - value: this.value, - address: this.address - ? this.address.toString(network) - : null, - path: this.path - ? this.path.toJSON() - : null - }; -}; - /** * Block Record * @alias module:wallet.BlockRecord - * @constructor - * @param {Hash} hash - * @param {Number} height - * @param {Number} time */ -function BlockRecord(hash, height, time) { - if (!(this instanceof BlockRecord)) - return new BlockRecord(hash, height, time); +class BlockRecord { + /** + * Create a block record. + * @constructor + * @param {Hash} hash + * @param {Number} height + * @param {Number} time + */ - this.hash = hash || encoding.NULL_HASH; - this.height = height != null ? height : -1; - this.time = time || 0; - this.hashes = new Set(); -} - -/** - * Add transaction to block record. - * @param {Hash} hash - * @returns {Boolean} - */ - -BlockRecord.prototype.add = function add(hash) { - if (this.hashes.has(hash)) - return false; - - this.hashes.add(hash); - - return true; -}; - -/** - * Remove transaction from block record. - * @param {Hash} hash - * @returns {Boolean} - */ - -BlockRecord.prototype.remove = function remove(hash) { - return this.hashes.delete(hash); -}; - -/** - * Instantiate wallet block from serialized tip data. - * @private - * @param {Buffer} data - */ - -BlockRecord.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - - this.hash = br.readHash('hex'); - this.height = br.readU32(); - this.time = br.readU32(); - - const count = br.readU32(); - - for (let i = 0; i < count; i++) { - const hash = br.readHash('hex'); - this.hashes.add(hash); + constructor(hash, height, time) { + this.hash = hash || encoding.NULL_HASH; + this.height = height != null ? height : -1; + this.time = time || 0; + this.hashes = new Set(); } - return this; -}; + /** + * Add transaction to block record. + * @param {Hash} hash + * @returns {Boolean} + */ -/** - * Instantiate wallet block from serialized data. - * @param {Buffer} data - * @returns {BlockRecord} - */ + add(hash) { + if (this.hashes.has(hash)) + return false; -BlockRecord.fromRaw = function fromRaw(data) { - return new BlockRecord().fromRaw(data); -}; + this.hashes.add(hash); -/** - * Get serialization size. - * @returns {Number} - */ + return true; + } -BlockRecord.prototype.getSize = function getSize() { - return 44 + this.hashes.size * 32; -}; + /** + * Remove transaction from block record. + * @param {Hash} hash + * @returns {Boolean} + */ -/** - * Serialize the wallet block as a tip (hash and height). - * @returns {Buffer} - */ + remove(hash) { + return this.hashes.delete(hash); + } -BlockRecord.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); + /** + * Instantiate wallet block from serialized tip data. + * @private + * @param {Buffer} data + */ - bw.writeHash(this.hash); - bw.writeU32(this.height); - bw.writeU32(this.time); + fromRaw(data) { + const br = bio.read(data); - bw.writeU32(this.hashes.size); + this.hash = br.readHash('hex'); + this.height = br.readU32(); + this.time = br.readU32(); - for (const hash of this.hashes) - bw.writeHash(hash); + const count = br.readU32(); - return bw.render(); -}; + for (let i = 0; i < count; i++) { + const hash = br.readHash('hex'); + this.hashes.add(hash); + } -/** - * Convert hashes set to an array. - * @returns {Hash[]} - */ + return this; + } -BlockRecord.prototype.toArray = function toArray() { - const hashes = []; - for (const hash of this.hashes) - hashes.push(hash); - return hashes; -}; + /** + * Instantiate wallet block from serialized data. + * @param {Buffer} data + * @returns {BlockRecord} + */ -/** - * Convert the block to a more json-friendly object. - * @returns {Object} - */ + static fromRaw(data) { + return new this().fromRaw(data); + } -BlockRecord.prototype.toJSON = function toJSON() { - return { - hash: encoding.revHex(this.hash), - height: this.height, - time: this.time, - hashes: this.toArray().map(encoding.revHex) - }; -}; + /** + * Get serialization size. + * @returns {Number} + */ -/** - * Instantiate wallet block from block meta. - * @private - * @param {BlockMeta} block - */ + getSize() { + return 44 + this.hashes.size * 32; + } -BlockRecord.prototype.fromMeta = function fromMeta(block) { - this.hash = block.hash; - this.height = block.height; - this.time = block.time; - return this; -}; + /** + * Serialize the wallet block as a tip (hash and height). + * @returns {Buffer} + */ -/** - * Instantiate wallet block from block meta. - * @param {BlockMeta} block - * @returns {BlockRecord} - */ + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); -BlockRecord.fromMeta = function fromMeta(block) { - return new BlockRecord().fromMeta(block); -}; + bw.writeHash(this.hash); + bw.writeU32(this.height); + bw.writeU32(this.time); + + bw.writeU32(this.hashes.size); + + for (const hash of this.hashes) + bw.writeHash(hash); + + return bw.render(); + } + + /** + * Convert hashes set to an array. + * @returns {Hash[]} + */ + + toArray() { + const hashes = []; + for (const hash of this.hashes) + hashes.push(hash); + return hashes; + } + + /** + * Convert the block to a more json-friendly object. + * @returns {Object} + */ + + toJSON() { + return { + hash: encoding.revHex(this.hash), + height: this.height, + time: this.time, + hashes: this.toArray().map(encoding.revHex) + }; + } + + /** + * Instantiate wallet block from block meta. + * @private + * @param {BlockMeta} block + */ + + fromMeta(block) { + this.hash = block.hash; + this.height = block.height; + this.time = block.time; + return this; + } + + /** + * Instantiate wallet block from block meta. + * @param {BlockMeta} block + * @returns {BlockRecord} + */ + + static fromMeta(block) { + return new this().fromMeta(block); + } +} /* * Expose diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index d5fe79c7..42101eec 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -33,2333 +33,2316 @@ const {encoding} = bio; const {Mnemonic} = HD; /** - * BIP44 Wallet + * Wallet * @alias module:wallet.Wallet - * @constructor - * @param {Object} options - * 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 {String?} options.type - Type of wallet (pubkeyhash, multisig) - * (default=pubkeyhash). - * @param {Boolean?} options.compressed - Whether to use compressed - * public keys (default=true). - * @param {Number?} options.m - `m` value for multisig. - * @param {Number?} options.n - `n` value for multisig. - * @param {String?} options.id - Wallet ID (used for storage) - * @param {String?} options.mnemonic - mnemonic phrase to use to instantiate an - * hd private key for wallet - * (default=account key "address"). + * @extends EventEmitter */ -function Wallet(wdb, options) { - if (!(this instanceof Wallet)) - return new Wallet(wdb, options); +class Wallet extends EventEmitter { + /** + * Create a wallet. + * @constructor + * @param {Object} options + */ - EventEmitter.call(this); + constructor(wdb, options) { + super(); - assert(wdb, 'WDB required.'); + assert(wdb, 'WDB required.'); - this.wdb = wdb; - this.db = wdb.db; - this.network = wdb.network; - this.logger = wdb.logger; - this.writeLock = new Lock(); - this.fundLock = new Lock(); + this.wdb = wdb; + this.db = wdb.db; + this.network = wdb.network; + this.logger = wdb.logger; + this.writeLock = new Lock(); + this.fundLock = new Lock(); - this.wid = 0; - this.id = null; - this.initialized = false; - this.watchOnly = false; - this.accountDepth = 0; - this.token = encoding.ZERO_HASH; - this.tokenDepth = 0; - this.master = new MasterKey(); + this.wid = 0; + this.id = null; + this.initialized = false; + this.watchOnly = false; + this.accountDepth = 0; + this.token = encoding.ZERO_HASH; + this.tokenDepth = 0; + this.master = new MasterKey(); - this.txdb = new TXDB(this.wdb); + this.txdb = new TXDB(this.wdb); - if (options) - this.fromOptions(options); -} - -Object.setPrototypeOf(Wallet.prototype, EventEmitter.prototype); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Wallet.prototype.fromOptions = function fromOptions(options) { - let key = options.master; - let id, token, mnemonic; - - if (key) { - if (typeof key === 'string') - key = HD.PrivateKey.fromBase58(key, this.network); - - assert(HD.isPrivate(key), - 'Must create wallet with hd private key.'); - } else { - mnemonic = new Mnemonic(options.mnemonic); - key = HD.fromMnemonic(mnemonic); + if (options) + this.fromOptions(options); } - this.master.fromKey(key, mnemonic, this.network); + /** + * Inject properties from options object. + * @private + * @param {Object} options + */ - if (options.wid != null) { - assert((options.wid >>> 0) === options.wid); - this.wid = options.wid; + fromOptions(options) { + let key = options.master; + let id, token, mnemonic; + + if (key) { + if (typeof key === 'string') + key = HD.PrivateKey.fromBase58(key, this.network); + + assert(HD.isPrivate(key), + 'Must create wallet with hd private key.'); + } else { + mnemonic = new Mnemonic(options.mnemonic); + key = HD.fromMnemonic(mnemonic); + } + + this.master.fromKey(key, mnemonic, this.network); + + if (options.wid != null) { + assert((options.wid >>> 0) === options.wid); + this.wid = options.wid; + } + + if (options.id) { + assert(common.isName(options.id), 'Bad wallet ID.'); + id = options.id; + } + + if (options.initialized != null) { + assert(typeof options.initialized === 'boolean'); + this.initialized = options.initialized; + } + + if (options.watchOnly != null) { + assert(typeof options.watchOnly === 'boolean'); + this.watchOnly = options.watchOnly; + } + + if (options.accountDepth != null) { + assert((options.accountDepth >>> 0) === options.accountDepth); + this.accountDepth = options.accountDepth; + } + + if (options.token) { + assert(Buffer.isBuffer(options.token)); + assert(options.token.length === 32); + token = options.token; + } + + if (options.tokenDepth != null) { + assert((options.tokenDepth >>> 0) === options.tokenDepth); + this.tokenDepth = options.tokenDepth; + } + + if (!id) + id = this.getID(); + + if (!token) + token = this.getToken(this.tokenDepth); + + this.id = id; + this.token = token; + + return this; } - if (options.id) { - assert(common.isName(options.id), 'Bad wallet ID.'); - id = options.id; + /** + * Instantiate wallet from options. + * @param {WalletDB} wdb + * @param {Object} options + * @returns {Wallet} + */ + + static fromOptions(wdb, options) { + return new this(wdb).fromOptions(options); } - if (options.initialized != null) { - assert(typeof options.initialized === 'boolean'); - this.initialized = options.initialized; + /** + * Attempt to intialize the wallet (generating + * the first addresses along with the lookahead + * addresses). Called automatically from the + * walletdb. + * @returns {Promise} + */ + + async init(options) { + const passphrase = options.passphrase; + + assert(!this.initialized); + this.initialized = true; + + if (passphrase) + await this.master.encrypt(passphrase); + + const account = await this._createAccount(options, passphrase); + assert(account); + + this.logger.info('Wallet initialized (%s).', this.id); + + return this.txdb.open(this); } - if (options.watchOnly != null) { - assert(typeof options.watchOnly === 'boolean'); - this.watchOnly = options.watchOnly; + /** + * Open wallet (done after retrieval). + * @returns {Promise} + */ + + async open() { + assert(this.initialized); + + const account = await this.getAccount(0); + + if (!account) + throw new Error('Default account not found.'); + + this.logger.info('Wallet opened (%s).', this.id); + + return this.txdb.open(this); } - if (options.accountDepth != null) { - assert((options.accountDepth >>> 0) === options.accountDepth); - this.accountDepth = options.accountDepth; + /** + * Close the wallet, unregister with the database. + * @returns {Promise} + */ + + async destroy() { + const unlock1 = await this.writeLock.lock(); + const unlock2 = await this.fundLock.lock(); + try { + await this.master.destroy(); + this.writeLock.destroy(); + this.fundLock.destroy(); + } finally { + unlock2(); + unlock1(); + } } - if (options.token) { - assert(Buffer.isBuffer(options.token)); - assert(options.token.length === 32); - token = options.token; + /** + * Add a public account key to the wallet (multisig). + * Saves the key in the wallet database. + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ + + async addSharedKey(acct, key) { + const unlock = await this.writeLock.lock(); + try { + return await this._addSharedKey(acct, key); + } finally { + unlock(); + } } - if (options.tokenDepth != null) { - assert((options.tokenDepth >>> 0) === options.tokenDepth); - this.tokenDepth = options.tokenDepth; + /** + * Add a public account key to the wallet without a lock. + * @private + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ + + async _addSharedKey(acct, key) { + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + const b = this.db.batch(); + const result = await account.addSharedKey(b, key); + await b.write(); + + return result; } - if (!id) - id = this.getID(); + /** + * Remove a public account key from the wallet (multisig). + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ - if (!token) - token = this.getToken(this.tokenDepth); - - this.id = id; - this.token = token; - - return this; -}; - -/** - * Instantiate wallet from options. - * @param {WalletDB} wdb - * @param {Object} options - * @returns {Wallet} - */ - -Wallet.fromOptions = function fromOptions(wdb, options) { - return new Wallet(wdb).fromOptions(options); -}; - -/** - * Attempt to intialize the wallet (generating - * the first addresses along with the lookahead - * addresses). Called automatically from the - * walletdb. - * @returns {Promise} - */ - -Wallet.prototype.init = async function init(options) { - const passphrase = options.passphrase; - - assert(!this.initialized); - this.initialized = true; - - if (passphrase) - await this.master.encrypt(passphrase); - - const account = await this._createAccount(options, passphrase); - assert(account); - - this.logger.info('Wallet initialized (%s).', this.id); - - return this.txdb.open(this); -}; - -/** - * Open wallet (done after retrieval). - * @returns {Promise} - */ - -Wallet.prototype.open = async function open() { - assert(this.initialized); - - const account = await this.getAccount(0); - - if (!account) - throw new Error('Default account not found.'); - - this.logger.info('Wallet opened (%s).', this.id); - - return this.txdb.open(this); -}; - -/** - * Close the wallet, unregister with the database. - * @returns {Promise} - */ - -Wallet.prototype.destroy = async function destroy() { - const unlock1 = await this.writeLock.lock(); - const unlock2 = await this.fundLock.lock(); - try { - await this.master.destroy(); - this.writeLock.destroy(); - this.fundLock.destroy(); - } finally { - unlock2(); - unlock1(); - } -}; - -/** - * Add a public account key to the wallet (multisig). - * Saves the key in the wallet database. - * @param {(Number|String)} acct - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Wallet.prototype.addSharedKey = async function addSharedKey(acct, key) { - const unlock = await this.writeLock.lock(); - try { - return await this._addSharedKey(acct, key); - } finally { - unlock(); - } -}; - -/** - * Add a public account key to the wallet without a lock. - * @private - * @param {(Number|String)} acct - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Wallet.prototype._addSharedKey = async function _addSharedKey(acct, key) { - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - const b = this.db.batch(); - const result = await account.addSharedKey(b, key); - await b.write(); - - return result; -}; - -/** - * Remove a public account key from the wallet (multisig). - * @param {(Number|String)} acct - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Wallet.prototype.removeSharedKey = async function removeSharedKey(acct, key) { - const unlock = await this.writeLock.lock(); - try { - return await this._removeSharedKey(acct, key); - } finally { - unlock(); - } -}; - -/** - * Remove a public account key from the wallet (multisig). - * @private - * @param {(Number|String)} acct - * @param {HDPublicKey} key - * @returns {Promise} - */ - -Wallet.prototype._removeSharedKey = async function _removeSharedKey(acct, key) { - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - const b = this.db.batch(); - const result = await account.removeSharedKey(b, key); - await b.write(); - - return result; -}; - -/** - * Change or set master key's passphrase. - * @param {String|Buffer} passphrase - * @param {String|Buffer} old - * @returns {Promise} - */ - -Wallet.prototype.setPassphrase = async function setPassphrase(passphrase, old) { - if (old != null) - await this.decrypt(old); - - await this.encrypt(passphrase); -}; - -/** - * Encrypt the wallet permanently. - * @param {String|Buffer} passphrase - * @returns {Promise} - */ - -Wallet.prototype.encrypt = async function encrypt(passphrase) { - const unlock = await this.writeLock.lock(); - try { - return await this._encrypt(passphrase); - } finally { - unlock(); - } -}; - -/** - * Encrypt the wallet permanently, without a lock. - * @private - * @param {String|Buffer} passphrase - * @returns {Promise} - */ - -Wallet.prototype._encrypt = async function _encrypt(passphrase) { - const key = await this.master.encrypt(passphrase, true); - const b = this.db.batch(); - - try { - await this.wdb.encryptKeys(b, this.wid, key); - } finally { - cleanse(key); + async removeSharedKey(acct, key) { + const unlock = await this.writeLock.lock(); + try { + return await this._removeSharedKey(acct, key); + } finally { + unlock(); + } } - this.save(b); + /** + * Remove a public account key from the wallet (multisig). + * @private + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ - await b.write(); -}; + async _removeSharedKey(acct, key) { + const account = await this.getAccount(acct); -/** - * Decrypt the wallet permanently. - * @param {String|Buffer} passphrase - * @returns {Promise} - */ + if (!account) + throw new Error('Account not found.'); -Wallet.prototype.decrypt = async function decrypt(passphrase) { - const unlock = await this.writeLock.lock(); - try { - return await this._decrypt(passphrase); - } finally { - unlock(); - } -}; + const b = this.db.batch(); + const result = await account.removeSharedKey(b, key); + await b.write(); -/** - * Decrypt the wallet permanently, without a lock. - * @private - * @param {String|Buffer} passphrase - * @returns {Promise} - */ - -Wallet.prototype._decrypt = async function _decrypt(passphrase) { - const key = await this.master.decrypt(passphrase, true); - const b = this.db.batch(); - - try { - await this.wdb.decryptKeys(b, this.wid, key); - } finally { - cleanse(key); + return result; } - this.save(b); + /** + * Change or set master key's passphrase. + * @param {String|Buffer} passphrase + * @param {String|Buffer} old + * @returns {Promise} + */ - await b.write(); -}; + async setPassphrase(passphrase, old) { + if (old != null) + await this.decrypt(old); -/** - * Generate a new token. - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ - -Wallet.prototype.retoken = async function retoken(passphrase) { - const unlock = await this.writeLock.lock(); - try { - return await this._retoken(passphrase); - } finally { - unlock(); + await this.encrypt(passphrase); } -}; -/** - * Generate a new token without a lock. - * @private - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ + /** + * Encrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + + async encrypt(passphrase) { + const unlock = await this.writeLock.lock(); + try { + return await this._encrypt(passphrase); + } finally { + unlock(); + } + } + + /** + * Encrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + + async _encrypt(passphrase) { + const key = await this.master.encrypt(passphrase, true); + const b = this.db.batch(); + + try { + await this.wdb.encryptKeys(b, this.wid, key); + } finally { + cleanse(key); + } + + this.save(b); + + await b.write(); + } + + /** + * Decrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + + async decrypt(passphrase) { + const unlock = await this.writeLock.lock(); + try { + return await this._decrypt(passphrase); + } finally { + unlock(); + } + } + + /** + * Decrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + + async _decrypt(passphrase) { + const key = await this.master.decrypt(passphrase, true); + const b = this.db.batch(); + + try { + await this.wdb.decryptKeys(b, this.wid, key); + } finally { + cleanse(key); + } + + this.save(b); + + await b.write(); + } + + /** + * Generate a new token. + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + + async retoken(passphrase) { + const unlock = await this.writeLock.lock(); + try { + return await this._retoken(passphrase); + } finally { + unlock(); + } + } + + /** + * Generate a new token without a lock. + * @private + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + + async _retoken(passphrase) { + if (passphrase) + await this.unlock(passphrase); + + this.tokenDepth += 1; + this.token = this.getToken(this.tokenDepth); + + const b = this.db.batch(); + this.save(b); + + await b.write(); + + return this.token; + } + + /** + * Rename the wallet. + * @param {String} id + * @returns {Promise} + */ + + async rename(id) { + const unlock = await this.writeLock.lock(); + try { + return await this.wdb.rename(this, id); + } finally { + unlock(); + } + } + + /** + * Rename account. + * @param {(String|Number)?} acct + * @param {String} name + * @returns {Promise} + */ + + async renameAccount(acct, name) { + const unlock = await this.writeLock.lock(); + try { + return await this._renameAccount(acct, name); + } finally { + unlock(); + } + } + + /** + * Rename account without a lock. + * @private + * @param {(String|Number)?} acct + * @param {String} name + * @returns {Promise} + */ + + async _renameAccount(acct, name) { + if (!common.isName(name)) + throw new Error('Bad account name.'); + + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + if (account.accountIndex === 0) + throw new Error('Cannot rename default account.'); + + if (await this.hasAccount(name)) + throw new Error('Account name not available.'); + + const b = this.db.batch(); + + this.wdb.renameAccount(b, account, name); + + await b.write(); + } + + /** + * Lock the wallet, destroy decrypted key. + */ + + async lock() { + const unlock1 = await this.writeLock.lock(); + const unlock2 = await this.fundLock.lock(); + try { + await this.master.lock(); + } finally { + unlock2(); + unlock1(); + } + } + + /** + * Unlock the key for `timeout` seconds. + * @param {Buffer|String} passphrase + * @param {Number?} [timeout=60] + */ + + unlock(passphrase, timeout) { + return this.master.unlock(passphrase, timeout); + } + + /** + * Generate the wallet ID if none was passed in. + * It is represented as HASH160(m/44->public|magic) + * converted to an "address" with a prefix + * of `0x03be04` (`WLT` in base58). + * @private + * @returns {Base58String} + */ + + getID() { + assert(this.master.key, 'Cannot derive id.'); + + const key = this.master.key.derive(44); + + const bw = bio.write(37); + bw.writeBytes(key.publicKey); + bw.writeU32(this.network.magic); + + const hash = hash160.digest(bw.render()); + + const b58 = bio.write(27); + b58.writeU8(0x03); + b58.writeU8(0xbe); + b58.writeU8(0x04); + b58.writeBytes(hash); + b58.writeChecksum(hash256.digest); + + return base58.encode(b58.render()); + } + + /** + * Generate the wallet api key if none was passed in. + * It is represented as HASH256(m/44'->private|nonce). + * @private + * @param {HDPrivateKey} master + * @param {Number} nonce + * @returns {Buffer} + */ + + getToken(nonce) { + if (!this.master.key) + throw new Error('Cannot derive token.'); + + const key = this.master.key.derive(44, true); + + const bw = bio.write(36); + bw.writeBytes(key.privateKey); + bw.writeU32(nonce); + + return hash256.digest(bw.render()); + } + + /** + * Create an account. Requires passphrase if master key is encrypted. + * @param {Object} options - See {@link Account} options. + * @returns {Promise} - Returns {@link Account}. + */ + + async createAccount(options, passphrase) { + const unlock = await this.writeLock.lock(); + try { + return await this._createAccount(options, passphrase); + } finally { + unlock(); + } + } + + /** + * Create an account without a lock. + * @param {Object} options - See {@link Account} options. + * @returns {Promise} - Returns {@link Account}. + */ + + async _createAccount(options, passphrase) { + let name = options.name; + + if (!name) + name = this.accountDepth.toString(10); + + if (await this.hasAccount(name)) + throw new Error('Account already exists.'); -Wallet.prototype._retoken = async function _retoken(passphrase) { - if (passphrase) await this.unlock(passphrase); - this.tokenDepth += 1; - this.token = this.getToken(this.tokenDepth); + let key; + if (this.watchOnly && options.accountKey) { + key = options.accountKey; - const b = this.db.batch(); - this.save(b); + if (typeof key === 'string') + key = HD.PublicKey.fromBase58(key, this.network); - await b.write(); + if (!HD.isPublic(key)) + throw new Error('Must add HD public keys to watch only wallet.'); + } else { + assert(this.master.key); + const type = this.network.keyPrefix.coinType; + key = this.master.key.deriveAccount(44, type, this.accountDepth); + key = key.toPublic(); + } - return this.token; -}; + const opt = { + wid: this.wid, + id: this.id, + name: this.accountDepth === 0 ? 'default' : name, + witness: options.witness, + watchOnly: this.watchOnly, + accountKey: key, + accountIndex: this.accountDepth, + type: options.type, + m: options.m, + n: options.n, + keys: options.keys + }; -/** - * Rename the wallet. - * @param {String} id - * @returns {Promise} - */ + const b = this.db.batch(); -Wallet.prototype.rename = async function rename(id) { - const unlock = await this.writeLock.lock(); - try { - return await this.wdb.rename(this, id); - } finally { - unlock(); - } -}; + const account = Account.fromOptions(this.wdb, opt); -/** - * Rename account. - * @param {(String|Number)?} acct - * @param {String} name - * @returns {Promise} - */ + await account.init(b); -Wallet.prototype.renameAccount = async function renameAccount(acct, name) { - const unlock = await this.writeLock.lock(); - try { - return await this._renameAccount(acct, name); - } finally { - unlock(); - } -}; + this.logger.info('Created account %s/%s/%d.', + account.id, + account.name, + account.accountIndex); -/** - * Rename account without a lock. - * @private - * @param {(String|Number)?} acct - * @param {String} name - * @returns {Promise} - */ + this.accountDepth += 1; + this.save(b); -Wallet.prototype._renameAccount = async function _renameAccount(acct, name) { - if (!common.isName(name)) - throw new Error('Bad account name.'); + await b.write(); - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - if (account.accountIndex === 0) - throw new Error('Cannot rename default account.'); - - if (await this.hasAccount(name)) - throw new Error('Account name not available.'); - - const b = this.db.batch(); - - this.wdb.renameAccount(b, account, name); - - await b.write(); -}; - -/** - * Lock the wallet, destroy decrypted key. - */ - -Wallet.prototype.lock = async function lock() { - const unlock1 = await this.writeLock.lock(); - const unlock2 = await this.fundLock.lock(); - try { - await this.master.lock(); - } finally { - unlock2(); - unlock1(); - } -}; - -/** - * Unlock the key for `timeout` seconds. - * @param {Buffer|String} passphrase - * @param {Number?} [timeout=60] - */ - -Wallet.prototype.unlock = function unlock(passphrase, timeout) { - return this.master.unlock(passphrase, timeout); -}; - -/** - * Generate the wallet ID if none was passed in. - * It is represented as HASH160(m/44->public|magic) - * converted to an "address" with a prefix - * of `0x03be04` (`WLT` in base58). - * @private - * @returns {Base58String} - */ - -Wallet.prototype.getID = function getID() { - assert(this.master.key, 'Cannot derive id.'); - - const key = this.master.key.derive(44); - - const bw = bio.write(37); - bw.writeBytes(key.publicKey); - bw.writeU32(this.network.magic); - - const hash = hash160.digest(bw.render()); - - const b58 = bio.write(27); - b58.writeU8(0x03); - b58.writeU8(0xbe); - b58.writeU8(0x04); - b58.writeBytes(hash); - b58.writeChecksum(hash256.digest); - - return base58.encode(b58.render()); -}; - -/** - * Generate the wallet api key if none was passed in. - * It is represented as HASH256(m/44'->private|nonce). - * @private - * @param {HDPrivateKey} master - * @param {Number} nonce - * @returns {Buffer} - */ - -Wallet.prototype.getToken = function getToken(nonce) { - if (!this.master.key) - throw new Error('Cannot derive token.'); - - const key = this.master.key.derive(44, true); - - const bw = bio.write(36); - bw.writeBytes(key.privateKey); - bw.writeU32(nonce); - - return hash256.digest(bw.render()); -}; - -/** - * Create an account. Requires passphrase if master key is encrypted. - * @param {Object} options - See {@link Account} options. - * @returns {Promise} - Returns {@link Account}. - */ - -Wallet.prototype.createAccount = async function createAccount(options, passphrase) { - const unlock = await this.writeLock.lock(); - try { - return await this._createAccount(options, passphrase); - } finally { - unlock(); - } -}; - -/** - * Create an account without a lock. - * @param {Object} options - See {@link Account} options. - * @returns {Promise} - Returns {@link Account}. - */ - -Wallet.prototype._createAccount = async function _createAccount(options, passphrase) { - let name = options.name; - - if (!name) - name = this.accountDepth.toString(10); - - if (await this.hasAccount(name)) - throw new Error('Account already exists.'); - - await this.unlock(passphrase); - - let key; - if (this.watchOnly && options.accountKey) { - key = options.accountKey; - - if (typeof key === 'string') - key = HD.PublicKey.fromBase58(key, this.network); - - if (!HD.isPublic(key)) - throw new Error('Must add HD public keys to watch only wallet.'); - } else { - assert(this.master.key); - const type = this.network.keyPrefix.coinType; - key = this.master.key.deriveAccount(44, type, this.accountDepth); - key = key.toPublic(); - } - - const opt = { - wid: this.wid, - id: this.id, - name: this.accountDepth === 0 ? 'default' : name, - witness: options.witness, - watchOnly: this.watchOnly, - accountKey: key, - accountIndex: this.accountDepth, - type: options.type, - m: options.m, - n: options.n, - keys: options.keys - }; - - const b = this.db.batch(); - - const account = Account.fromOptions(this.wdb, opt); - - await account.init(b); - - this.logger.info('Created account %s/%s/%d.', - account.id, - account.name, - account.accountIndex); - - this.accountDepth += 1; - this.save(b); - - await b.write(); - - return account; -}; - -/** - * Ensure an account. Requires passphrase if master key is encrypted. - * @param {Object} options - See {@link Account} options. - * @returns {Promise} - Returns {@link Account}. - */ - -Wallet.prototype.ensureAccount = async function ensureAccount(options, passphrase) { - const name = options.name; - const account = await this.getAccount(name); - - if (account) return account; + } - return this.createAccount(options, passphrase); -}; + /** + * Ensure an account. Requires passphrase if master key is encrypted. + * @param {Object} options - See {@link Account} options. + * @returns {Promise} - Returns {@link Account}. + */ -/** - * List account names and indexes from the db. - * @returns {Promise} - Returns Array. - */ + async ensureAccount(options, passphrase) { + const name = options.name; + const account = await this.getAccount(name); -Wallet.prototype.getAccounts = function getAccounts() { - return this.wdb.getAccounts(this.wid); -}; + if (account) + return account; -/** - * Get all wallet address hashes. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns Array. - */ + return this.createAccount(options, passphrase); + } -Wallet.prototype.getAddressHashes = function getAddressHashes(acct) { - if (acct != null) - return this.getAccountHashes(acct); - return this.wdb.getWalletHashes(this.wid); -}; + /** + * List account names and indexes from the db. + * @returns {Promise} - Returns Array. + */ -/** - * Get all account address hashes. - * @param {String|Number} acct - * @returns {Promise} - Returns Array. - */ + getAccounts() { + return this.wdb.getAccounts(this.wid); + } -Wallet.prototype.getAccountHashes = async function getAccountHashes(acct) { - const index = await this.getAccountIndex(acct); + /** + * Get all wallet address hashes. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns Array. + */ - if (index === -1) - throw new Error('Account not found.'); + getAddressHashes(acct) { + if (acct != null) + return this.getAccountHashes(acct); + return this.wdb.getWalletHashes(this.wid); + } - return this.wdb.getAccountHashes(this.wid, index); -}; + /** + * Get all account address hashes. + * @param {String|Number} acct + * @returns {Promise} - Returns Array. + */ -/** - * Retrieve an account from the database. - * @param {Number|String} acct - * @returns {Promise} - Returns {@link Account}. - */ + async getAccountHashes(acct) { + const index = await this.getAccountIndex(acct); -Wallet.prototype.getAccount = async function getAccount(acct) { - const index = await this.getAccountIndex(acct); + if (index === -1) + throw new Error('Account not found.'); - if (index === -1) - return null; + return this.wdb.getAccountHashes(this.wid, index); + } - const account = await this.wdb.getAccount(this.wid, index); + /** + * Retrieve an account from the database. + * @param {Number|String} acct + * @returns {Promise} - Returns {@link Account}. + */ - if (!account) - return null; + async getAccount(acct) { + const index = await this.getAccountIndex(acct); - account.wid = this.wid; - account.id = this.id; - account.watchOnly = this.watchOnly; + if (index === -1) + return null; - return account; -}; + const account = await this.wdb.getAccount(this.wid, index); -/** - * Lookup the corresponding account name's index. - * @param {String|Number} acct - Account name/index. - * @returns {Promise} - Returns Number. - */ + if (!account) + return null; -Wallet.prototype.getAccountIndex = function getAccountIndex(acct) { - if (acct == null) - return -1; + account.wid = this.wid; + account.id = this.id; + account.watchOnly = this.watchOnly; - if (typeof acct === 'number') - return acct; + return account; + } - return this.wdb.getAccountIndex(this.wid, acct); -}; + /** + * Lookup the corresponding account name's index. + * @param {String|Number} acct - Account name/index. + * @returns {Promise} - Returns Number. + */ -/** - * Lookup the corresponding account name's index. - * @param {String|Number} acct - Account name/index. - * @returns {Promise} - Returns Number. - * @throws on non-existent account - */ + getAccountIndex(acct) { + if (acct == null) + return -1; -Wallet.prototype.ensureIndex = async function ensureIndex(acct) { - if (acct == null || acct === -1) - return -1; + if (typeof acct === 'number') + return acct; - const index = await this.getAccountIndex(acct); + return this.wdb.getAccountIndex(this.wid, acct); + } - if (index === -1) - throw new Error('Account not found.'); + /** + * Lookup the corresponding account name's index. + * @param {String|Number} acct - Account name/index. + * @returns {Promise} - Returns Number. + * @throws on non-existent account + */ - return index; -}; + async ensureIndex(acct) { + if (acct == null || acct === -1) + return -1; -/** - * Lookup the corresponding account index's name. - * @param {WalletID} wid - * @param {Number} index - Account index. - * @returns {Promise} - Returns String. - */ + const index = await this.getAccountIndex(acct); + + if (index === -1) + throw new Error('Account not found.'); -Wallet.prototype.getAccountName = async function getAccountName(index) { - if (typeof index === 'string') return index; - - return this.wdb.getAccountName(this.wid, index); -}; - -/** - * Test whether an account exists. - * @param {Number|String} acct - * @returns {Promise} - Returns {@link Boolean}. - */ - -Wallet.prototype.hasAccount = async function hasAccount(acct) { - const index = await this.getAccountIndex(acct); - - if (index === -1) - return false; - - return this.db.hasAccount(this.wid, index); -}; - -/** - * Create a new receiving address (increments receiveDepth). - * @param {(Number|String)?} acct - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Wallet.prototype.createReceive = function createReceive(acct = 0) { - return this.createKey(acct, 0); -}; - -/** - * Create a new change address (increments receiveDepth). - * @param {(Number|String)?} acct - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Wallet.prototype.createChange = function createChange(acct = 0) { - return this.createKey(acct, 1); -}; - -/** - * Create a new nested address (increments receiveDepth). - * @param {(Number|String)?} acct - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Wallet.prototype.createNested = function createNested(acct = 0) { - return this.createKey(acct, 2); -}; - -/** - * Create a new address (increments depth). - * @param {(Number|String)?} acct - * @param {Number} branch - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Wallet.prototype.createKey = async function createKey(acct, branch) { - const unlock = await this.writeLock.lock(); - try { - return await this._createKey(acct, branch); - } finally { - unlock(); - } -}; - -/** - * Create a new address (increments depth) without a lock. - * @private - * @param {(Number|String)?} acct - * @param {Number} branche - * @returns {Promise} - Returns {@link WalletKey}. - */ - -Wallet.prototype._createKey = async function _createKey(acct, branch) { - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - const b = this.db.batch(); - const key = await account.createKey(b, branch); - await b.write(); - - return key; -}; - -/** - * Save the wallet to the database. Necessary - * when address depth and keys change. - * @returns {Promise} - */ - -Wallet.prototype.save = function save(b) { - return this.wdb.save(b, this); -}; - -/** - * Test whether the wallet possesses an address. - * @param {Address|Hash} address - * @returns {Promise} - Returns Boolean. - */ - -Wallet.prototype.hasAddress = async function hasAddress(address) { - const hash = Address.getHash(address, 'hex'); - const path = await this.getPath(hash); - return path != null; -}; - -/** - * Get path by address hash. - * @param {Address|Hash} address - * @returns {Promise} - Returns {@link Path}. - */ - -Wallet.prototype.getPath = async function getPath(address) { - const hash = Address.getHash(address, 'hex'); - return this.wdb.getPath(this.wid, hash); -}; - -/** - * Get path by address hash (without account name). - * @private - * @param {Address|Hash} address - * @returns {Promise} - Returns {@link Path}. - */ - -Wallet.prototype.readPath = async function readPath(address) { - const hash = Address.getHash(address, 'hex'); - return this.wdb.readPath(this.wid, hash); -}; - -/** - * Test whether the wallet contains a path. - * @param {Address|Hash} address - * @returns {Promise} - Returns {Boolean}. - */ - -Wallet.prototype.hasPath = async function hasPath(address) { - const hash = Address.getHash(address, 'hex'); - return this.wdb.hasPath(this.wid, hash); -}; - -/** - * Get all wallet paths. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns {@link Path}. - */ - -Wallet.prototype.getPaths = async function getPaths(acct) { - if (acct != null) - return this.getAccountPaths(acct); - - return this.wdb.getWalletPaths(this.wid); -}; - -/** - * Get all account paths. - * @param {String|Number} acct - * @returns {Promise} - Returns {@link Path}. - */ - -Wallet.prototype.getAccountPaths = async function getAccountPaths(acct) { - const index = await this.getAccountIndex(acct); - - if (index === -1) - throw new Error('Account not found.'); - - const hashes = await this.getAccountHashes(index); - const name = await this.getAccountName(acct); - - assert(name); - - const result = []; - - for (const hash of hashes) { - const path = await this.readPath(hash); - - assert(path); - assert(path.account === index); - - path.name = name; - - result.push(path); } - return result; -}; + /** + * Lookup the corresponding account index's name. + * @param {WalletID} wid + * @param {Number} index - Account index. + * @returns {Promise} - Returns String. + */ -/** - * Import a keyring (will not exist on derivation chain). - * Rescanning must be invoked manually. - * @param {(String|Number)?} acct - * @param {WalletKey} ring - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ + async getAccountName(index) { + if (typeof index === 'string') + return index; -Wallet.prototype.importKey = async function importKey(acct, ring, passphrase) { - const unlock = await this.writeLock.lock(); - try { - return await this._importKey(acct, ring, passphrase); - } finally { - unlock(); - } -}; - -/** - * Import a keyring (will not exist on derivation chain) without a lock. - * @private - * @param {(String|Number)?} acct - * @param {WalletKey} ring - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ - -Wallet.prototype._importKey = async function _importKey(acct, ring, passphrase) { - if (!this.watchOnly) { - if (!ring.privateKey) - throw new Error('Cannot import pubkey into non watch-only wallet.'); - } else { - if (ring.privateKey) - throw new Error('Cannot import privkey into watch-only wallet.'); + return this.wdb.getAccountName(this.wid, index); } - const hash = ring.getHash('hex'); + /** + * Test whether an account exists. + * @param {Number|String} acct + * @returns {Promise} - Returns {@link Boolean}. + */ - if (await this.getPath(hash)) - throw new Error('Key already exists.'); + async hasAccount(acct) { + const index = await this.getAccountIndex(acct); - const account = await this.getAccount(acct); + if (index === -1) + return false; - if (!account) - throw new Error('Account not found.'); - - if (account.type !== Account.types.PUBKEYHASH) - throw new Error('Cannot import into non-pkh account.'); - - await this.unlock(passphrase); - - const key = WalletKey.fromRing(account, ring); - const path = key.toPath(); - - if (this.master.encrypted) { - path.data = this.master.encipher(path.data, path.hash); - assert(path.data); - path.encrypted = true; + return this.db.hasAccount(this.wid, index); } - const b = this.db.batch(); - await account.savePath(b, path); - await b.write(); -}; + /** + * Create a new receiving address (increments receiveDepth). + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. + */ -/** - * Import a keyring (will not exist on derivation chain). - * Rescanning must be invoked manually. - * @param {(String|Number)?} acct - * @param {WalletKey} ring - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ - -Wallet.prototype.importAddress = async function importAddress(acct, address) { - const unlock = await this.writeLock.lock(); - try { - return await this._importAddress(acct, address); - } finally { - unlock(); - } -}; - -/** - * Import a keyring (will not exist on derivation chain) without a lock. - * @private - * @param {(String|Number)?} acct - * @param {WalletKey} ring - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ - -Wallet.prototype._importAddress = async function _importAddress(acct, address) { - if (!this.watchOnly) - throw new Error('Cannot import address into non watch-only wallet.'); - - if (await this.getPath(address)) - throw new Error('Address already exists.'); - - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - if (account.type !== Account.types.PUBKEYHASH) - throw new Error('Cannot import into non-pkh account.'); - - const path = Path.fromAddress(account, address); - - const b = this.db.batch(); - await account.savePath(b, path); - await b.write(); -}; - -/** - * Fill a transaction with inputs, estimate - * transaction size, calculate fee, and add a change output. - * @see MTX#selectCoins - * @see MTX#fill - * @param {MTX} mtx - _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 - * kilobyte for fee calculation. - * See {@link TX#getMinFee} vs. {@link TX#getRoundFee}. - * @param {Rate} options.rate - Rate used for fee calculation. - * @param {Boolean} options.confirmed - Select only confirmed coins. - * @param {Boolean} options.free - Do not apply a fee if the - * transaction priority is high enough to be considered free. - * @param {Amount?} options.hardFee - Use a hard fee rather than - * calculating one. - * @param {Number|Boolean} options.subtractFee - Whether to subtract the - * fee from existing outputs rather than adding more inputs. - */ - -Wallet.prototype.fund = async function fund(mtx, options, force) { - const unlock = await this.fundLock.lock(force); - try { - return await this._fund(mtx, options); - } finally { - unlock(); - } -}; - -/** - * Fill a transaction with inputs without a lock. - * @private - * @see MTX#selectCoins - * @see MTX#fill - */ - -Wallet.prototype._fund = async function _fund(mtx, options) { - if (!options) - options = {}; - - if (!this.initialized) - throw new Error('Wallet is not initialized.'); - - if (this.watchOnly) - throw new Error('Cannot fund from watch-only wallet.'); - - const acct = options.account || 0; - const change = await this.changeAddress(acct); - - if (!change) - throw new Error('Account not found.'); - - let rate = options.rate; - if (rate == null) - rate = await this.wdb.estimateFee(options.blocks); - - let coins; - if (options.smart) { - coins = await this.getSmartCoins(options.account); - } else { - coins = await this.getCoins(options.account); - coins = this.txdb.filterLocked(coins); + createReceive(acct = 0) { + return this.createKey(acct, 0); } - await mtx.fund(coins, { - selection: options.selection, - round: options.round, - depth: options.depth, - hardFee: options.hardFee, - subtractFee: options.subtractFee, - subtractIndex: options.subtractIndex, - changeAddress: change, - height: this.wdb.state.height, - rate: rate, - maxFee: options.maxFee, - estimate: prev => this.estimateSize(prev) - }); + /** + * Create a new change address (increments receiveDepth). + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. + */ - assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); -}; + createChange(acct = 0) { + return this.createKey(acct, 1); + } -/** - * Get account by address. - * @param {Address} address - * @returns {Account} - */ + /** + * Create a new nested address (increments receiveDepth). + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. + */ -Wallet.prototype.getAccountByAddress = async function getAccountByAddress(address) { - const hash = Address.getHash(address, 'hex'); - const path = await this.getPath(hash); + createNested(acct = 0) { + return this.createKey(acct, 2); + } - if (!path) - return null; + /** + * Create a new address (increments depth). + * @param {(Number|String)?} acct + * @param {Number} branch + * @returns {Promise} - Returns {@link WalletKey}. + */ - return this.getAccount(path.account); -}; - -/** - * Input size estimator for max possible tx size. - * @param {Script} prev - * @returns {Number} - */ - -Wallet.prototype.estimateSize = async function estimateSize(prev) { - const scale = consensus.WITNESS_SCALE_FACTOR; - const address = prev.getAddress(); - - if (!address) - return -1; - - const account = await this.getAccountByAddress(address); - - if (!account) - return -1; - - let size = 0; - - if (prev.isScripthash()) { - // Nested bullshit. - if (account.witness) { - switch (account.type) { - case Account.types.PUBKEYHASH: - size += 23; // redeem script - size *= 4; // vsize - break; - case Account.types.MULTISIG: - size += 35; // redeem script - size *= 4; // vsize - break; - } + async createKey(acct, branch) { + const unlock = await this.writeLock.lock(); + try { + return await this._createKey(acct, branch); + } finally { + unlock(); } } - switch (account.type) { - case Account.types.PUBKEYHASH: - // P2PKH - // OP_PUSHDATA0 [signature] - size += 1 + 73; - // OP_PUSHDATA0 [key] - size += 1 + 33; - break; - case Account.types.MULTISIG: - // P2SH Multisig - // OP_0 - size += 1; - // OP_PUSHDATA0 [signature] ... - size += (1 + 73) * account.m; - // OP_PUSHDATA2 [redeem] - size += 3; - // m value - size += 1; - // OP_PUSHDATA0 [key] ... - size += (1 + 33) * account.n; - // n value - size += 1; - // OP_CHECKMULTISIG - size += 1; - break; + /** + * Create a new address (increments depth) without a lock. + * @private + * @param {(Number|String)?} acct + * @param {Number} branche + * @returns {Promise} - Returns {@link WalletKey}. + */ + + async _createKey(acct, branch) { + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + const b = this.db.batch(); + const key = await account.createKey(b, branch); + await b.write(); + + return key; } - if (account.witness) { - // Varint witness items length. - size += 1; - // Calculate vsize if - // we're a witness program. - size = (size + scale - 1) / scale | 0; - } else { - // Byte for varint - // size of input script. - size += encoding.sizeVarint(size); + /** + * Save the wallet to the database. Necessary + * when address depth and keys change. + * @returns {Promise} + */ + + save(b) { + return this.wdb.save(b, this); } - return size; -}; + /** + * Test whether the wallet possesses an address. + * @param {Address|Hash} address + * @returns {Promise} - Returns Boolean. + */ -/** - * Build a transaction, fill it with outputs and inputs, - * sort the members according to BIP69 (set options.sort=false - * to avoid sorting), set locktime, and template it. - * @param {Object} options - See {@link Wallet#fund options}. - * @param {Object[]} options.outputs - See {@link MTX#addOutput}. - * @returns {Promise} - Returns {@link MTX}. - */ + async hasAddress(address) { + const hash = Address.getHash(address, 'hex'); + const path = await this.getPath(hash); + return path != null; + } -Wallet.prototype.createTX = async function createTX(options, force) { - const outputs = options.outputs; - const mtx = new MTX(); + /** + * Get path by address hash. + * @param {Address|Hash} address + * @returns {Promise} - Returns {@link Path}. + */ - assert(Array.isArray(outputs), 'Outputs must be an array.'); - assert(outputs.length > 0, 'No outputs available.'); + async getPath(address) { + const hash = Address.getHash(address, 'hex'); + return this.wdb.getPath(this.wid, hash); + } - // Add the outputs - for (const obj of outputs) { - const output = new Output(obj); - const addr = output.getAddress(); + /** + * Get path by address hash (without account name). + * @private + * @param {Address|Hash} address + * @returns {Promise} - Returns {@link Path}. + */ - if (output.isDust()) - throw new Error('Output is dust.'); + async readPath(address) { + const hash = Address.getHash(address, 'hex'); + return this.wdb.readPath(this.wid, hash); + } - if (output.value > 0) { - if (!addr) - throw new Error('Cannot send to unknown address.'); + /** + * Test whether the wallet contains a path. + * @param {Address|Hash} address + * @returns {Promise} - Returns {Boolean}. + */ - if (addr.isNull()) - throw new Error('Cannot send to null address.'); + async hasPath(address) { + const hash = Address.getHash(address, 'hex'); + return this.wdb.hasPath(this.wid, hash); + } + + /** + * Get all wallet paths. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link Path}. + */ + + async getPaths(acct) { + if (acct != null) + return this.getAccountPaths(acct); + + return this.wdb.getWalletPaths(this.wid); + } + + /** + * Get all account paths. + * @param {String|Number} acct + * @returns {Promise} - Returns {@link Path}. + */ + + async getAccountPaths(acct) { + const index = await this.getAccountIndex(acct); + + if (index === -1) + throw new Error('Account not found.'); + + const hashes = await this.getAccountHashes(index); + const name = await this.getAccountName(acct); + + assert(name); + + const result = []; + + for (const hash of hashes) { + const path = await this.readPath(hash); + + assert(path); + assert(path.account === index); + + path.name = name; + + result.push(path); } - mtx.outputs.push(output); + return result; } - // Fill the inputs with unspents - await this.fund(mtx, options, force); + /** + * Import a keyring (will not exist on derivation chain). + * Rescanning must be invoked manually. + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ - // Sort members a la BIP69 - if (options.sort !== false) - mtx.sortMembers(); - - // Set the locktime to target value. - if (options.locktime != null) - mtx.setLocktime(options.locktime); - - // Consensus sanity checks. - assert(mtx.isSane(), 'TX failed sanity check.'); - assert(mtx.verifyInputs(this.wdb.state.height + 1), - 'TX failed context check.'); - - const total = await this.template(mtx); - - if (total === 0) - throw new Error('Templating failed.'); - - return mtx; -}; - -/** - * Build a transaction, fill it with outputs and inputs, - * sort the members according to BIP69, set locktime, - * sign and broadcast. Doing this all in one go prevents - * coins from being double spent. - * @param {Object} options - See {@link Wallet#fund options}. - * @param {Object[]} options.outputs - See {@link MTX#addOutput}. - * @returns {Promise} - Returns {@link TX}. - */ - -Wallet.prototype.send = async function send(options, passphrase) { - const unlock = await this.fundLock.lock(); - try { - return await this._send(options, passphrase); - } finally { - unlock(); - } -}; - -/** - * Build and send a transaction without a lock. - * @private - * @param {Object} options - See {@link Wallet#fund options}. - * @param {Object[]} options.outputs - See {@link MTX#addOutput}. - * @returns {Promise} - Returns {@link TX}. - */ - -Wallet.prototype._send = async function _send(options, passphrase) { - const mtx = await this.createTX(options, true); - - await this.sign(mtx, passphrase); - - if (!mtx.isSigned()) - throw new Error('TX could not be fully signed.'); - - const tx = mtx.toTX(); - - // Policy sanity checks. - if (tx.getSigopsCost(mtx.view) > policy.MAX_TX_SIGOPS_COST) - throw new Error('TX exceeds policy sigops.'); - - if (tx.getWeight() > policy.MAX_TX_WEIGHT) - throw new Error('TX exceeds policy weight.'); - - await this.wdb.addTX(tx); - - this.logger.debug('Sending wallet tx (%s): %s', this.id, tx.txid()); - - await this.wdb.send(tx); - - return tx; -}; - -/** - * Intentionally double-spend outputs by - * increasing fee for an existing transaction. - * @param {Hash} hash - * @param {Rate} rate - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - Returns {@link TX}. - */ - -Wallet.prototype.increaseFee = async function increaseFee(hash, rate, passphrase) { - assert((rate >>> 0) === rate, 'Rate must be a number.'); - - const wtx = await this.getTX(hash); - - if (!wtx) - throw new Error('Transaction not found.'); - - if (wtx.height !== -1) - throw new Error('Transaction is confirmed.'); - - const tx = wtx.tx; - - if (tx.isCoinbase()) - throw new Error('Transaction is a coinbase.'); - - const view = await this.getSpentView(tx); - - if (!tx.hasCoins(view)) - throw new Error('Not all coins available.'); - - const oldFee = tx.getFee(view); - - let fee = tx.getMinFee(null, rate); - - if (fee > MTX.Selector.MAX_FEE) - fee = MTX.Selector.MAX_FEE; - - if (oldFee >= fee) - throw new Error('Fee is not increasing.'); - - const mtx = MTX.fromTX(tx); - mtx.view = view; - - for (const input of mtx.inputs) { - input.script.clear(); - input.witness.clear(); + async importKey(acct, ring, passphrase) { + const unlock = await this.writeLock.lock(); + try { + return await this._importKey(acct, ring, passphrase); + } finally { + unlock(); + } } - let change; - for (let i = 0; i < mtx.outputs.length; i++) { - const output = mtx.outputs[i]; - const addr = output.getAddress(); + /** + * Import a keyring (will not exist on derivation chain) without a lock. + * @private + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ - if (!addr) - continue; + async _importKey(acct, ring, passphrase) { + if (!this.watchOnly) { + if (!ring.privateKey) + throw new Error('Cannot import pubkey into non watch-only wallet.'); + } else { + if (ring.privateKey) + throw new Error('Cannot import privkey into watch-only wallet.'); + } - const path = await this.getPath(addr); + const hash = ring.getHash('hex'); + + if (await this.getPath(hash)) + throw new Error('Key already exists.'); + + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + if (account.type !== Account.types.PUBKEYHASH) + throw new Error('Cannot import into non-pkh account.'); + + await this.unlock(passphrase); + + const key = WalletKey.fromRing(account, ring); + const path = key.toPath(); + + if (this.master.encrypted) { + path.data = this.master.encipher(path.data, path.hash); + assert(path.data); + path.encrypted = true; + } + + const b = this.db.batch(); + await account.savePath(b, path); + await b.write(); + } + + /** + * Import a keyring (will not exist on derivation chain). + * Rescanning must be invoked manually. + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + + async importAddress(acct, address) { + const unlock = await this.writeLock.lock(); + try { + return await this._importAddress(acct, address); + } finally { + unlock(); + } + } + + /** + * Import a keyring (will not exist on derivation chain) without a lock. + * @private + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + + async _importAddress(acct, address) { + if (!this.watchOnly) + throw new Error('Cannot import address into non watch-only wallet.'); + + if (await this.getPath(address)) + throw new Error('Address already exists.'); + + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + if (account.type !== Account.types.PUBKEYHASH) + throw new Error('Cannot import into non-pkh account.'); + + const path = Path.fromAddress(account, address); + + const b = this.db.batch(); + await account.savePath(b, path); + await b.write(); + } + + /** + * Fill a transaction with inputs, estimate + * transaction size, calculate fee, and add a change output. + * @see MTX#selectCoins + * @see MTX#fill + * @param {MTX} mtx - _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 + * kilobyte for fee calculation. + * See {@link TX#getMinFee} vs. {@link TX#getRoundFee}. + * @param {Rate} options.rate - Rate used for fee calculation. + * @param {Boolean} options.confirmed - Select only confirmed coins. + * @param {Boolean} options.free - Do not apply a fee if the + * transaction priority is high enough to be considered free. + * @param {Amount?} options.hardFee - Use a hard fee rather than + * calculating one. + * @param {Number|Boolean} options.subtractFee - Whether to subtract the + * fee from existing outputs rather than adding more inputs. + */ + + async fund(mtx, options, force) { + const unlock = await this.fundLock.lock(force); + try { + return await this._fund(mtx, options); + } finally { + unlock(); + } + } + + /** + * Fill a transaction with inputs without a lock. + * @private + * @see MTX#selectCoins + * @see MTX#fill + */ + + async _fund(mtx, options) { + if (!options) + options = {}; + + if (!this.initialized) + throw new Error('Wallet is not initialized.'); + + if (this.watchOnly) + throw new Error('Cannot fund from watch-only wallet.'); + + const acct = options.account || 0; + const change = await this.changeAddress(acct); + + if (!change) + throw new Error('Account not found.'); + + let rate = options.rate; + if (rate == null) + rate = await this.wdb.estimateFee(options.blocks); + + let coins; + if (options.smart) { + coins = await this.getSmartCoins(options.account); + } else { + coins = await this.getCoins(options.account); + coins = this.txdb.filterLocked(coins); + } + + await mtx.fund(coins, { + selection: options.selection, + round: options.round, + depth: options.depth, + hardFee: options.hardFee, + subtractFee: options.subtractFee, + subtractIndex: options.subtractIndex, + changeAddress: change, + height: this.wdb.state.height, + rate: rate, + maxFee: options.maxFee, + estimate: prev => this.estimateSize(prev) + }); + + assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); + } + + /** + * Get account by address. + * @param {Address} address + * @returns {Account} + */ + + async getAccountByAddress(address) { + const hash = Address.getHash(address, 'hex'); + const path = await this.getPath(hash); if (!path) - continue; + return null; - if (path.branch === 1) { - change = output; - mtx.changeIndex = i; - break; + return this.getAccount(path.account); + } + + /** + * Input size estimator for max possible tx size. + * @param {Script} prev + * @returns {Number} + */ + + async estimateSize(prev) { + const scale = consensus.WITNESS_SCALE_FACTOR; + const address = prev.getAddress(); + + if (!address) + return -1; + + const account = await this.getAccountByAddress(address); + + if (!account) + return -1; + + let size = 0; + + if (prev.isScripthash()) { + // Nested bullshit. + if (account.witness) { + switch (account.type) { + case Account.types.PUBKEYHASH: + size += 23; // redeem script + size *= 4; // vsize + break; + case Account.types.MULTISIG: + size += 35; // redeem script + size *= 4; // vsize + break; + } + } + } + + switch (account.type) { + case Account.types.PUBKEYHASH: + // P2PKH + // OP_PUSHDATA0 [signature] + size += 1 + 73; + // OP_PUSHDATA0 [key] + size += 1 + 33; + break; + case Account.types.MULTISIG: + // P2SH Multisig + // OP_0 + size += 1; + // OP_PUSHDATA0 [signature] ... + size += (1 + 73) * account.m; + // OP_PUSHDATA2 [redeem] + size += 3; + // m value + size += 1; + // OP_PUSHDATA0 [key] ... + size += (1 + 33) * account.n; + // n value + size += 1; + // OP_CHECKMULTISIG + size += 1; + break; + } + + if (account.witness) { + // Varint witness items length. + size += 1; + // Calculate vsize if + // we're a witness program. + size = (size + scale - 1) / scale | 0; + } else { + // Byte for varint + // size of input script. + size += encoding.sizeVarint(size); + } + + return size; + } + + /** + * Build a transaction, fill it with outputs and inputs, + * sort the members according to BIP69 (set options.sort=false + * to avoid sorting), set locktime, and template it. + * @param {Object} options - See {@link Wallet#fund options}. + * @param {Object[]} options.outputs - See {@link MTX#addOutput}. + * @returns {Promise} - Returns {@link MTX}. + */ + + async createTX(options, force) { + const outputs = options.outputs; + const mtx = new MTX(); + + assert(Array.isArray(outputs), 'Outputs must be an array.'); + assert(outputs.length > 0, 'No outputs available.'); + + // Add the outputs + for (const obj of outputs) { + const output = new Output(obj); + const addr = output.getAddress(); + + if (output.isDust()) + throw new Error('Output is dust.'); + + if (output.value > 0) { + if (!addr) + throw new Error('Cannot send to unknown address.'); + + if (addr.isNull()) + throw new Error('Cannot send to null address.'); + } + + mtx.outputs.push(output); + } + + // Fill the inputs with unspents + await this.fund(mtx, options, force); + + // Sort members a la BIP69 + if (options.sort !== false) + mtx.sortMembers(); + + // Set the locktime to target value. + if (options.locktime != null) + mtx.setLocktime(options.locktime); + + // Consensus sanity checks. + assert(mtx.isSane(), 'TX failed sanity check.'); + assert(mtx.verifyInputs(this.wdb.state.height + 1), + 'TX failed context check.'); + + const total = await this.template(mtx); + + if (total === 0) + throw new Error('Templating failed.'); + + return mtx; + } + + /** + * Build a transaction, fill it with outputs and inputs, + * sort the members according to BIP69, set locktime, + * sign and broadcast. Doing this all in one go prevents + * coins from being double spent. + * @param {Object} options - See {@link Wallet#fund options}. + * @param {Object[]} options.outputs - See {@link MTX#addOutput}. + * @returns {Promise} - Returns {@link TX}. + */ + + async send(options, passphrase) { + const unlock = await this.fundLock.lock(); + try { + return await this._send(options, passphrase); + } finally { + unlock(); } } - if (!change) - throw new Error('No change output.'); + /** + * Build and send a transaction without a lock. + * @private + * @param {Object} options - See {@link Wallet#fund options}. + * @param {Object[]} options.outputs - See {@link MTX#addOutput}. + * @returns {Promise} - Returns {@link TX}. + */ - change.value += oldFee; + async _send(options, passphrase) { + const mtx = await this.createTX(options, true); - if (mtx.getFee() !== 0) - throw new Error('Arithmetic error for change.'); + await this.sign(mtx, passphrase); - change.value -= fee; + if (!mtx.isSigned()) + throw new Error('TX could not be fully signed.'); - if (change.value < 0) - throw new Error('Fee is too high.'); + const tx = mtx.toTX(); - if (change.isDust()) { - mtx.outputs.splice(mtx.changeIndex, 1); - mtx.changeIndex = -1; - } + // Policy sanity checks. + if (tx.getSigopsCost(mtx.view) > policy.MAX_TX_SIGOPS_COST) + throw new Error('TX exceeds policy sigops.'); - await this.sign(mtx, passphrase); + if (tx.getWeight() > policy.MAX_TX_WEIGHT) + throw new Error('TX exceeds policy weight.'); - if (!mtx.isSigned()) - throw new Error('TX could not be fully signed.'); + await this.wdb.addTX(tx); - const ntx = mtx.toTX(); + this.logger.debug('Sending wallet tx (%s): %s', this.id, tx.txid()); - this.logger.debug( - 'Increasing fee for wallet tx (%s): %s', - this.id, ntx.txid()); - - await this.wdb.addTX(ntx); - await this.wdb.send(ntx); - - return ntx; -}; - -/** - * Resend pending wallet transactions. - * @returns {Promise} - */ - -Wallet.prototype.resend = async function resend() { - const wtxs = await this.getPending(); - - if (wtxs.length > 0) - this.logger.info('Rebroadcasting %d transactions.', wtxs.length); - - const txs = []; - - for (const wtx of wtxs) - txs.push(wtx.tx); - - const sorted = common.sortDeps(txs); - - for (const tx of sorted) await this.wdb.send(tx); - return txs; -}; + return tx; + } -/** - * Derive necessary addresses for signing a transaction. - * @param {MTX} mtx - * @param {Number?} index - Input index. - * @returns {Promise} - Returns {@link WalletKey}[]. - */ + /** + * Intentionally double-spend outputs by + * increasing fee for an existing transaction. + * @param {Hash} hash + * @param {Rate} rate + * @param {(String|Buffer)?} passphrase + * @returns {Promise} - Returns {@link TX}. + */ -Wallet.prototype.deriveInputs = async function deriveInputs(mtx) { - assert(mtx.mutable); + async increaseFee(hash, rate, passphrase) { + assert((rate >>> 0) === rate, 'Rate must be a number.'); - const paths = await this.getInputPaths(mtx); - const rings = []; + const wtx = await this.getTX(hash); + + if (!wtx) + throw new Error('Transaction not found.'); + + if (wtx.height !== -1) + throw new Error('Transaction is confirmed.'); + + const tx = wtx.tx; + + if (tx.isCoinbase()) + throw new Error('Transaction is a coinbase.'); + + const view = await this.getSpentView(tx); + + if (!tx.hasCoins(view)) + throw new Error('Not all coins available.'); + + const oldFee = tx.getFee(view); + + let fee = tx.getMinFee(null, rate); + + if (fee > MTX.Selector.MAX_FEE) + fee = MTX.Selector.MAX_FEE; + + if (oldFee >= fee) + throw new Error('Fee is not increasing.'); + + const mtx = MTX.fromTX(tx); + mtx.view = view; + + for (const input of mtx.inputs) { + input.script.clear(); + input.witness.clear(); + } + + let change; + for (let i = 0; i < mtx.outputs.length; i++) { + const output = mtx.outputs[i]; + const addr = output.getAddress(); + + if (!addr) + continue; + + const path = await this.getPath(addr); + + if (!path) + continue; + + if (path.branch === 1) { + change = output; + mtx.changeIndex = i; + break; + } + } + + if (!change) + throw new Error('No change output.'); + + change.value += oldFee; + + if (mtx.getFee() !== 0) + throw new Error('Arithmetic error for change.'); + + change.value -= fee; + + if (change.value < 0) + throw new Error('Fee is too high.'); + + if (change.isDust()) { + mtx.outputs.splice(mtx.changeIndex, 1); + mtx.changeIndex = -1; + } + + await this.sign(mtx, passphrase); + + if (!mtx.isSigned()) + throw new Error('TX could not be fully signed.'); + + const ntx = mtx.toTX(); + + this.logger.debug( + 'Increasing fee for wallet tx (%s): %s', + this.id, ntx.txid()); + + await this.wdb.addTX(ntx); + await this.wdb.send(ntx); + + return ntx; + } + + /** + * Resend pending wallet transactions. + * @returns {Promise} + */ + + async resend() { + const wtxs = await this.getPending(); + + if (wtxs.length > 0) + this.logger.info('Rebroadcasting %d transactions.', wtxs.length); + + const txs = []; + + for (const wtx of wtxs) + txs.push(wtx.tx); + + const sorted = common.sortDeps(txs); + + for (const tx of sorted) + await this.wdb.send(tx); + + return txs; + } + + /** + * Derive necessary addresses for signing a transaction. + * @param {MTX} mtx + * @param {Number?} index - Input index. + * @returns {Promise} - Returns {@link WalletKey}[]. + */ + + async deriveInputs(mtx) { + assert(mtx.mutable); + + const paths = await this.getInputPaths(mtx); + const rings = []; + + for (const path of paths) { + const account = await this.getAccount(path.account); + + if (!account) + continue; + + const ring = account.derivePath(path, this.master); + + if (ring) + rings.push(ring); + } + + return rings; + } + + /** + * Retrieve a single keyring by address. + * @param {Address|Hash} hash + * @returns {Promise} + */ + + async getKey(address) { + const hash = Address.getHash(address, 'hex'); + const path = await this.getPath(hash); + + if (!path) + return null; - for (const path of paths) { const account = await this.getAccount(path.account); if (!account) - continue; + return null; - const ring = account.derivePath(path, this.master); - - if (ring) - rings.push(ring); + return account.derivePath(path, this.master); } - return rings; -}; + /** + * Retrieve a single keyring by address + * (with the private key reference). + * @param {Address|Hash} hash + * @param {(Buffer|String)?} passphrase + * @returns {Promise} + */ -/** - * Retrieve a single keyring by address. - * @param {Address|Hash} hash - * @returns {Promise} - */ - -Wallet.prototype.getKey = async function getKey(address) { - const hash = Address.getHash(address, 'hex'); - const path = await this.getPath(hash); - - if (!path) - return null; - - const account = await this.getAccount(path.account); - - if (!account) - return null; - - return account.derivePath(path, this.master); -}; - -/** - * Retrieve a single keyring by address - * (with the private key reference). - * @param {Address|Hash} hash - * @param {(Buffer|String)?} passphrase - * @returns {Promise} - */ - -Wallet.prototype.getPrivateKey = async function getPrivateKey(address, passphrase) { - const hash = Address.getHash(address, 'hex'); - const path = await this.getPath(hash); - - if (!path) - return null; - - const account = await this.getAccount(path.account); - - if (!account) - return null; - - await this.unlock(passphrase); - - const key = account.derivePath(path, this.master); - - if (!key.privateKey) - return null; - - return key; -}; - -/** - * Map input addresses to paths. - * @param {MTX} mtx - * @returns {Promise} - Returns {@link Path}[]. - */ - -Wallet.prototype.getInputPaths = async function getInputPaths(mtx) { - assert(mtx.mutable); - - if (!mtx.hasCoins()) - throw new Error('Not all coins available.'); - - const hashes = mtx.getInputHashes('hex'); - const paths = []; - - for (const hash of hashes) { + async getPrivateKey(address, passphrase) { + const hash = Address.getHash(address, 'hex'); const path = await this.getPath(hash); - if (path) - paths.push(path); - } - - return paths; -}; - -/** - * Map output addresses to paths. - * @param {TX} tx - * @returns {Promise} - Returns {@link Path}[]. - */ - -Wallet.prototype.getOutputPaths = async function getOutputPaths(tx) { - const paths = []; - const hashes = tx.getOutputHashes('hex'); - - for (const hash of hashes) { - const path = await this.getPath(hash); - if (path) - paths.push(path); - } - - return paths; -}; - -/** - * Increase lookahead for account. - * @param {(Number|String)?} account - * @param {Number} lookahead - * @returns {Promise} - */ - -Wallet.prototype.setLookahead = async function setLookahead(acct, lookahead) { - const unlock = await this.writeLock.lock(); - try { - return this._setLookahead(acct, lookahead); - } finally { - unlock(); - } -}; - -/** - * Increase lookahead for account (without a lock). - * @private - * @param {(Number|String)?} account - * @param {Number} lookahead - * @returns {Promise} - */ - -Wallet.prototype._setLookahead = async function _setLookahead(acct, lookahead) { - const account = await this.getAccount(acct); - - if (!account) - throw new Error('Account not found.'); - - const b = this.db.batch(); - await account.setLookahead(b, lookahead); - await b.write(); -}; - -/** - * 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 {Promise} - */ - -Wallet.prototype.syncOutputDepth = async function syncOutputDepth(tx) { - const map = new Map(); - - for (const hash of tx.getOutputHashes('hex')) { - const path = await this.readPath(hash); if (!path) - continue; + return null; - if (path.index === -1) - continue; + const account = await this.getAccount(path.account); - if (!map.has(path.account)) - map.set(path.account, []); + if (!account) + return null; - map.get(path.account).push(path); + await this.unlock(passphrase); + + const key = account.derivePath(path, this.master); + + if (!key.privateKey) + return null; + + return key; } - const derived = []; - const b = this.db.batch(); + /** + * Map input addresses to paths. + * @param {MTX} mtx + * @returns {Promise} - Returns {@link Path}[]. + */ - for (const [acct, paths] of map) { - let receive = -1; - let change = -1; - let nested = -1; + async getInputPaths(mtx) { + assert(mtx.mutable); - for (const path of paths) { - switch (path.branch) { - case 0: - if (path.index > receive) - receive = path.index; - break; - case 1: - if (path.index > change) - change = path.index; - break; - case 2: - if (path.index > nested) - nested = path.index; - break; + if (!mtx.hasCoins()) + throw new Error('Not all coins available.'); + + const hashes = mtx.getInputHashes('hex'); + const paths = []; + + for (const hash of hashes) { + const path = await this.getPath(hash); + if (path) + paths.push(path); + } + + return paths; + } + + /** + * Map output addresses to paths. + * @param {TX} tx + * @returns {Promise} - Returns {@link Path}[]. + */ + + async getOutputPaths(tx) { + const paths = []; + const hashes = tx.getOutputHashes('hex'); + + for (const hash of hashes) { + const path = await this.getPath(hash); + if (path) + paths.push(path); + } + + return paths; + } + + /** + * Increase lookahead for account. + * @param {(Number|String)?} account + * @param {Number} lookahead + * @returns {Promise} + */ + + async setLookahead(acct, lookahead) { + const unlock = await this.writeLock.lock(); + try { + return this._setLookahead(acct, lookahead); + } finally { + unlock(); + } + } + + /** + * Increase lookahead for account (without a lock). + * @private + * @param {(Number|String)?} account + * @param {Number} lookahead + * @returns {Promise} + */ + + async _setLookahead(acct, lookahead) { + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + const b = this.db.batch(); + await account.setLookahead(b, lookahead); + await b.write(); + } + + /** + * 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 {Promise} + */ + + async syncOutputDepth(tx) { + const map = new Map(); + + for (const hash of tx.getOutputHashes('hex')) { + const path = await this.readPath(hash); + + if (!path) + continue; + + if (path.index === -1) + continue; + + if (!map.has(path.account)) + map.set(path.account, []); + + map.get(path.account).push(path); + } + + const derived = []; + const b = this.db.batch(); + + for (const [acct, paths] of map) { + let receive = -1; + let change = -1; + let nested = -1; + + for (const path of paths) { + switch (path.branch) { + case 0: + if (path.index > receive) + receive = path.index; + break; + case 1: + if (path.index > change) + change = path.index; + break; + case 2: + if (path.index > nested) + nested = path.index; + break; + } + } + + receive += 2; + change += 2; + nested += 2; + + const account = await this.getAccount(acct); + assert(account); + + const ring = await account.syncDepth(b, receive, change, nested); + + if (ring) + derived.push(ring); + } + + await b.write(); + + return derived; + } + + /** + * Build input scripts templates for a transaction (does not + * sign, only creates signature slots). Only builds scripts + * for inputs that are redeemable by this wallet. + * @param {MTX} mtx + * @returns {Promise} - Returns Number + * (total number of scripts built). + */ + + async template(mtx) { + const rings = await this.deriveInputs(mtx); + return mtx.template(rings); + } + + /** + * Build input scripts and sign inputs for a transaction. Only attempts + * to build/sign inputs that are redeemable by this wallet. + * @param {MTX} tx + * @param {Object|String|Buffer} options - Options or passphrase. + * @returns {Promise} - Returns Number (total number + * of inputs scripts built and signed). + */ + + async sign(mtx, passphrase) { + if (this.watchOnly) + throw new Error('Cannot sign from a watch-only wallet.'); + + await this.unlock(passphrase); + + const rings = await this.deriveInputs(mtx); + + return mtx.signAsync(rings, Script.hashType.ALL, this.wdb.workers); + } + + /** + * Get a coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + getCoinView(tx) { + return this.txdb.getCoinView(tx); + } + + /** + * Get a historical coin viewpoint. + * @param {TX} tx + * @returns {Promise} - Returns {@link CoinView}. + */ + + getSpentView(tx) { + return this.txdb.getSpentView(tx); + } + + /** + * Convert transaction to transaction details. + * @param {TXRecord} wtx + * @returns {Promise} - Returns {@link Details}. + */ + + toDetails(wtx) { + return this.txdb.toDetails(wtx); + } + + /** + * Get transaction details. + * @param {Hash} hash + * @returns {Promise} - Returns {@link Details}. + */ + + getDetails(hash) { + return this.txdb.getDetails(hash); + } + + /** + * Get a coin from the wallet. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + getCoin(hash, index) { + return this.txdb.getCoin(hash, index); + } + + /** + * Get a transaction from the wallet. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + getTX(hash) { + return this.txdb.getTX(hash); + } + + /** + * List blocks for the wallet. + * @returns {Promise} - Returns {@link BlockRecord}. + */ + + getBlocks() { + return this.txdb.getBlocks(); + } + + /** + * Get a block from the wallet. + * @param {Number} height + * @returns {Promise} - Returns {@link BlockRecord}. + */ + + getBlock(height) { + return this.txdb.getBlock(height); + } + + /** + * Add a transaction to the wallets TX history. + * @param {TX} tx + * @returns {Promise} + */ + + async add(tx, block) { + const unlock = await this.writeLock.lock(); + try { + return await this._add(tx, block); + } finally { + unlock(); + } + } + + /** + * Add a transaction to the wallet without a lock. + * Potentially resolves orphans. + * @private + * @param {TX} tx + * @returns {Promise} + */ + + async _add(tx, block) { + const details = await this.txdb.add(tx, block); + + if (details) { + const derived = await this.syncOutputDepth(tx); + if (derived.length > 0) { + this.wdb.emit('address', this, derived); + this.emit('address', derived); } } - receive += 2; - change += 2; - nested += 2; - - const account = await this.getAccount(acct); - assert(account); - - const ring = await account.syncDepth(b, receive, change, nested); - - if (ring) - derived.push(ring); + return details; } - await b.write(); + /** + * Revert a block. + * @param {Number} height + * @returns {Promise} + */ - return derived; -}; - -/** - * Build input scripts templates for a transaction (does not - * sign, only creates signature slots). Only builds scripts - * for inputs that are redeemable by this wallet. - * @param {MTX} mtx - * @returns {Promise} - Returns Number - * (total number of scripts built). - */ - -Wallet.prototype.template = async function template(mtx) { - const rings = await this.deriveInputs(mtx); - return mtx.template(rings); -}; - -/** - * Build input scripts and sign inputs for a transaction. Only attempts - * to build/sign inputs that are redeemable by this wallet. - * @param {MTX} tx - * @param {Object|String|Buffer} options - Options or passphrase. - * @returns {Promise} - Returns Number (total number - * of inputs scripts built and signed). - */ - -Wallet.prototype.sign = async function sign(mtx, passphrase) { - if (this.watchOnly) - throw new Error('Cannot sign from a watch-only wallet.'); - - await this.unlock(passphrase); - - const rings = await this.deriveInputs(mtx); - - return mtx.signAsync(rings, Script.hashType.ALL, this.wdb.workers); -}; - -/** - * Get a coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -Wallet.prototype.getCoinView = function getCoinView(tx) { - return this.txdb.getCoinView(tx); -}; - -/** - * Get a historical coin viewpoint. - * @param {TX} tx - * @returns {Promise} - Returns {@link CoinView}. - */ - -Wallet.prototype.getSpentView = function getSpentView(tx) { - return this.txdb.getSpentView(tx); -}; - -/** - * Convert transaction to transaction details. - * @param {TXRecord} wtx - * @returns {Promise} - Returns {@link Details}. - */ - -Wallet.prototype.toDetails = function toDetails(wtx) { - return this.txdb.toDetails(wtx); -}; - -/** - * Get transaction details. - * @param {Hash} hash - * @returns {Promise} - Returns {@link Details}. - */ - -Wallet.prototype.getDetails = function getDetails(hash) { - return this.txdb.getDetails(hash); -}; - -/** - * Get a coin from the wallet. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -Wallet.prototype.getCoin = function getCoin(hash, index) { - return this.txdb.getCoin(hash, index); -}; - -/** - * Get a transaction from the wallet. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ - -Wallet.prototype.getTX = function getTX(hash) { - return this.txdb.getTX(hash); -}; - -/** - * List blocks for the wallet. - * @returns {Promise} - Returns {@link BlockRecord}. - */ - -Wallet.prototype.getBlocks = function getBlocks() { - return this.txdb.getBlocks(); -}; - -/** - * Get a block from the wallet. - * @param {Number} height - * @returns {Promise} - Returns {@link BlockRecord}. - */ - -Wallet.prototype.getBlock = function getBlock(height) { - return this.txdb.getBlock(height); -}; - -/** - * Add a transaction to the wallets TX history. - * @param {TX} tx - * @returns {Promise} - */ - -Wallet.prototype.add = async function add(tx, block) { - const unlock = await this.writeLock.lock(); - try { - return await this._add(tx, block); - } finally { - unlock(); - } -}; - -/** - * Add a transaction to the wallet without a lock. - * Potentially resolves orphans. - * @private - * @param {TX} tx - * @returns {Promise} - */ - -Wallet.prototype._add = async function _add(tx, block) { - const details = await this.txdb.add(tx, block); - - if (details) { - const derived = await this.syncOutputDepth(tx); - if (derived.length > 0) { - this.wdb.emit('address', this, derived); - this.emit('address', derived); + async revert(height) { + const unlock = await this.writeLock.lock(); + try { + return await this.txdb.revert(height); + } finally { + unlock(); } } - return details; -}; + /** + * Remove a wallet transaction. + * @param {Hash} hash + * @returns {Promise} + */ -/** - * Revert a block. - * @param {Number} height - * @returns {Promise} - */ - -Wallet.prototype.revert = async function revert(height) { - const unlock = await this.writeLock.lock(); - try { - return await this.txdb.revert(height); - } finally { - unlock(); + async remove(hash) { + const unlock = await this.writeLock.lock(); + try { + return await this.txdb.remove(hash); + } finally { + unlock(); + } } -}; -/** - * Remove a wallet transaction. - * @param {Hash} hash - * @returns {Promise} - */ + /** + * Zap stale TXs from wallet. + * @param {(Number|String)?} acct + * @param {Number} age - Age threshold (unix time, default=72 hours). + * @returns {Promise} + */ -Wallet.prototype.remove = async function remove(hash) { - const unlock = await this.writeLock.lock(); - try { - return await this.txdb.remove(hash); - } finally { - unlock(); + async zap(acct, age) { + const unlock = await this.writeLock.lock(); + try { + return await this._zap(acct, age); + } finally { + unlock(); + } } -}; -/** - * Zap stale TXs from wallet. - * @param {(Number|String)?} acct - * @param {Number} age - Age threshold (unix time, default=72 hours). - * @returns {Promise} - */ + /** + * Zap stale TXs from wallet without a lock. + * @private + * @param {(Number|String)?} acct + * @param {Number} age + * @returns {Promise} + */ -Wallet.prototype.zap = async function zap(acct, age) { - const unlock = await this.writeLock.lock(); - try { - return await this._zap(acct, age); - } finally { - unlock(); + async _zap(acct, age) { + const account = await this.ensureIndex(acct); + return this.txdb.zap(account, age); } -}; -/** - * Zap stale TXs from wallet without a lock. - * @private - * @param {(Number|String)?} acct - * @param {Number} age - * @returns {Promise} - */ + /** + * Abandon transaction. + * @param {Hash} hash + * @returns {Promise} + */ -Wallet.prototype._zap = async function _zap(acct, age) { - const account = await this.ensureIndex(acct); - return this.txdb.zap(account, age); -}; - -/** - * Abandon transaction. - * @param {Hash} hash - * @returns {Promise} - */ - -Wallet.prototype.abandon = async function abandon(hash) { - const unlock = await this.writeLock.lock(); - try { - return await this._abandon(hash); - } finally { - unlock(); + async abandon(hash) { + const unlock = await this.writeLock.lock(); + try { + return await this._abandon(hash); + } finally { + unlock(); + } } -}; -/** - * Abandon transaction without a lock. - * @private - * @param {Hash} hash - * @returns {Promise} - */ + /** + * Abandon transaction without a lock. + * @private + * @param {Hash} hash + * @returns {Promise} + */ -Wallet.prototype._abandon = function _abandon(hash) { - return this.txdb.abandon(hash); -}; + _abandon(hash) { + return this.txdb.abandon(hash); + } -/** - * Lock a single coin. - * @param {Coin|Outpoint} coin - */ + /** + * Lock a single coin. + * @param {Coin|Outpoint} coin + */ -Wallet.prototype.lockCoin = function lockCoin(coin) { - return this.txdb.lockCoin(coin); -}; + lockCoin(coin) { + return this.txdb.lockCoin(coin); + } -/** - * Unlock a single coin. - * @param {Coin|Outpoint} coin - */ + /** + * Unlock a single coin. + * @param {Coin|Outpoint} coin + */ -Wallet.prototype.unlockCoin = function unlockCoin(coin) { - return this.txdb.unlockCoin(coin); -}; + unlockCoin(coin) { + return this.txdb.unlockCoin(coin); + } -/** - * Test locked status of a single coin. - * @param {Coin|Outpoint} coin - */ + /** + * Test locked status of a single coin. + * @param {Coin|Outpoint} coin + */ -Wallet.prototype.isLocked = function isLocked(coin) { - return this.txdb.isLocked(coin); -}; + isLocked(coin) { + return this.txdb.isLocked(coin); + } -/** - * Return an array of all locked outpoints. - * @returns {Outpoint[]} - */ + /** + * Return an array of all locked outpoints. + * @returns {Outpoint[]} + */ -Wallet.prototype.getLocked = function getLocked() { - return this.txdb.getLocked(); -}; + getLocked() { + return this.txdb.getLocked(); + } -/** - * Get all transactions in transaction history. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns {@link TX}[]. - */ + /** + * Get all transactions in transaction history. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link TX}[]. + */ -Wallet.prototype.getHistory = async function getHistory(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getHistory(account); -}; + async getHistory(acct) { + const account = await this.ensureIndex(acct); + return this.txdb.getHistory(account); + } -/** - * Get all available coins. - * @param {(String|Number)?} account - * @returns {Promise} - Returns {@link Coin}[]. - */ + /** + * Get all available coins. + * @param {(String|Number)?} account + * @returns {Promise} - Returns {@link Coin}[]. + */ -Wallet.prototype.getCoins = async function getCoins(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getCoins(account); -}; + async getCoins(acct) { + const account = await this.ensureIndex(acct); + return this.txdb.getCoins(account); + } -/** - * Get all available credits. - * @param {(String|Number)?} account - * @returns {Promise} - Returns {@link Credit}[]. - */ + /** + * Get all available credits. + * @param {(String|Number)?} account + * @returns {Promise} - Returns {@link Credit}[]. + */ -Wallet.prototype.getCredits = async function getCredits(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getCredits(account); -}; + async getCredits(acct) { + const account = await this.ensureIndex(acct); + return this.txdb.getCredits(account); + } -/** - * Get "smart" coins. - * @param {(String|Number)?} account - * @returns {Promise} - Returns {@link Coin}[]. - */ + /** + * Get "smart" coins. + * @param {(String|Number)?} account + * @returns {Promise} - Returns {@link Coin}[]. + */ -Wallet.prototype.getSmartCoins = async function getSmartCoins(acct) { - const credits = await this.getCredits(acct); - const coins = []; + async getSmartCoins(acct) { + const credits = await this.getCredits(acct); + const coins = []; - for (const credit of credits) { - const coin = credit.coin; + for (const credit of credits) { + const coin = credit.coin; - if (credit.spent) - continue; + if (credit.spent) + continue; - if (this.txdb.isLocked(coin)) - continue; + if (this.txdb.isLocked(coin)) + continue; + + // Always used confirmed coins. + if (coin.height !== -1) { + coins.push(coin); + continue; + } + + // Use unconfirmed only if they were + // created as a result of one of our + // _own_ transactions. i.e. they're + // not low-fee and not in danger of + // being double-spent by a bad actor. + if (!credit.own) + continue; - // Always used confirmed coins. - if (coin.height !== -1) { coins.push(coin); - continue; } - // Use unconfirmed only if they were - // created as a result of one of our - // _own_ transactions. i.e. they're - // not low-fee and not in danger of - // being double-spent by a bad actor. - if (!credit.own) - continue; - - coins.push(coin); + return coins; } - return coins; -}; + /** + * Get all pending/unconfirmed transactions. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link TX}[]. + */ -/** - * Get all pending/unconfirmed transactions. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns {@link TX}[]. - */ + async getPending(acct) { + const account = await this.ensureIndex(acct); + return this.txdb.getPending(account); + } -Wallet.prototype.getPending = async function getPending(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getPending(account); -}; + /** + * Get wallet balance. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link Balance}. + */ -/** - * Get wallet balance. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns {@link Balance}. - */ + async getBalance(acct) { + const account = await this.ensureIndex(acct); + return this.txdb.getBalance(account); + } -Wallet.prototype.getBalance = async function getBalance(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getBalance(account); -}; + /** + * Get a range of transactions between two timestamps. + * @param {(String|Number)?} acct + * @param {Object} options + * @param {Number} options.start + * @param {Number} options.end + * @returns {Promise} - Returns {@link TX}[]. + */ -/** - * Get a range of transactions between two timestamps. - * @param {(String|Number)?} acct - * @param {Object} options - * @param {Number} options.start - * @param {Number} options.end - * @returns {Promise} - Returns {@link TX}[]. - */ + async getRange(acct, options) { + const account = await this.ensureIndex(acct); + return this.txdb.getRange(account, options); + } -Wallet.prototype.getRange = async function getRange(acct, options) { - const account = await this.ensureIndex(acct); - return this.txdb.getRange(account, options); -}; + /** + * Get the last N transactions. + * @param {(String|Number)?} acct + * @param {Number} limit + * @returns {Promise} - Returns {@link TX}[]. + */ -/** - * Get the last N transactions. - * @param {(String|Number)?} acct - * @param {Number} limit - * @returns {Promise} - Returns {@link TX}[]. - */ + async getLast(acct, limit) { + const account = await this.ensureIndex(acct); + return this.txdb.getLast(account, limit); + } -Wallet.prototype.getLast = async function getLast(acct, limit) { - const account = await this.ensureIndex(acct); - return this.txdb.getLast(account, limit); -}; + /** + * Get account key. + * @param {Number} [acct=0] + * @returns {HDPublicKey} + */ -/** - * Get account key. - * @param {Number} [acct=0] - * @returns {HDPublicKey} - */ + async accountKey(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.accountKey; + } -Wallet.prototype.accountKey = async function accountKey(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.accountKey; -}; + /** + * Get current receive depth. + * @param {Number} [acct=0] + * @returns {Number} + */ -/** - * Get current receive depth. - * @param {Number} [acct=0] - * @returns {Number} - */ + async receiveDepth(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.receiveDepth; + } -Wallet.prototype.receiveDepth = async function receiveDepth(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.receiveDepth; -}; + /** + * Get current change depth. + * @param {Number} [acct=0] + * @returns {Number} + */ -/** - * Get current change depth. - * @param {Number} [acct=0] - * @returns {Number} - */ + async changeDepth(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.changeDepth; + } -Wallet.prototype.changeDepth = async function changeDepth(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.changeDepth; -}; + /** + * Get current nested depth. + * @param {Number} [acct=0] + * @returns {Number} + */ -/** - * Get current nested depth. - * @param {Number} [acct=0] - * @returns {Number} - */ + async nestedDepth(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.nestedDepth; + } -Wallet.prototype.nestedDepth = async function nestedDepth(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.nestedDepth; -}; + /** + * Get current receive address. + * @param {Number} [acct=0] + * @returns {Address} + */ -/** - * Get current receive address. - * @param {Number} [acct=0] - * @returns {Address} - */ + async receiveAddress(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.receiveAddress(); + } -Wallet.prototype.receiveAddress = async function receiveAddress(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.receiveAddress(); -}; + /** + * Get current change address. + * @param {Number} [acct=0] + * @returns {Address} + */ -/** - * Get current change address. - * @param {Number} [acct=0] - * @returns {Address} - */ + async changeAddress(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.changeAddress(); + } -Wallet.prototype.changeAddress = async function changeAddress(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.changeAddress(); -}; + /** + * Get current nested address. + * @param {Number} [acct=0] + * @returns {Address} + */ -/** - * Get current nested address. - * @param {Number} [acct=0] - * @returns {Address} - */ + async nestedAddress(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.nestedAddress(); + } -Wallet.prototype.nestedAddress = async function nestedAddress(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.nestedAddress(); -}; + /** + * Get current receive key. + * @param {Number} [acct=0] + * @returns {WalletKey} + */ -/** - * Get current receive key. - * @param {Number} [acct=0] - * @returns {WalletKey} - */ + async receiveKey(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.receiveKey(); + } -Wallet.prototype.receiveKey = async function receiveKey(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.receiveKey(); -}; + /** + * Get current change key. + * @param {Number} [acct=0] + * @returns {WalletKey} + */ -/** - * Get current change key. - * @param {Number} [acct=0] - * @returns {WalletKey} - */ + async changeKey(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.changeKey(); + } -Wallet.prototype.changeKey = async function changeKey(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.changeKey(); -}; + /** + * Get current nested key. + * @param {Number} [acct=0] + * @returns {WalletKey} + */ -/** - * Get current nested key. - * @param {Number} [acct=0] - * @returns {WalletKey} - */ + async nestedKey(acct = 0) { + const account = await this.getAccount(acct); + if (!account) + throw new Error('Account not found.'); + return account.nestedKey(); + } -Wallet.prototype.nestedKey = async function nestedKey(acct = 0) { - const account = await this.getAccount(acct); - if (!account) - throw new Error('Account not found.'); - return account.nestedKey(); -}; + /** + * Convert the wallet to a more inspection-friendly object. + * @returns {Object} + */ -/** - * Convert the wallet to a more inspection-friendly object. - * @returns {Object} - */ + inspect() { + return { + wid: this.wid, + id: this.id, + network: this.network.type, + initialized: this.initialized, + accountDepth: this.accountDepth, + token: this.token.toString('hex'), + tokenDepth: this.tokenDepth, + master: this.master + }; + } -Wallet.prototype.inspect = function inspect() { - return { - wid: this.wid, - id: this.id, - network: this.network.type, - initialized: this.initialized, - accountDepth: this.accountDepth, - token: this.token.toString('hex'), - tokenDepth: this.tokenDepth, - master: this.master - }; -}; + /** + * Convert the wallet to an object suitable for + * serialization. + * @param {Boolean?} unsafe - Whether to include + * the master key in the JSON. + * @returns {Object} + */ -/** - * Convert the wallet to an object suitable for - * serialization. - * @param {Boolean?} unsafe - Whether to include - * the master key in the JSON. - * @returns {Object} - */ + toJSON(unsafe, balance) { + return { + network: this.network.type, + wid: this.wid, + id: this.id, + initialized: this.initialized, + watchOnly: this.watchOnly, + accountDepth: this.accountDepth, + token: this.token.toString('hex'), + tokenDepth: this.tokenDepth, + master: this.master.toJSON(unsafe), + balance: balance ? balance.toJSON(true) : null + }; + } -Wallet.prototype.toJSON = function toJSON(unsafe, balance) { - return { - network: this.network.type, - wid: this.wid, - id: this.id, - initialized: this.initialized, - watchOnly: this.watchOnly, - accountDepth: this.accountDepth, - token: this.token.toString('hex'), - tokenDepth: this.tokenDepth, - master: this.master.toJSON(unsafe), - balance: balance ? balance.toJSON(true) : null - }; -}; + /** + * Calculate serialization size. + * @returns {Number} + */ -/** - * Calculate serialization size. - * @returns {Number} - */ + getSize() { + let size = 0; + size += 50; + size += encoding.sizeVarString(this.id, 'ascii'); + size += encoding.sizeVarlen(this.master.getSize()); + return size; + } -Wallet.prototype.getSize = function getSize() { - let size = 0; - size += 50; - size += encoding.sizeVarString(this.id, 'ascii'); - size += encoding.sizeVarlen(this.master.getSize()); - return size; -}; + /** + * Serialize the wallet. + * @returns {Buffer} + */ -/** - * Serialize the wallet. - * @returns {Buffer} - */ + toRaw() { + const size = this.getSize(); + const bw = bio.write(size); -Wallet.prototype.toRaw = function toRaw() { - const size = this.getSize(); - const bw = bio.write(size); + bw.writeU32(this.network.magic); + bw.writeU32(this.wid); + bw.writeVarString(this.id, 'ascii'); + bw.writeU8(this.initialized ? 1 : 0); + bw.writeU8(this.watchOnly ? 1 : 0); + bw.writeU32(this.accountDepth); + bw.writeBytes(this.token); + bw.writeU32(this.tokenDepth); + bw.writeVarBytes(this.master.toRaw()); - bw.writeU32(this.network.magic); - bw.writeU32(this.wid); - bw.writeVarString(this.id, 'ascii'); - bw.writeU8(this.initialized ? 1 : 0); - bw.writeU8(this.watchOnly ? 1 : 0); - bw.writeU32(this.accountDepth); - bw.writeBytes(this.token); - bw.writeU32(this.tokenDepth); - bw.writeVarBytes(this.master.toRaw()); + return bw.render(); + } - return bw.render(); -}; + /** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ + fromRaw(data) { + const br = bio.read(data); + const network = Network.fromMagic(br.readU32()); -Wallet.prototype.fromRaw = function fromRaw(data) { - const br = bio.read(data); - const network = Network.fromMagic(br.readU32()); + assert(network === this.network, 'Wallet network mismatch.'); - assert(network === this.network, 'Wallet network mismatch.'); + this.wid = br.readU32(); + this.id = br.readVarString('ascii'); + this.initialized = br.readU8() === 1; + this.watchOnly = br.readU8() === 1; + this.accountDepth = br.readU32(); + this.token = br.readBytes(32); + this.tokenDepth = br.readU32(); + this.master.fromRaw(br.readVarBytes(), this.network); - this.wid = br.readU32(); - this.id = br.readVarString('ascii'); - this.initialized = br.readU8() === 1; - this.watchOnly = br.readU8() === 1; - this.accountDepth = br.readU32(); - this.token = br.readBytes(32); - this.tokenDepth = br.readU32(); - this.master.fromRaw(br.readVarBytes(), this.network); + return this; + } - return this; -}; + /** + * Instantiate a wallet from serialized data. + * @param {Buffer} data + * @returns {Wallet} + */ -/** - * Instantiate a wallet from serialized data. - * @param {Buffer} data - * @returns {Wallet} - */ + static fromRaw(wdb, data) { + return new this(wdb).fromRaw(data); + } -Wallet.fromRaw = function fromRaw(wdb, data) { - return new Wallet(wdb).fromRaw(data); -}; + /** + * Test an object to see if it is a Wallet. + * @param {Object} obj + * @returns {Boolean} + */ -/** - * Test an object to see if it is a Wallet. - * @param {Object} obj - * @returns {Boolean} - */ - -Wallet.isWallet = function isWallet(obj) { - return obj instanceof Wallet; -}; + static isWallet(obj) { + return obj instanceof Wallet; + } +} /* * Expose diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index adbb5d78..4fb8647c 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -40,49 +40,1907 @@ const { /** * WalletDB * @alias module:wallet.WalletDB - * @constructor - * @param {Object} options + * @extends EventEmitter */ -function WalletDB(options) { - if (!(this instanceof WalletDB)) - return new WalletDB(options); +class WalletDB extends EventEmitter { + /** + * Create a wallet db. + * @constructor + * @param {Object} options + */ - EventEmitter.call(this); + constructor(options) { + super(); - this.options = new WalletOptions(options); + this.options = new WalletOptions(options); - this.network = this.options.network; - this.logger = this.options.logger.context('wallet'); - this.workers = this.options.workers; - this.client = this.options.client || new NullClient(this); - this.feeRate = this.options.feeRate; - this.db = new BDB(this.options); + this.network = this.options.network; + this.logger = this.options.logger.context('wallet'); + this.workers = this.options.workers; + this.client = this.options.client || new NullClient(this); + this.feeRate = this.options.feeRate; + this.db = new BDB(this.options); - this.primary = null; - this.state = new ChainState(); - this.height = 0; - this.wallets = new Map(); - this.depth = 0; - this.rescanning = false; + this.primary = null; + this.state = new ChainState(); + this.height = 0; + this.wallets = new Map(); + this.depth = 0; + this.rescanning = false; - // Wallet read lock. - this.readLock = new MapLock(); + // Wallet read lock. + this.readLock = new MapLock(); - // Wallet write lock (creation and rename). - this.writeLock = new Lock(); + // Wallet write lock (creation and rename). + this.writeLock = new Lock(); - // Lock for handling anything tx related. - this.txLock = new Lock(); + // Lock for handling anything tx related. + this.txLock = new Lock(); - // Address and outpoint filter. - this.filter = new BloomFilter(); + // Address and outpoint filter. + this.filter = new BloomFilter(); - this.init(); + this.init(); + } + + /** + * Initialize walletdb. + * @private + */ + + init() { + let items = 3000000; + let flag = -1; + + // Highest number of items with an + // FPR of 0.001. We have to do this + // by hand because BloomFilter.fromRate's + // policy limit enforcing is fairly + // naive. + if (this.options.spv) { + items = 20000; + flag = BloomFilter.flags.ALL; + } + + this.filter = BloomFilter.fromRate(items, 0.001, flag); + this._bind(); + } + + /** + * Bind to node events. + * @private + */ + + _bind() { + this.client.on('error', (err) => { + this.emit('error', err); + }); + + this.client.on('connect', async () => { + try { + await this.syncNode(); + } catch (e) { + this.emit('error', e); + } + }); + + this.client.bind('block connect', async (entry, txs) => { + try { + await this.addBlock(entry, txs); + } catch (e) { + this.emit('error', e); + } + }); + + this.client.bind('block disconnect', async (entry) => { + try { + await this.removeBlock(entry); + } catch (e) { + this.emit('error', e); + } + }); + + this.client.hook('block rescan', async (entry, txs) => { + try { + await this.rescanBlock(entry, txs); + } catch (e) { + this.emit('error', e); + } + }); + + this.client.bind('tx', async (tx) => { + try { + await this.addTX(tx); + } catch (e) { + this.emit('error', e); + } + }); + + this.client.bind('chain reset', async (tip) => { + try { + await this.resetChain(tip); + } catch (e) { + this.emit('error', e); + } + }); + } + + /** + * Open the walletdb, wait for the database to load. + * @returns {Promise} + */ + + async open() { + await this.db.open(); + await this.db.checkVersion('V', 7); + + this.depth = await this.getDepth(); + + if (this.options.wipeNoReally) + await this.wipe(); + + await this.watch(); + await this.connect(); + + this.logger.info( + 'WalletDB loaded (depth=%d, height=%d, start=%d).', + this.depth, + this.state.height, + this.state.startHeight); + + const wallet = await this.ensure({ + id: 'primary' + }); + + const addr = await wallet.receiveAddress(); + + this.logger.info( + 'Loaded primary wallet (id=%s, wid=%d, address=%s)', + wallet.id, wallet.wid, addr.toString(this.network)); + + this.primary = wallet; + } + + /** + * Close the walletdb, wait for the database to close. + * @returns {Promise} + */ + + async close() { + await this.disconnect(); + + for (const wallet of this.wallets.values()) { + await wallet.destroy(); + this.unregister(wallet); + } + + await this.db.close(); + } + + /** + * Watch addresses and outpoints. + * @private + * @returns {Promise} + */ + + async watch() { + const piter = this.db.iterator({ + gte: layout.p(encoding.NULL_HASH), + lte: layout.p(encoding.HIGH_HASH) + }); + + let hashes = 0; + + await piter.each((key) => { + const data = layout.pp(key); + + this.filter.add(data, 'hex'); + + hashes += 1; + }); + + this.logger.info('Added %d hashes to WalletDB filter.', hashes); + + const oiter = this.db.iterator({ + gte: layout.o(encoding.NULL_HASH, 0), + lte: layout.o(encoding.HIGH_HASH, 0xffffffff) + }); + + let outpoints = 0; + + await oiter.each((key) => { + const [hash, index] = layout.oo(key); + const outpoint = new Outpoint(hash, index); + const data = outpoint.toRaw(); + + this.filter.add(data); + + outpoints += 1; + }); + + this.logger.info('Added %d outpoints to WalletDB filter.', outpoints); + } + + /** + * Connect to the node server (client required). + * @returns {Promise} + */ + + async connect() { + return this.client.open(); + } + + /** + * Disconnect from node server (client required). + * @returns {Promise} + */ + + async disconnect() { + return this.client.close(); + } + + /** + * Sync state with server on every connect. + * @returns {Promise} + */ + + async syncNode() { + const unlock = await this.txLock.lock(); + try { + this.logger.info('Resyncing from server...'); + await this.syncState(); + await this.syncFilter(); + await this.syncChain(); + await this.resend(); + } finally { + unlock(); + } + } + + /** + * Initialize and write initial sync state. + * @returns {Promise} + */ + + async syncState() { + const cache = await this.getState(); + + if (cache) { + this.state = cache; + this.height = cache.height; + return; + } + + this.logger.info('Initializing database state from server.'); + + const b = this.db.batch(); + + let tip = null; + + const hashes = await this.client.getHashes(); + + for (let height = 0; height < hashes.length; height++) { + const hash = hashes[height]; + const meta = new BlockMeta(hash, height); + b.put(layout.h(height), meta.toHash()); + tip = meta; + } + + assert(tip); + + const state = this.state.clone(); + state.startHeight = tip.height; + state.startHash = tip.hash; + state.height = tip.height; + state.marked = false; + + b.put(layout.R, state.toRaw()); + + await b.write(); + + this.state = state; + this.height = state.height; + } + + /** + * Connect and sync with the chain server. + * @private + * @returns {Promise} + */ + + async syncChain() { + let height = this.state.height; + + this.logger.info('Syncing state from height %d.', height); + + for (;;) { + const tip = await this.getBlock(height); + assert(tip); + + if (await this.client.getEntry(tip.hash)) + break; + + assert(height !== 0); + height -= 1; + } + + await this.scan(height); + } + + /** + * Rescan blockchain from a given height. + * @private + * @param {Number?} height + * @returns {Promise} + */ + + async scan(height) { + if (height == null) + height = this.state.startHeight; + + assert((height >>> 0) === height, 'WDB: Must pass in a height.'); + + await this.rollback(height); + + this.logger.info( + 'WalletDB is scanning %d blocks.', + this.state.height - height + 1); + + const tip = await this.getTip(); + + try { + this.rescanning = true; + await this.client.rescan(tip.hash); + } finally { + this.rescanning = false; + } + } + + /** + * Force a rescan. + * @param {Number} height + * @returns {Promise} + */ + + async rescan(height) { + const unlock = await this.txLock.lock(); + try { + return await this._rescan(height); + } finally { + unlock(); + } + } + + /** + * Force a rescan (without a lock). + * @private + * @param {Number} height + * @returns {Promise} + */ + + async _rescan(height) { + return this.scan(height); + } + + /** + * Broadcast a transaction via chain server. + * @param {TX} tx + * @returns {Promise} + */ + + async send(tx) { + return this.client.send(tx); + } + + /** + * Estimate smart fee from chain server. + * @param {Number} blocks + * @returns {Promise} + */ + + async estimateFee(blocks) { + if (this.feeRate > 0) + return this.feeRate; + + const rate = await this.client.estimateFee(blocks); + + if (rate < this.network.feeRate) + return this.network.feeRate; + + if (rate > this.network.maxFeeRate) + return this.network.maxFeeRate; + + return rate; + } + + /** + * Send filter to the remote node. + * @private + * @returns {Promise} + */ + + syncFilter() { + this.logger.info('Sending filter to server (%dmb).', + this.filter.size / 8 / (1 << 20)); + + return this.client.setFilter(this.filter); + } + + /** + * Add data to remote filter. + * @private + * @param {Buffer} data + * @returns {Promise} + */ + + addFilter(data) { + return this.client.addFilter(data); + } + + /** + * Reset remote filter. + * @private + * @returns {Promise} + */ + + resetFilter() { + return this.client.resetFilter(); + } + + /** + * Backup the wallet db. + * @param {String} path + * @returns {Promise} + */ + + backup(path) { + return this.db.backup(path); + } + + /** + * Wipe the txdb - NEVER USE. + * @returns {Promise} + */ + + async wipe() { + this.logger.warning('Wiping WalletDB TXDB...'); + this.logger.warning('I hope you know what you\'re doing.'); + + const iter = this.db.iterator({ + gte: Buffer.from([0x00]), + lte: Buffer.from([0xff]) + }); + + const b = this.db.batch(); + + let total = 0; + + await iter.each((key) => { + switch (key[0]) { + case 0x62: // b + case 0x63: // c + case 0x65: // e + case 0x74: // t + case 0x6f: // o + case 0x68: // h + case 0x52: // R + b.del(key); + total += 1; + break; + } + }); + + this.logger.warning('Wiped %d txdb records.', total); + + await b.write(); + } + + /** + * Get current wallet wid depth. + * @private + * @returns {Promise} + */ + + async getDepth() { + // This may seem like a strange way to do + // this, but updating a global state when + // creating a new wallet is actually pretty + // damn tricky. There would be major atomicity + // issues if updating a global state inside + // a "scoped" state. So, we avoid all the + // nonsense of adding a global lock to + // walletdb.create by simply seeking to the + // highest wallet wid. + const iter = this.db.iterator({ + gte: layout.w(0x00000000), + lte: layout.w(0xffffffff), + reverse: true, + limit: 1 + }); + + if (!await iter.next()) + return 1; + + const {key} = iter; + + await iter.end(); + + const depth = layout.ww(key); + + return depth + 1; + } + + /** + * Test the bloom filter against a tx or address hash. + * @private + * @param {Hash} hash + * @returns {Boolean} + */ + + testFilter(data) { + return this.filter.test(data, 'hex'); + } + + /** + * Add hash to local and remote filters. + * @private + * @param {Hash} hash + */ + + addHash(hash) { + this.filter.add(hash, 'hex'); + return this.addFilter(hash); + } + + /** + * Add outpoint to local filter. + * @private + * @param {Hash} hash + * @param {Number} index + */ + + addOutpoint(hash, index) { + const outpoint = new Outpoint(hash, index); + this.filter.add(outpoint.toRaw()); + } + + /** + * Dump database (for debugging). + * @returns {Promise} - Returns Object. + */ + + dump() { + return this.db.dump(); + } + + /** + * Register an object with the walletdb. + * @param {Object} object + */ + + register(wallet) { + assert(!this.wallets.has(wallet.wid)); + this.wallets.set(wallet.wid, wallet); + } + + /** + * Unregister a object with the walletdb. + * @param {Object} object + * @returns {Boolean} + */ + + unregister(wallet) { + assert(this.wallets.has(wallet.wid)); + this.wallets.delete(wallet.wid); + } + + /** + * Map wallet id to wid. + * @param {String} id + * @returns {Promise} - Returns {WalletID}. + */ + + async getWID(id) { + if (!id) + return null; + + if (typeof id === 'number') + return id; + + const data = await this.db.get(layout.l(id)); + + if (!data) + return null; + + assert(data.length === 4); + + return data.readUInt32LE(0, true); + } + + /** + * Get a wallet from the database, setup watcher. + * @param {WalletID} wid + * @returns {Promise} - Returns {@link Wallet}. + */ + + async get(id) { + const wid = await this.getWID(id); + + if (!wid) + return null; + + const unlock = await this.readLock.lock(wid); + + try { + return await this._get(wid); + } finally { + unlock(); + } + } + + /** + * Get a wallet from the database without a lock. + * @private + * @param {WalletID} wid + * @returns {Promise} - Returns {@link Wallet}. + */ + + async _get(wid) { + const cache = this.wallets.get(wid); + + if (cache) + return cache; + + const data = await this.db.get(layout.w(wid)); + + if (!data) + return null; + + const wallet = Wallet.fromRaw(this, data); + + await wallet.open(); + + this.register(wallet); + + return wallet; + } + + /** + * Save a wallet to the database. + * @param {Wallet} wallet + */ + + save(b, wallet) { + const wid = wallet.wid; + const id = wallet.id; + + b.put(layout.w(wid), wallet.toRaw()); + b.put(layout.l(id), u32(wid)); + } + + /** + * Rename a wallet. + * @param {Wallet} wallet + * @param {String} id + * @returns {Promise} + */ + + async rename(wallet, id) { + const unlock = await this.writeLock.lock(); + try { + return await this._rename(wallet, id); + } finally { + unlock(); + } + } + + /** + * Rename a wallet without a lock. + * @private + * @param {Wallet} wallet + * @param {String} id + * @returns {Promise} + */ + + async _rename(wallet, id) { + if (!common.isName(id)) + throw new Error('WDB: Bad wallet ID.'); + + if (await this.has(id)) + throw new Error('WDB: ID not available.'); + + const old = wallet.id; + const b = this.db.batch(); + + b.del(layout.l(old)); + + wallet.id = id; + + this.save(b, wallet); + + await b.write(); + } + + /** + * Rename an account. + * @param {Account} account + * @param {String} name + */ + + renameAccount(b, account, name) { + // Remove old wid/name->account index. + b.del(layout.i(account.wid, account.name)); + + account.name = name; + + this.saveAccount(b, account); + } + + /** + * Get a wallet with token auth first. + * @param {WalletID} wid + * @param {Buffer} token + * @returns {Promise} - Returns {@link Wallet}. + */ + + async auth(wid, token) { + const wallet = await this.get(wid); + + if (!wallet) + return null; + + // Compare in constant time: + if (!ccmp(token, wallet.token)) + throw new Error('WDB: Authentication error.'); + + return wallet; + } + + /** + * Create a new wallet, save to database, setup watcher. + * @param {Object} options - See {@link Wallet}. + * @returns {Promise} - Returns {@link Wallet}. + */ + + async create(options) { + const unlock = await this.writeLock.lock(); + + if (!options) + options = {}; + + try { + return await this._create(options); + } finally { + unlock(); + } + } + + /** + * Create a new wallet, save to database without a lock. + * @private + * @param {Object} options - See {@link Wallet}. + * @returns {Promise} - Returns {@link Wallet}. + */ + + async _create(options) { + if (await this.has(options.id)) + throw new Error('WDB: Wallet already exists.'); + + const wallet = Wallet.fromOptions(this, options); + + wallet.wid = this.depth; + + await wallet.init(options); + + this.depth += 1; + + this.register(wallet); + + this.logger.info('Created wallet %s in WalletDB.', wallet.id); + + return wallet; + } + + /** + * Test for the existence of a wallet. + * @param {WalletID} id + * @returns {Promise} + */ + + async has(id) { + const wid = await this.getWID(id); + return wid != null; + } + + /** + * Attempt to create wallet, return wallet if already exists. + * @param {Object} options - See {@link Wallet}. + * @returns {Promise} + */ + + async ensure(options) { + const wallet = await this.get(options.id); + + if (wallet) + return wallet; + + return this.create(options); + } + + /** + * Get an account from the database by wid. + * @private + * @param {WalletID} wid + * @param {Number} index - Account index. + * @returns {Promise} - Returns {@link Wallet}. + */ + + async getAccount(wid, index) { + const data = await this.db.get(layout.a(wid, index)); + + if (!data) + return null; + + return Account.fromRaw(this, data); + } + + /** + * List account names and indexes from the db. + * @param {WalletID} wid + * @returns {Promise} - Returns Array. + */ + + getAccounts(wid) { + return this.db.values({ + gte: layout.n(wid, 0x00000000), + lte: layout.n(wid, 0xffffffff), + parse: data => data.toString('ascii') + }); + } + + /** + * Lookup the corresponding account name's index. + * @param {WalletID} wid + * @param {String} name - Account name/index. + * @returns {Promise} - Returns Number. + */ + + async getAccountIndex(wid, name) { + const index = await this.db.get(layout.i(wid, name)); + + if (!index) + return -1; + + return index.readUInt32LE(0, true); + } + + /** + * Lookup the corresponding account index's name. + * @param {WalletID} wid + * @param {Number} index + * @returns {Promise} - Returns Number. + */ + + async getAccountName(wid, index) { + const name = await this.db.get(layout.n(wid, index)); + + if (!name) + return null; + + return name.toString('ascii'); + } + + /** + * Save an account to the database. + * @param {Account} account + * @returns {Promise} + */ + + saveAccount(b, account) { + const wid = account.wid; + const index = account.accountIndex; + const name = account.name; + + // Account data + b.put(layout.a(wid, index), account.toRaw()); + + // Name->Index lookups + b.put(layout.i(wid, name), u32(index)); + + // Index->Name lookups + b.put(layout.n(wid, index), Buffer.from(name, 'ascii')); + } + + /** + * Test for the existence of an account. + * @param {WalletID} wid + * @param {String|Number} acct + * @returns {Promise} - Returns Boolean. + */ + + hasAccount(wid, index) { + return this.db.has(layout.a(wid, index)); + } + + /** + * Save an address to the path map. + * @param {Wallet} wallet + * @param {WalletKey} ring + * @returns {Promise} + */ + + saveKey(b, wid, ring) { + return this.savePath(b, wid, ring.toPath()); + } + + /** + * Save a path to the path map. + * + * The path map exists in the form of: + * - `p[address-hash] -> wid map` + * - `P[wid][address-hash] -> path data` + * - `r[wid][account-index][address-hash] -> dummy` + * + * @param {Wallet} wallet + * @param {Path} path + * @returns {Promise} + */ + + async savePath(b, wid, path) { + // Address Hash -> Wallet Map + await this.addPathMap(b, path.hash, wid); + + // Wallet ID + Address Hash -> Path Data + b.put(layout.P(wid, path.hash), path.toRaw()); + + // Wallet ID + Account Index + Address Hash -> Dummy + b.put(layout.r(wid, path.account, path.hash), null); + } + + /** + * Retrieve path by hash. + * @param {WalletID} wid + * @param {Hash} hash + * @returns {Promise} + */ + + async getPath(wid, hash) { + const path = await this.readPath(wid, hash); + + if (!path) + return null; + + path.name = await this.getAccountName(wid, path.account); + assert(path.name); + + return path; + } + + /** + * Retrieve path by hash. + * @param {WalletID} wid + * @param {Hash} hash + * @returns {Promise} + */ + + async readPath(wid, hash) { + const data = await this.db.get(layout.P(wid, hash)); + + if (!data) + return null; + + const path = Path.fromRaw(data); + path.hash = hash; + + return path; + } + + /** + * Test whether a wallet contains a path. + * @param {WalletID} wid + * @param {Hash} hash + * @returns {Promise} + */ + + hasPath(wid, hash) { + return this.db.has(layout.P(wid, hash)); + } + + /** + * Get all address hashes. + * @returns {Promise} + */ + + getHashes() { + return this.db.keys({ + gte: layout.p(encoding.NULL_HASH), + lte: layout.p(encoding.HIGH_HASH), + parse: layout.pp + }); + } + + /** + * Get all outpoints. + * @returns {Promise} + */ + + getOutpoints() { + return this.db.keys({ + gte: layout.o(encoding.NULL_HASH, 0), + lte: layout.o(encoding.HIGH_HASH, 0xffffffff), + parse: (key) => { + const [hash, index] = layout.oo(key); + return new Outpoint(hash, index); + } + }); + } + + /** + * Get all address hashes. + * @param {WalletID} wid + * @returns {Promise} + */ + + getWalletHashes(wid) { + return this.db.keys({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH), + parse: layout.Pp + }); + } + + /** + * Get all account address hashes. + * @param {WalletID} wid + * @param {Number} account + * @returns {Promise} + */ + + getAccountHashes(wid, account) { + return this.db.keys({ + gte: layout.r(wid, account, encoding.NULL_HASH), + lte: layout.r(wid, account, encoding.HIGH_HASH), + parse: layout.rr + }); + } + + /** + * Get all paths for a wallet. + * @param {WalletID} wid + * @returns {Promise} + */ + + async getWalletPaths(wid) { + const items = await this.db.range({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH) + }); + + const paths = []; + + for (const item of items) { + const hash = layout.Pp(item.key); + const path = Path.fromRaw(item.value); + + path.hash = hash; + path.name = await this.getAccountName(wid, path.account); + assert(path.name); + + paths.push(path); + } + + return paths; + } + + /** + * Get all wallet ids. + * @returns {Promise} + */ + + getWallets() { + return this.db.keys({ + gte: layout.l('\x00'), + lte: layout.l('\xff'), + parse: layout.ll + }); + } + + /** + * Encrypt all imported keys for a wallet. + * @param {WalletID} wid + * @param {Buffer} key + * @returns {Promise} + */ + + async encryptKeys(b, wid, key) { + const iter = this.db.iterator({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH), + values: true + }); + + await iter.each((k, value) => { + const hash = layout.Pp(k); + const path = Path.fromRaw(value); + + if (!path.data) + return; + + assert(!path.encrypted); + + const bhash = Buffer.from(hash, 'hex'); + const iv = bhash.slice(0, 16); + + path.data = aes.encipher(path.data, key, iv); + path.encrypted = true; + + b.put(k, path.toRaw()); + }); + } + + /** + * Decrypt all imported keys for a wallet. + * @param {WalletID} wid + * @param {Buffer} key + * @returns {Promise} + */ + + async decryptKeys(b, wid, key) { + const iter = this.db.iterator({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH), + values: true + }); + + await iter.each((k, value) => { + const hash = layout.Pp(k); + const path = Path.fromRaw(value); + + if (!path.data) + return; + + assert(path.encrypted); + + const bhash = Buffer.from(hash, 'hex'); + const iv = bhash.slice(0, 16); + + path.data = aes.decipher(path.data, key, iv); + path.encrypted = false; + + b.put(k, path.toRaw()); + }); + } + + /** + * Resend all pending transactions. + * @returns {Promise} + */ + + async resend() { + const wids = await this.db.keys({ + gte: layout.w(0x00000000), + lte: layout.w(0xffffffff), + parse: k => layout.ww(k) + }); + + this.logger.info('Resending from %d wallets.', wids.length); + + for (const wid of wids) + await this.resendPending(wid); + } + + /** + * Resend all pending transactions for a specific wallet. + * @private + * @param {WalletID} wid + * @returns {Promise} + */ + + async resendPending(wid) { + const layout = layouts.txdb; + const prefix = layout.prefix(wid); + const b = this.db.bucket(prefix); + + const hashes = await b.keys({ + gte: layout.p(encoding.NULL_HASH), + lte: layout.p(encoding.HIGH_HASH), + parse: k => layout.pp(k) + }); + + if (hashes.length === 0) + return; + + this.logger.info( + 'Rebroadcasting %d transactions for %d.', + hashes.length, + wid); + + const txs = []; + + for (const hash of hashes) { + const data = await b.get(layout.t(hash)); + + if (!data) + continue; + + const wtx = TXRecord.fromRaw(data); + + if (wtx.tx.isCoinbase()) + continue; + + txs.push(wtx.tx); + } + + for (const tx of common.sortDeps(txs)) + await this.send(tx); + } + + /** + * Get all wallet ids by output addresses and outpoints. + * @param {Hash[]} hashes + * @returns {Promise} + */ + + async getWalletsByTX(tx) { + const result = new Set(); + + if (!tx.isCoinbase()) { + for (const {prevout} of tx.inputs) { + const {hash, index} = prevout; + + if (!this.testFilter(prevout.toRaw())) + continue; + + const map = await this.getOutpointMap(hash, index); + + if (!map) + continue; + + for (const wid of map.wids) + result.add(wid); + } + } + + const hashes = tx.getOutputHashes('hex'); + + for (const hash of hashes) { + if (!this.testFilter(hash)) + continue; + + const map = await this.getPathMap(hash); + + if (!map) + continue; + + for (const wid of map.wids) + result.add(wid); + } + + if (result.size === 0) + return null; + + return result; + } + + /** + * Get the best block hash. + * @returns {Promise} + */ + + async getState() { + const data = await this.db.get(layout.R); + + if (!data) + return null; + + return ChainState.fromRaw(data); + } + + /** + * Sync the current chain state to tip. + * @param {BlockMeta} tip + * @returns {Promise} + */ + + async setTip(tip) { + const b = this.db.batch(); + const state = this.state.clone(); + + if (tip.height < state.height) { + // Hashes ahead of our new tip + // that we need to delete. + while (state.height !== tip.height) { + b.del(layout.h(state.height)); + state.height -= 1; + } + } else if (tip.height > state.height) { + assert(tip.height === state.height + 1, 'Bad chain sync.'); + state.height += 1; + } + + if (tip.height < state.startHeight) { + state.startHeight = tip.height; + state.startHash = tip.hash; + state.marked = false; + } + + // Save tip and state. + b.put(layout.h(tip.height), tip.toHash()); + b.put(layout.R, state.toRaw()); + + await b.write(); + + this.state = state; + this.height = state.height; + } + + /** + * Mark current state. + * @param {BlockMeta} block + * @returns {Promise} + */ + + async markState(block) { + const state = this.state.clone(); + state.startHeight = block.height; + state.startHash = block.hash; + state.marked = true; + + const b = this.db.batch(); + b.put(layout.R, state.toRaw()); + await b.write(); + + this.state = state; + this.height = state.height; + } + + /** + * Get a wallet map. + * @param {Buffer} key + * @returns {Promise} + */ + + async getMap(key) { + const data = await this.db.get(key); + + if (!data) + return null; + + return MapRecord.fromRaw(data); + } + + /** + * Add wid to a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + async addMap(b, key, wid) { + const data = await this.db.get(key); + + if (!data) { + const map = new MapRecord(); + map.add(wid); + b.put(key, map.toRaw()); + return; + } + + assert(data.length >= 4); + + const len = data.readUInt32LE(0, true); + const bw = bio.write(data.length + 4); + + bw.writeU32(len + 1); + bw.copy(data, 4, data.length); + bw.writeU32(wid); + + b.put(key, bw.render()); + } + + /** + * Remove wid from a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + async removeMap(b, key, wid) { + const map = await this.getMap(key); + + if (!map) + return; + + if (!map.remove(wid)) + return; + + if (map.size === 0) { + b.del(key); + return; + } + + b.put(key, map.toRaw()); + } + + /** + * Get a wallet map. + * @param {Buffer} key + * @returns {Promise} + */ + + getPathMap(hash) { + return this.getMap(layout.p(hash)); + } + + /** + * Add wid to a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + async addPathMap(b, hash, wid) { + await this.addHash(hash); + return this.addMap(b, layout.p(hash), wid); + } + + /** + * Remove wid from a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + removePathMap(b, hash, wid) { + return this.removeMap(b, layout.p(hash), wid); + } + + /** + * Get a wallet map. + * @param {Buffer} key + * @returns {Promise} + */ + + async getBlockMap(height) { + return this.getMap(layout.b(height)); + } + + /** + * Add wid to a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + addBlockMap(b, height, wid) { + return this.addMap(b, layout.b(height), wid); + } + + /** + * Remove wid from a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + removeBlockMap(b, height, wid) { + return this.removeMap(b, layout.b(height), wid); + } + + /** + * Get a wallet map. + * @param {Buffer} key + * @returns {Promise} + */ + + getTXMap(hash) { + return this.getMap(layout.T(hash)); + } + + /** + * Add wid to a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + addTXMap(b, hash, wid) { + return this.addMap(b, layout.T(hash), wid); + } + + /** + * Remove wid from a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + removeTXMap(b, hash, wid) { + return this.removeMap(b, layout.T(hash), wid); + } + + /** + * Get a wallet map. + * @param {Buffer} key + * @returns {Promise} + */ + + getOutpointMap(hash, index) { + return this.getMap(layout.o(hash, index)); + } + + /** + * Add wid to a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + async addOutpointMap(b, hash, index, wid) { + await this.addOutpoint(hash, index); + return this.addMap(b, layout.o(hash, index), wid); + } + + /** + * Remove wid from a wallet map. + * @param {Wallet} wallet + * @param {Buffer} key + * @param {Number} wid + */ + + removeOutpointMap(b, hash, index, wid) { + return this.removeMap(b, layout.o(hash, index), wid); + } + + /** + * Get a wallet block meta. + * @param {Hash} hash + * @returns {Promise} + */ + + async getBlock(height) { + const data = await this.db.get(layout.h(height)); + + if (!data) + return null; + + const block = new BlockMeta(); + block.hash = data.toString('hex'); + block.height = height; + + return block; + } + + /** + * Get wallet tip. + * @param {Hash} hash + * @returns {Promise} + */ + + async getTip() { + const tip = await this.getBlock(this.state.height); + + if (!tip) + throw new Error('WDB: Tip not found!'); + + return tip; + } + + /** + * Sync with chain height. + * @param {Number} height + * @returns {Promise} + */ + + async rollback(height) { + if (height > this.state.height) + throw new Error('WDB: Cannot rollback to the future.'); + + if (height === this.state.height) { + this.logger.info('Rolled back to same height (%d).', height); + return; + } + + this.logger.info( + 'Rolling back %d WalletDB blocks to height %d.', + this.state.height - height, height); + + const tip = await this.getBlock(height); + assert(tip); + + await this.revert(tip.height); + await this.setTip(tip); + } + + /** + * Revert TXDB to an older state. + * @param {Number} target + * @returns {Promise} + */ + + async revert(target) { + const iter = this.db.iterator({ + gte: layout.b(target + 1), + lte: layout.b(0xffffffff), + reverse: true, + values: true + }); + + let total = 0; + + await iter.each(async (key, value) => { + const height = layout.bb(key); + const block = MapRecord.fromRaw(value); + + for (const wid of block.wids) { + const wallet = await this.get(wid); + assert(wallet); + total += await wallet.revert(height); + } + }); + + this.logger.info('Rolled back %d WalletDB transactions.', total); + } + + /** + * Add a block's transactions and write the new best hash. + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async addBlock(entry, txs) { + const unlock = await this.txLock.lock(); + try { + return await this._addBlock(entry, txs); + } finally { + unlock(); + } + } + + /** + * Add a block's transactions without a lock. + * @private + * @param {ChainEntry} entry + * @param {TX[]} txs + * @returns {Promise} + */ + + async _addBlock(entry, txs) { + const tip = BlockMeta.fromEntry(entry); + + if (tip.height < this.state.height) { + this.logger.warning( + 'WalletDB is connecting low blocks (%d).', + tip.height); + return 0; + } + + this.logger.debug('Adding block: %d.', entry.height); + + if (tip.height === this.state.height) { + // We let blocks of the same height + // through specifically for rescans: + // we always want to rescan the last + // block since the state may have + // updated before the block was fully + // processed (in the case of a crash). + this.logger.warning('Already saw WalletDB block (%d).', tip.height); + } else if (tip.height !== this.state.height + 1) { + await this.scan(this.state.height); + return 0; + } + + // Sync the state to the new tip. + await this.setTip(tip); + + if (this.options.checkpoints && !this.state.marked) { + if (tip.height <= this.network.lastCheckpoint) + return 0; + } + + let total = 0; + + for (const tx of txs) { + if (await this._addTX(tx, tip)) + total += 1; + } + + if (total > 0) { + this.logger.info('Connected WalletDB block %s (tx=%d).', + encoding.revHex(tip.hash), total); + } + + return total; + } + + /** + * Unconfirm a block's transactions + * and write the new best hash (SPV version). + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async removeBlock(entry) { + const unlock = await this.txLock.lock(); + try { + return await this._removeBlock(entry); + } finally { + unlock(); + } + } + + /** + * Unconfirm a block's transactions. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async _removeBlock(entry) { + const tip = BlockMeta.fromEntry(entry); + + if (tip.height === 0) + throw new Error('WDB: Bad disconnection (genesis block).'); + + if (tip.height > this.state.height) { + this.logger.warning( + 'WalletDB is disconnecting high blocks (%d).', + tip.height); + return 0; + } + + if (tip.height !== this.state.height) + throw new Error('WDB: Bad disconnection (height mismatch).'); + + const prev = await this.getBlock(tip.height - 1); + assert(prev); + + // Get the map of block->wids. + const map = await this.getBlockMap(tip.height); + + if (!map) { + await this.setTip(prev); + return 0; + } + + let total = 0; + + for (const wid of map.wids) { + const wallet = await this.get(wid); + assert(wallet); + total += await wallet.revert(tip.height); + } + + // Sync the state to the previous tip. + await this.setTip(prev); + + this.logger.warning('Disconnected wallet block %s (tx=%d).', + encoding.revHex(tip.hash), total); + + return total; + } + + /** + * Rescan a block. + * @private + * @param {ChainEntry} entry + * @param {TX[]} txs + * @returns {Promise} + */ + + async rescanBlock(entry, txs) { + if (!this.rescanning) { + this.logger.warning('Unsolicited rescan block: %d.', entry.height); + return; + } + + if (entry.height > this.state.height + 1) { + this.logger.warning('Rescan block too high: %d.', entry.height); + return; + } + + try { + await this._addBlock(entry, txs); + } catch (e) { + this.emit('error', e); + throw e; + } + } + + /** + * Add a transaction to the database, map addresses + * to wallet IDs, potentially store orphans, resolve + * orphans, or confirm a transaction. + * @param {TX} tx + * @returns {Promise} + */ + + async addTX(tx) { + const unlock = await this.txLock.lock(); + try { + return await this._addTX(tx); + } finally { + unlock(); + } + } + + /** + * Add a transaction to the database without a lock. + * @private + * @param {TX} tx + * @param {BlockMeta} block + * @returns {Promise} + */ + + async _addTX(tx, block) { + const wids = await this.getWalletsByTX(tx); + + assert(!tx.mutable, 'WDB: Cannot add mutable TX.'); + + if (!wids) + return null; + + if (block && !this.state.marked) + await this.markState(block); + + this.logger.info( + 'Incoming transaction for %d wallets in WalletDB (%s).', + wids.size, tx.txid()); + + let result = false; + + // Insert the transaction + // into every matching wallet. + for (const wid of wids) { + const wallet = await this.get(wid); + + assert(wallet); + + if (await wallet.add(tx, block)) { + this.logger.info( + 'Added transaction to wallet in WalletDB: %s (%d).', + wallet.id, wid); + result = true; + } + } + + if (!result) + return null; + + return wids; + } + + /** + * Handle a chain reset. + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async resetChain(entry) { + const unlock = await this.txLock.lock(); + try { + return await this._resetChain(entry); + } finally { + unlock(); + } + } + + /** + * Handle a chain reset without a lock. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ + + async _resetChain(entry) { + if (entry.height > this.state.height) + throw new Error('WDB: Bad reset height.'); + + await this.rollback(entry.height); + } } -Object.setPrototypeOf(WalletDB.prototype, EventEmitter.prototype); - /** * Database layout. * @type {Object} @@ -91,1997 +1949,144 @@ Object.setPrototypeOf(WalletDB.prototype, EventEmitter.prototype); WalletDB.layout = layout; /** - * Initialize walletdb. - * @private - */ - -WalletDB.prototype.init = function init() { - let items = 3000000; - let flag = -1; - - // Highest number of items with an - // FPR of 0.001. We have to do this - // by hand because BloomFilter.fromRate's - // policy limit enforcing is fairly - // naive. - if (this.options.spv) { - items = 20000; - flag = BloomFilter.flags.ALL; - } - - this.filter = BloomFilter.fromRate(items, 0.001, flag); - this._bind(); -}; - -/** - * Bind to node events. - * @private - */ - -WalletDB.prototype._bind = function _bind() { - this.client.on('error', (err) => { - this.emit('error', err); - }); - - this.client.on('connect', async () => { - try { - await this.syncNode(); - } catch (e) { - this.emit('error', e); - } - }); - - this.client.bind('block connect', async (entry, txs) => { - try { - await this.addBlock(entry, txs); - } catch (e) { - this.emit('error', e); - } - }); - - this.client.bind('block disconnect', async (entry) => { - try { - await this.removeBlock(entry); - } catch (e) { - this.emit('error', e); - } - }); - - this.client.hook('block rescan', async (entry, txs) => { - try { - await this.rescanBlock(entry, txs); - } catch (e) { - this.emit('error', e); - } - }); - - this.client.bind('tx', async (tx) => { - try { - await this.addTX(tx); - } catch (e) { - this.emit('error', e); - } - }); - - this.client.bind('chain reset', async (tip) => { - try { - await this.resetChain(tip); - } catch (e) { - this.emit('error', e); - } - }); -}; - -/** - * Open the walletdb, wait for the database to load. - * @returns {Promise} - */ - -WalletDB.prototype.open = async function open() { - await this.db.open(); - await this.db.checkVersion('V', 7); - - this.depth = await this.getDepth(); - - if (this.options.wipeNoReally) - await this.wipe(); - - await this.watch(); - await this.connect(); - - this.logger.info( - 'WalletDB loaded (depth=%d, height=%d, start=%d).', - this.depth, - this.state.height, - this.state.startHeight); - - const wallet = await this.ensure({ - id: 'primary' - }); - - const addr = await wallet.receiveAddress(); - - this.logger.info( - 'Loaded primary wallet (id=%s, wid=%d, address=%s)', - wallet.id, wallet.wid, addr.toString(this.network)); - - this.primary = wallet; -}; - -/** - * Close the walletdb, wait for the database to close. - * @returns {Promise} - */ - -WalletDB.prototype.close = async function close() { - await this.disconnect(); - - for (const wallet of this.wallets.values()) { - await wallet.destroy(); - this.unregister(wallet); - } - - await this.db.close(); -}; - -/** - * Watch addresses and outpoints. - * @private - * @returns {Promise} - */ - -WalletDB.prototype.watch = async function watch() { - const piter = this.db.iterator({ - gte: layout.p(encoding.NULL_HASH), - lte: layout.p(encoding.HIGH_HASH) - }); - - let hashes = 0; - - await piter.each((key) => { - const data = layout.pp(key); - - this.filter.add(data, 'hex'); - - hashes += 1; - }); - - this.logger.info('Added %d hashes to WalletDB filter.', hashes); - - const oiter = this.db.iterator({ - gte: layout.o(encoding.NULL_HASH, 0), - lte: layout.o(encoding.HIGH_HASH, 0xffffffff) - }); - - let outpoints = 0; - - await oiter.each((key) => { - const [hash, index] = layout.oo(key); - const outpoint = new Outpoint(hash, index); - const data = outpoint.toRaw(); - - this.filter.add(data); - - outpoints += 1; - }); - - this.logger.info('Added %d outpoints to WalletDB filter.', outpoints); -}; - -/** - * Connect to the node server (client required). - * @returns {Promise} - */ - -WalletDB.prototype.connect = async function connect() { - return this.client.open(); -}; - -/** - * Disconnect from node server (client required). - * @returns {Promise} - */ - -WalletDB.prototype.disconnect = async function disconnect() { - return this.client.close(); -}; - -/** - * Sync state with server on every connect. - * @returns {Promise} - */ - -WalletDB.prototype.syncNode = async function syncNode() { - const unlock = await this.txLock.lock(); - try { - this.logger.info('Resyncing from server...'); - await this.syncState(); - await this.syncFilter(); - await this.syncChain(); - await this.resend(); - } finally { - unlock(); - } -}; - -/** - * Initialize and write initial sync state. - * @returns {Promise} - */ - -WalletDB.prototype.syncState = async function syncState() { - const cache = await this.getState(); - - if (cache) { - this.state = cache; - this.height = cache.height; - return; - } - - this.logger.info('Initializing database state from server.'); - - const b = this.db.batch(); - - let tip = null; - - const hashes = await this.client.getHashes(); - - for (let height = 0; height < hashes.length; height++) { - const hash = hashes[height]; - const meta = new BlockMeta(hash, height); - b.put(layout.h(height), meta.toHash()); - tip = meta; - } - - assert(tip); - - const state = this.state.clone(); - state.startHeight = tip.height; - state.startHash = tip.hash; - state.height = tip.height; - state.marked = false; - - b.put(layout.R, state.toRaw()); - - await b.write(); - - this.state = state; - this.height = state.height; -}; - -/** - * Connect and sync with the chain server. - * @private - * @returns {Promise} - */ - -WalletDB.prototype.syncChain = async function syncChain() { - let height = this.state.height; - - this.logger.info('Syncing state from height %d.', height); - - for (;;) { - const tip = await this.getBlock(height); - assert(tip); - - if (await this.client.getEntry(tip.hash)) - break; - - assert(height !== 0); - height -= 1; - } - - await this.scan(height); -}; - -/** - * Rescan blockchain from a given height. - * @private - * @param {Number?} height - * @returns {Promise} - */ - -WalletDB.prototype.scan = async function scan(height) { - if (height == null) - height = this.state.startHeight; - - assert((height >>> 0) === height, 'WDB: Must pass in a height.'); - - await this.rollback(height); - - this.logger.info( - 'WalletDB is scanning %d blocks.', - this.state.height - height + 1); - - const tip = await this.getTip(); - - try { - this.rescanning = true; - await this.client.rescan(tip.hash); - } finally { - this.rescanning = false; - } -}; - -/** - * Force a rescan. - * @param {Number} height - * @returns {Promise} - */ - -WalletDB.prototype.rescan = async function rescan(height) { - const unlock = await this.txLock.lock(); - try { - return await this._rescan(height); - } finally { - unlock(); - } -}; - -/** - * Force a rescan (without a lock). - * @private - * @param {Number} height - * @returns {Promise} - */ - -WalletDB.prototype._rescan = async function _rescan(height) { - return this.scan(height); -}; - -/** - * Broadcast a transaction via chain server. - * @param {TX} tx - * @returns {Promise} - */ - -WalletDB.prototype.send = async function send(tx) { - return this.client.send(tx); -}; - -/** - * Estimate smart fee from chain server. - * @param {Number} blocks - * @returns {Promise} - */ - -WalletDB.prototype.estimateFee = async function estimateFee(blocks) { - if (this.feeRate > 0) - return this.feeRate; - - const rate = await this.client.estimateFee(blocks); - - if (rate < this.network.feeRate) - return this.network.feeRate; - - if (rate > this.network.maxFeeRate) - return this.network.maxFeeRate; - - return rate; -}; - -/** - * Send filter to the remote node. - * @private - * @returns {Promise} - */ - -WalletDB.prototype.syncFilter = function syncFilter() { - this.logger.info('Sending filter to server (%dmb).', - this.filter.size / 8 / (1 << 20)); - - return this.client.setFilter(this.filter); -}; - -/** - * Add data to remote filter. - * @private - * @param {Buffer} data - * @returns {Promise} - */ - -WalletDB.prototype.addFilter = function addFilter(data) { - return this.client.addFilter(data); -}; - -/** - * Reset remote filter. - * @private - * @returns {Promise} - */ - -WalletDB.prototype.resetFilter = function resetFilter() { - return this.client.resetFilter(); -}; - -/** - * Backup the wallet db. - * @param {String} path - * @returns {Promise} - */ - -WalletDB.prototype.backup = function backup(path) { - return this.db.backup(path); -}; - -/** - * Wipe the txdb - NEVER USE. - * @returns {Promise} - */ - -WalletDB.prototype.wipe = async function wipe() { - this.logger.warning('Wiping WalletDB TXDB...'); - this.logger.warning('I hope you know what you\'re doing.'); - - const iter = this.db.iterator({ - gte: Buffer.from([0x00]), - lte: Buffer.from([0xff]) - }); - - const b = this.db.batch(); - - let total = 0; - - await iter.each((key) => { - switch (key[0]) { - case 0x62: // b - case 0x63: // c - case 0x65: // e - case 0x74: // t - case 0x6f: // o - case 0x68: // h - case 0x52: // R - b.del(key); - total += 1; - break; - } - }); - - this.logger.warning('Wiped %d txdb records.', total); - - await b.write(); -}; - -/** - * Get current wallet wid depth. - * @private - * @returns {Promise} - */ - -WalletDB.prototype.getDepth = async function getDepth() { - // This may seem like a strange way to do - // this, but updating a global state when - // creating a new wallet is actually pretty - // damn tricky. There would be major atomicity - // issues if updating a global state inside - // a "scoped" state. So, we avoid all the - // nonsense of adding a global lock to - // walletdb.create by simply seeking to the - // highest wallet wid. - const iter = this.db.iterator({ - gte: layout.w(0x00000000), - lte: layout.w(0xffffffff), - reverse: true, - limit: 1 - }); - - if (!await iter.next()) - return 1; - - const {key} = iter; - - await iter.end(); - - const depth = layout.ww(key); - - return depth + 1; -}; - -/** - * Test the bloom filter against a tx or address hash. - * @private - * @param {Hash} hash - * @returns {Boolean} - */ - -WalletDB.prototype.testFilter = function testFilter(data) { - return this.filter.test(data, 'hex'); -}; - -/** - * Add hash to local and remote filters. - * @private - * @param {Hash} hash - */ - -WalletDB.prototype.addHash = function addHash(hash) { - this.filter.add(hash, 'hex'); - return this.addFilter(hash); -}; - -/** - * Add outpoint to local filter. - * @private - * @param {Hash} hash - * @param {Number} index - */ - -WalletDB.prototype.addOutpoint = function addOutpoint(hash, index) { - const outpoint = new Outpoint(hash, index); - this.filter.add(outpoint.toRaw()); -}; - -/** - * Dump database (for debugging). - * @returns {Promise} - Returns Object. - */ - -WalletDB.prototype.dump = function dump() { - return this.db.dump(); -}; - -/** - * Register an object with the walletdb. - * @param {Object} object - */ - -WalletDB.prototype.register = function register(wallet) { - assert(!this.wallets.has(wallet.wid)); - this.wallets.set(wallet.wid, wallet); -}; - -/** - * Unregister a object with the walletdb. - * @param {Object} object - * @returns {Boolean} - */ - -WalletDB.prototype.unregister = function unregister(wallet) { - assert(this.wallets.has(wallet.wid)); - this.wallets.delete(wallet.wid); -}; - -/** - * Map wallet id to wid. - * @param {String} id - * @returns {Promise} - Returns {WalletID}. - */ - -WalletDB.prototype.getWID = async function getWID(id) { - if (!id) - return null; - - if (typeof id === 'number') - return id; - - const data = await this.db.get(layout.l(id)); - - if (!data) - return null; - - assert(data.length === 4); - - return data.readUInt32LE(0, true); -}; - -/** - * Get a wallet from the database, setup watcher. - * @param {WalletID} wid - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype.get = async function get(id) { - const wid = await this.getWID(id); - - if (!wid) - return null; - - const unlock = await this.readLock.lock(wid); - - try { - return await this._get(wid); - } finally { - unlock(); - } -}; - -/** - * Get a wallet from the database without a lock. - * @private - * @param {WalletID} wid - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype._get = async function _get(wid) { - const cache = this.wallets.get(wid); - - if (cache) - return cache; - - const data = await this.db.get(layout.w(wid)); - - if (!data) - return null; - - const wallet = Wallet.fromRaw(this, data); - - await wallet.open(); - - this.register(wallet); - - return wallet; -}; - -/** - * Save a wallet to the database. - * @param {Wallet} wallet - */ - -WalletDB.prototype.save = function save(b, wallet) { - const wid = wallet.wid; - const id = wallet.id; - - b.put(layout.w(wid), wallet.toRaw()); - b.put(layout.l(id), u32(wid)); -}; - -/** - * Rename a wallet. - * @param {Wallet} wallet - * @param {String} id - * @returns {Promise} - */ - -WalletDB.prototype.rename = async function rename(wallet, id) { - const unlock = await this.writeLock.lock(); - try { - return await this._rename(wallet, id); - } finally { - unlock(); - } -}; - -/** - * Rename a wallet without a lock. - * @private - * @param {Wallet} wallet - * @param {String} id - * @returns {Promise} - */ - -WalletDB.prototype._rename = async function _rename(wallet, id) { - if (!common.isName(id)) - throw new Error('WDB: Bad wallet ID.'); - - if (await this.has(id)) - throw new Error('WDB: ID not available.'); - - const old = wallet.id; - const b = this.db.batch(); - - b.del(layout.l(old)); - - wallet.id = id; - - this.save(b, wallet); - - await b.write(); -}; - -/** - * Rename an account. - * @param {Account} account - * @param {String} name - */ - -WalletDB.prototype.renameAccount = function renameAccount(b, account, name) { - // Remove old wid/name->account index. - b.del(layout.i(account.wid, account.name)); - - account.name = name; - - this.saveAccount(b, account); -}; - -/** - * Get a wallet with token auth first. - * @param {WalletID} wid - * @param {Buffer} token - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype.auth = async function auth(wid, token) { - const wallet = await this.get(wid); - - if (!wallet) - return null; - - // Compare in constant time: - if (!ccmp(token, wallet.token)) - throw new Error('WDB: Authentication error.'); - - return wallet; -}; - -/** - * Create a new wallet, save to database, setup watcher. - * @param {Object} options - See {@link Wallet}. - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype.create = async function create(options) { - const unlock = await this.writeLock.lock(); - - if (!options) - options = {}; - - try { - return await this._create(options); - } finally { - unlock(); - } -}; - -/** - * Create a new wallet, save to database without a lock. - * @private - * @param {Object} options - See {@link Wallet}. - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype._create = async function _create(options) { - if (await this.has(options.id)) - throw new Error('WDB: Wallet already exists.'); - - const wallet = Wallet.fromOptions(this, options); - - wallet.wid = this.depth; - - await wallet.init(options); - - this.depth += 1; - - this.register(wallet); - - this.logger.info('Created wallet %s in WalletDB.', wallet.id); - - return wallet; -}; - -/** - * Test for the existence of a wallet. - * @param {WalletID} id - * @returns {Promise} - */ - -WalletDB.prototype.has = async function has(id) { - const wid = await this.getWID(id); - return wid != null; -}; - -/** - * Attempt to create wallet, return wallet if already exists. - * @param {Object} options - See {@link Wallet}. - * @returns {Promise} - */ - -WalletDB.prototype.ensure = async function ensure(options) { - const wallet = await this.get(options.id); - - if (wallet) - return wallet; - - return this.create(options); -}; - -/** - * Get an account from the database by wid. - * @private - * @param {WalletID} wid - * @param {Number} index - Account index. - * @returns {Promise} - Returns {@link Wallet}. - */ - -WalletDB.prototype.getAccount = async function getAccount(wid, index) { - const data = await this.db.get(layout.a(wid, index)); - - if (!data) - return null; - - return Account.fromRaw(this, data); -}; - -/** - * List account names and indexes from the db. - * @param {WalletID} wid - * @returns {Promise} - Returns Array. - */ - -WalletDB.prototype.getAccounts = function getAccounts(wid) { - return this.db.values({ - gte: layout.n(wid, 0x00000000), - lte: layout.n(wid, 0xffffffff), - parse: data => data.toString('ascii') - }); -}; - -/** - * Lookup the corresponding account name's index. - * @param {WalletID} wid - * @param {String} name - Account name/index. - * @returns {Promise} - Returns Number. - */ - -WalletDB.prototype.getAccountIndex = async function getAccountIndex(wid, name) { - const index = await this.db.get(layout.i(wid, name)); - - if (!index) - return -1; - - return index.readUInt32LE(0, true); -}; - -/** - * Lookup the corresponding account index's name. - * @param {WalletID} wid - * @param {Number} index - * @returns {Promise} - Returns Number. - */ - -WalletDB.prototype.getAccountName = async function getAccountName(wid, index) { - const name = await this.db.get(layout.n(wid, index)); - - if (!name) - return null; - - return name.toString('ascii'); -}; - -/** - * Save an account to the database. - * @param {Account} account - * @returns {Promise} - */ - -WalletDB.prototype.saveAccount = function saveAccount(b, account) { - const wid = account.wid; - const index = account.accountIndex; - const name = account.name; - - // Account data - b.put(layout.a(wid, index), account.toRaw()); - - // Name->Index lookups - b.put(layout.i(wid, name), u32(index)); - - // Index->Name lookups - b.put(layout.n(wid, index), Buffer.from(name, 'ascii')); -}; - -/** - * Test for the existence of an account. - * @param {WalletID} wid - * @param {String|Number} acct - * @returns {Promise} - Returns Boolean. - */ - -WalletDB.prototype.hasAccount = function hasAccount(wid, index) { - return this.db.has(layout.a(wid, index)); -}; - -/** - * Save an address to the path map. - * @param {Wallet} wallet - * @param {WalletKey} ring - * @returns {Promise} - */ - -WalletDB.prototype.saveKey = function saveKey(b, wid, ring) { - return this.savePath(b, wid, ring.toPath()); -}; - -/** - * Save a path to the path map. - * - * The path map exists in the form of: - * - `p[address-hash] -> wid map` - * - `P[wid][address-hash] -> path data` - * - `r[wid][account-index][address-hash] -> dummy` - * - * @param {Wallet} wallet - * @param {Path} path - * @returns {Promise} - */ - -WalletDB.prototype.savePath = async function savePath(b, wid, path) { - // Address Hash -> Wallet Map - await this.addPathMap(b, path.hash, wid); - - // Wallet ID + Address Hash -> Path Data - b.put(layout.P(wid, path.hash), path.toRaw()); - - // Wallet ID + Account Index + Address Hash -> Dummy - b.put(layout.r(wid, path.account, path.hash), null); -}; - -/** - * Retrieve path by hash. - * @param {WalletID} wid - * @param {Hash} hash - * @returns {Promise} - */ - -WalletDB.prototype.getPath = async function getPath(wid, hash) { - const path = await this.readPath(wid, hash); - - if (!path) - return null; - - path.name = await this.getAccountName(wid, path.account); - assert(path.name); - - return path; -}; - -/** - * Retrieve path by hash. - * @param {WalletID} wid - * @param {Hash} hash - * @returns {Promise} - */ - -WalletDB.prototype.readPath = async function readPath(wid, hash) { - const data = await this.db.get(layout.P(wid, hash)); - - if (!data) - return null; - - const path = Path.fromRaw(data); - path.hash = hash; - - return path; -}; - -/** - * Test whether a wallet contains a path. - * @param {WalletID} wid - * @param {Hash} hash - * @returns {Promise} - */ - -WalletDB.prototype.hasPath = function hasPath(wid, hash) { - return this.db.has(layout.P(wid, hash)); -}; - -/** - * Get all address hashes. - * @returns {Promise} - */ - -WalletDB.prototype.getHashes = function getHashes() { - return this.db.keys({ - gte: layout.p(encoding.NULL_HASH), - lte: layout.p(encoding.HIGH_HASH), - parse: layout.pp - }); -}; - -/** - * Get all outpoints. - * @returns {Promise} - */ - -WalletDB.prototype.getOutpoints = function getOutpoints() { - return this.db.keys({ - gte: layout.o(encoding.NULL_HASH, 0), - lte: layout.o(encoding.HIGH_HASH, 0xffffffff), - parse: (key) => { - const [hash, index] = layout.oo(key); - return new Outpoint(hash, index); - } - }); -}; - -/** - * Get all address hashes. - * @param {WalletID} wid - * @returns {Promise} - */ - -WalletDB.prototype.getWalletHashes = function getWalletHashes(wid) { - return this.db.keys({ - gte: layout.P(wid, encoding.NULL_HASH), - lte: layout.P(wid, encoding.HIGH_HASH), - parse: layout.Pp - }); -}; - -/** - * Get all account address hashes. - * @param {WalletID} wid - * @param {Number} account - * @returns {Promise} - */ - -WalletDB.prototype.getAccountHashes = function getAccountHashes(wid, account) { - return this.db.keys({ - gte: layout.r(wid, account, encoding.NULL_HASH), - lte: layout.r(wid, account, encoding.HIGH_HASH), - parse: layout.rr - }); -}; - -/** - * Get all paths for a wallet. - * @param {WalletID} wid - * @returns {Promise} - */ - -WalletDB.prototype.getWalletPaths = async function getWalletPaths(wid) { - const items = await this.db.range({ - gte: layout.P(wid, encoding.NULL_HASH), - lte: layout.P(wid, encoding.HIGH_HASH) - }); - - const paths = []; - - for (const item of items) { - const hash = layout.Pp(item.key); - const path = Path.fromRaw(item.value); - - path.hash = hash; - path.name = await this.getAccountName(wid, path.account); - assert(path.name); - - paths.push(path); - } - - return paths; -}; - -/** - * Get all wallet ids. - * @returns {Promise} - */ - -WalletDB.prototype.getWallets = function getWallets() { - return this.db.keys({ - gte: layout.l('\x00'), - lte: layout.l('\xff'), - parse: layout.ll - }); -}; - -/** - * Encrypt all imported keys for a wallet. - * @param {WalletID} wid - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.encryptKeys = async function encryptKeys(b, wid, key) { - const iter = this.db.iterator({ - gte: layout.P(wid, encoding.NULL_HASH), - lte: layout.P(wid, encoding.HIGH_HASH), - values: true - }); - - await iter.each((k, value) => { - const hash = layout.Pp(k); - const path = Path.fromRaw(value); - - if (!path.data) - return; - - assert(!path.encrypted); - - const bhash = Buffer.from(hash, 'hex'); - const iv = bhash.slice(0, 16); - - path.data = aes.encipher(path.data, key, iv); - path.encrypted = true; - - b.put(k, path.toRaw()); - }); -}; - -/** - * Decrypt all imported keys for a wallet. - * @param {WalletID} wid - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.decryptKeys = async function decryptKeys(b, wid, key) { - const iter = this.db.iterator({ - gte: layout.P(wid, encoding.NULL_HASH), - lte: layout.P(wid, encoding.HIGH_HASH), - values: true - }); - - await iter.each((k, value) => { - const hash = layout.Pp(k); - const path = Path.fromRaw(value); - - if (!path.data) - return; - - assert(path.encrypted); - - const bhash = Buffer.from(hash, 'hex'); - const iv = bhash.slice(0, 16); - - path.data = aes.decipher(path.data, key, iv); - path.encrypted = false; - - b.put(k, path.toRaw()); - }); -}; - -/** - * Resend all pending transactions. - * @returns {Promise} - */ - -WalletDB.prototype.resend = async function resend() { - const wids = await this.db.keys({ - gte: layout.w(0x00000000), - lte: layout.w(0xffffffff), - parse: k => layout.ww(k) - }); - - this.logger.info('Resending from %d wallets.', wids.length); - - for (const wid of wids) - await this.resendPending(wid); -}; - -/** - * Resend all pending transactions for a specific wallet. - * @private - * @param {WalletID} wid - * @returns {Promise} - */ - -WalletDB.prototype.resendPending = async function resendPending(wid) { - const layout = layouts.txdb; - const prefix = layout.prefix(wid); - const b = this.db.bucket(prefix); - - const hashes = await b.keys({ - gte: layout.p(encoding.NULL_HASH), - lte: layout.p(encoding.HIGH_HASH), - parse: k => layout.pp(k) - }); - - if (hashes.length === 0) - return; - - this.logger.info( - 'Rebroadcasting %d transactions for %d.', - hashes.length, - wid); - - const txs = []; - - for (const hash of hashes) { - const data = await b.get(layout.t(hash)); - - if (!data) - continue; - - const wtx = TXRecord.fromRaw(data); - - if (wtx.tx.isCoinbase()) - continue; - - txs.push(wtx.tx); - } - - for (const tx of common.sortDeps(txs)) - await this.send(tx); -}; - -/** - * Get all wallet ids by output addresses and outpoints. - * @param {Hash[]} hashes - * @returns {Promise} - */ - -WalletDB.prototype.getWalletsByTX = async function getWalletsByTX(tx) { - const result = new Set(); - - if (!tx.isCoinbase()) { - for (const {prevout} of tx.inputs) { - const {hash, index} = prevout; - - if (!this.testFilter(prevout.toRaw())) - continue; - - const map = await this.getOutpointMap(hash, index); - - if (!map) - continue; - - for (const wid of map.wids) - result.add(wid); - } - } - - const hashes = tx.getOutputHashes('hex'); - - for (const hash of hashes) { - if (!this.testFilter(hash)) - continue; - - const map = await this.getPathMap(hash); - - if (!map) - continue; - - for (const wid of map.wids) - result.add(wid); - } - - if (result.size === 0) - return null; - - return result; -}; - -/** - * Get the best block hash. - * @returns {Promise} - */ - -WalletDB.prototype.getState = async function getState() { - const data = await this.db.get(layout.R); - - if (!data) - return null; - - return ChainState.fromRaw(data); -}; - -/** - * Sync the current chain state to tip. - * @param {BlockMeta} tip - * @returns {Promise} - */ - -WalletDB.prototype.setTip = async function setTip(tip) { - const b = this.db.batch(); - const state = this.state.clone(); - - if (tip.height < state.height) { - // Hashes ahead of our new tip - // that we need to delete. - while (state.height !== tip.height) { - b.del(layout.h(state.height)); - state.height -= 1; - } - } else if (tip.height > state.height) { - assert(tip.height === state.height + 1, 'Bad chain sync.'); - state.height += 1; - } - - if (tip.height < state.startHeight) { - state.startHeight = tip.height; - state.startHash = tip.hash; - state.marked = false; - } - - // Save tip and state. - b.put(layout.h(tip.height), tip.toHash()); - b.put(layout.R, state.toRaw()); - - await b.write(); - - this.state = state; - this.height = state.height; -}; - -/** - * Mark current state. - * @param {BlockMeta} block - * @returns {Promise} - */ - -WalletDB.prototype.markState = async function markState(block) { - const state = this.state.clone(); - state.startHeight = block.height; - state.startHash = block.hash; - state.marked = true; - - const b = this.db.batch(); - b.put(layout.R, state.toRaw()); - await b.write(); - - this.state = state; - this.height = state.height; -}; - -/** - * Get a wallet map. - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.getMap = async function getMap(key) { - const data = await this.db.get(key); - - if (!data) - return null; - - return MapRecord.fromRaw(data); -}; - -/** - * Add wid to a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.addMap = async function addMap(b, key, wid) { - const data = await this.db.get(key); - - if (!data) { - const map = new MapRecord(); - map.add(wid); - b.put(key, map.toRaw()); - return; - } - - assert(data.length >= 4); - - const len = data.readUInt32LE(0, true); - const bw = bio.write(data.length + 4); - - bw.writeU32(len + 1); - bw.copy(data, 4, data.length); - bw.writeU32(wid); - - b.put(key, bw.render()); -}; - -/** - * Remove wid from a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.removeMap = async function removeMap(b, key, wid) { - const map = await this.getMap(key); - - if (!map) - return; - - if (!map.remove(wid)) - return; - - if (map.size === 0) { - b.del(key); - return; - } - - b.put(key, map.toRaw()); -}; - -/** - * Get a wallet map. - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.getPathMap = function getPathMap(hash) { - return this.getMap(layout.p(hash)); -}; - -/** - * Add wid to a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.addPathMap = async function addPathMap(b, hash, wid) { - await this.addHash(hash); - return this.addMap(b, layout.p(hash), wid); -}; - -/** - * Remove wid from a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.removePathMap = function removePathMap(b, hash, wid) { - return this.removeMap(b, layout.p(hash), wid); -}; - -/** - * Get a wallet map. - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.getBlockMap = async function getBlockMap(height) { - return this.getMap(layout.b(height)); -}; - -/** - * Add wid to a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.addBlockMap = function addBlockMap(b, height, wid) { - return this.addMap(b, layout.b(height), wid); -}; - -/** - * Remove wid from a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.removeBlockMap = function removeBlockMap(b, height, wid) { - return this.removeMap(b, layout.b(height), wid); -}; - -/** - * Get a wallet map. - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.getTXMap = function getTXMap(hash) { - return this.getMap(layout.T(hash)); -}; - -/** - * Add wid to a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.addTXMap = function addTXMap(b, hash, wid) { - return this.addMap(b, layout.T(hash), wid); -}; - -/** - * Remove wid from a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.removeTXMap = function removeTXMap(b, hash, wid) { - return this.removeMap(b, layout.T(hash), wid); -}; - -/** - * Get a wallet map. - * @param {Buffer} key - * @returns {Promise} - */ - -WalletDB.prototype.getOutpointMap = function getOutpointMap(hash, index) { - return this.getMap(layout.o(hash, index)); -}; - -/** - * Add wid to a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.addOutpointMap = async function addOutpointMap(b, hash, index, wid) { - await this.addOutpoint(hash, index); - return this.addMap(b, layout.o(hash, index), wid); -}; - -/** - * Remove wid from a wallet map. - * @param {Wallet} wallet - * @param {Buffer} key - * @param {Number} wid - */ - -WalletDB.prototype.removeOutpointMap = function removeOutpointMap(b, hash, index, wid) { - return this.removeMap(b, layout.o(hash, index), wid); -}; - -/** - * Get a wallet block meta. - * @param {Hash} hash - * @returns {Promise} - */ - -WalletDB.prototype.getBlock = async function getBlock(height) { - const data = await this.db.get(layout.h(height)); - - if (!data) - return null; - - const block = new BlockMeta(); - block.hash = data.toString('hex'); - block.height = height; - - return block; -}; - -/** - * Get wallet tip. - * @param {Hash} hash - * @returns {Promise} - */ - -WalletDB.prototype.getTip = async function getTip() { - const tip = await this.getBlock(this.state.height); - - if (!tip) - throw new Error('WDB: Tip not found!'); - - return tip; -}; - -/** - * Sync with chain height. - * @param {Number} height - * @returns {Promise} - */ - -WalletDB.prototype.rollback = async function rollback(height) { - if (height > this.state.height) - throw new Error('WDB: Cannot rollback to the future.'); - - if (height === this.state.height) { - this.logger.info('Rolled back to same height (%d).', height); - return; - } - - this.logger.info( - 'Rolling back %d WalletDB blocks to height %d.', - this.state.height - height, height); - - const tip = await this.getBlock(height); - assert(tip); - - await this.revert(tip.height); - await this.setTip(tip); -}; - -/** - * Revert TXDB to an older state. - * @param {Number} target - * @returns {Promise} - */ - -WalletDB.prototype.revert = async function revert(target) { - const iter = this.db.iterator({ - gte: layout.b(target + 1), - lte: layout.b(0xffffffff), - reverse: true, - values: true - }); - - let total = 0; - - await iter.each(async (key, value) => { - const height = layout.bb(key); - const block = MapRecord.fromRaw(value); - - for (const wid of block.wids) { - const wallet = await this.get(wid); - assert(wallet); - total += await wallet.revert(height); - } - }); - - this.logger.info('Rolled back %d WalletDB transactions.', total); -}; - -/** - * Add a block's transactions and write the new best hash. - * @param {ChainEntry} entry - * @returns {Promise} - */ - -WalletDB.prototype.addBlock = async function addBlock(entry, txs) { - const unlock = await this.txLock.lock(); - try { - return await this._addBlock(entry, txs); - } finally { - unlock(); - } -}; - -/** - * Add a block's transactions without a lock. - * @private - * @param {ChainEntry} entry - * @param {TX[]} txs - * @returns {Promise} - */ - -WalletDB.prototype._addBlock = async function _addBlock(entry, txs) { - const tip = BlockMeta.fromEntry(entry); - - if (tip.height < this.state.height) { - this.logger.warning( - 'WalletDB is connecting low blocks (%d).', - tip.height); - return 0; - } - - this.logger.debug('Adding block: %d.', entry.height); - - if (tip.height === this.state.height) { - // We let blocks of the same height - // through specifically for rescans: - // we always want to rescan the last - // block since the state may have - // updated before the block was fully - // processed (in the case of a crash). - this.logger.warning('Already saw WalletDB block (%d).', tip.height); - } else if (tip.height !== this.state.height + 1) { - await this.scan(this.state.height); - return 0; - } - - // Sync the state to the new tip. - await this.setTip(tip); - - if (this.options.checkpoints && !this.state.marked) { - if (tip.height <= this.network.lastCheckpoint) - return 0; - } - - let total = 0; - - for (const tx of txs) { - if (await this._addTX(tx, tip)) - total += 1; - } - - if (total > 0) { - this.logger.info('Connected WalletDB block %s (tx=%d).', - encoding.revHex(tip.hash), total); - } - - return total; -}; - -/** - * Unconfirm a block's transactions - * and write the new best hash (SPV version). - * @param {ChainEntry} entry - * @returns {Promise} - */ - -WalletDB.prototype.removeBlock = async function removeBlock(entry) { - const unlock = await this.txLock.lock(); - try { - return await this._removeBlock(entry); - } finally { - unlock(); - } -}; - -/** - * Unconfirm a block's transactions. - * @private - * @param {ChainEntry} entry - * @returns {Promise} - */ - -WalletDB.prototype._removeBlock = async function _removeBlock(entry) { - const tip = BlockMeta.fromEntry(entry); - - if (tip.height === 0) - throw new Error('WDB: Bad disconnection (genesis block).'); - - if (tip.height > this.state.height) { - this.logger.warning( - 'WalletDB is disconnecting high blocks (%d).', - tip.height); - return 0; - } - - if (tip.height !== this.state.height) - throw new Error('WDB: Bad disconnection (height mismatch).'); - - const prev = await this.getBlock(tip.height - 1); - assert(prev); - - // Get the map of block->wids. - const map = await this.getBlockMap(tip.height); - - if (!map) { - await this.setTip(prev); - return 0; - } - - let total = 0; - - for (const wid of map.wids) { - const wallet = await this.get(wid); - assert(wallet); - total += await wallet.revert(tip.height); - } - - // Sync the state to the previous tip. - await this.setTip(prev); - - this.logger.warning('Disconnected wallet block %s (tx=%d).', - encoding.revHex(tip.hash), total); - - return total; -}; - -/** - * Rescan a block. - * @private - * @param {ChainEntry} entry - * @param {TX[]} txs - * @returns {Promise} - */ - -WalletDB.prototype.rescanBlock = async function rescanBlock(entry, txs) { - if (!this.rescanning) { - this.logger.warning('Unsolicited rescan block: %d.', entry.height); - return; - } - - if (entry.height > this.state.height + 1) { - this.logger.warning('Rescan block too high: %d.', entry.height); - return; - } - - try { - await this._addBlock(entry, txs); - } catch (e) { - this.emit('error', e); - throw e; - } -}; - -/** - * Add a transaction to the database, map addresses - * to wallet IDs, potentially store orphans, resolve - * orphans, or confirm a transaction. - * @param {TX} tx - * @returns {Promise} - */ - -WalletDB.prototype.addTX = async function addTX(tx) { - const unlock = await this.txLock.lock(); - try { - return await this._addTX(tx); - } finally { - unlock(); - } -}; - -/** - * Add a transaction to the database without a lock. - * @private - * @param {TX} tx - * @param {BlockMeta} block - * @returns {Promise} - */ - -WalletDB.prototype._addTX = async function _addTX(tx, block) { - const wids = await this.getWalletsByTX(tx); - - assert(!tx.mutable, 'WDB: Cannot add mutable TX.'); - - if (!wids) - return null; - - if (block && !this.state.marked) - await this.markState(block); - - this.logger.info( - 'Incoming transaction for %d wallets in WalletDB (%s).', - wids.size, tx.txid()); - - let result = false; - - // Insert the transaction - // into every matching wallet. - for (const wid of wids) { - const wallet = await this.get(wid); - - assert(wallet); - - if (await wallet.add(tx, block)) { - this.logger.info( - 'Added transaction to wallet in WalletDB: %s (%d).', - wallet.id, wid); - result = true; - } - } - - if (!result) - return null; - - return wids; -}; - -/** - * Handle a chain reset. - * @param {ChainEntry} entry - * @returns {Promise} - */ - -WalletDB.prototype.resetChain = async function resetChain(entry) { - const unlock = await this.txLock.lock(); - try { - return await this._resetChain(entry); - } finally { - unlock(); - } -}; - -/** - * Handle a chain reset without a lock. - * @private - * @param {ChainEntry} entry - * @returns {Promise} - */ - -WalletDB.prototype._resetChain = async function _resetChain(entry) { - if (entry.height > this.state.height) - throw new Error('WDB: Bad reset height.'); - - await this.rollback(entry.height); -}; - -/** - * WalletOptions + * Wallet Options * @alias module:wallet.WalletOptions - * @constructor - * @param {Object} options */ -function WalletOptions(options) { - if (!(this instanceof WalletOptions)) - return new WalletOptions(options); +class WalletOptions { + /** + * Create wallet options. + * @constructor + * @param {Object} options + */ - this.network = Network.primary; - this.logger = Logger.global; - this.workers = null; - this.client = null; - this.feeRate = 0; + constructor(options) { + this.network = Network.primary; + this.logger = Logger.global; + this.workers = null; + this.client = null; + this.feeRate = 0; - this.prefix = null; - this.location = null; - this.db = 'memory'; - this.maxFiles = 64; - this.cacheSize = 16 << 20; - this.compression = true; - this.bufferKeys = layout.binary; + this.prefix = null; + this.location = null; + this.db = 'memory'; + this.maxFiles = 64; + this.cacheSize = 16 << 20; + this.compression = true; + this.bufferKeys = layout.binary; - this.spv = false; - this.witness = false; - this.checkpoints = false; - this.startHeight = 0; - this.wipeNoReally = false; + this.spv = false; + this.witness = false; + this.checkpoints = false; + this.startHeight = 0; + this.wipeNoReally = false; - if (options) - this.fromOptions(options); + if (options) + this.fromOptions(options); + } + + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {WalletOptions} + */ + + fromOptions(options) { + if (options.network != null) + this.network = Network.get(options.network); + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.workers != null) { + assert(typeof options.workers === 'object'); + this.workers = options.workers; + } + + if (options.client != null) { + assert(typeof options.client === 'object'); + this.client = options.client; + } + + if (options.feeRate != null) { + assert((options.feeRate >>> 0) === options.feeRate); + this.feeRate = options.feeRate; + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.location = path.join(this.prefix, 'walletdb'); + } + + if (options.location != null) { + assert(typeof options.location === 'string'); + this.location = options.location; + } + + if (options.db != null) { + assert(typeof options.db === 'string'); + this.db = options.db; + } + + if (options.maxFiles != null) { + assert((options.maxFiles >>> 0) === options.maxFiles); + this.maxFiles = options.maxFiles; + } + + if (options.cacheSize != null) { + assert(Number.isSafeInteger(options.cacheSize) && options.cacheSize >= 0); + this.cacheSize = options.cacheSize; + } + + if (options.compression != null) { + assert(typeof options.compression === 'boolean'); + this.compression = options.compression; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + + if (options.checkpoints != null) { + assert(typeof options.checkpoints === 'boolean'); + this.checkpoints = options.checkpoints; + } + + if (options.startHeight != null) { + assert(typeof options.startHeight === 'number'); + assert(options.startHeight >= 0); + this.startHeight = options.startHeight; + } + + if (options.wipeNoReally != null) { + assert(typeof options.wipeNoReally === 'boolean'); + this.wipeNoReally = options.wipeNoReally; + } + + return this; + } + + /** + * Instantiate chain options from object. + * @param {Object} options + * @returns {WalletOptions} + */ + + static fromOptions(options) { + return new this().fromOptions(options); + } } -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {WalletOptions} - */ - -WalletOptions.prototype.fromOptions = function fromOptions(options) { - if (options.network != null) - this.network = Network.get(options.network); - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.workers != null) { - assert(typeof options.workers === 'object'); - this.workers = options.workers; - } - - if (options.client != null) { - assert(typeof options.client === 'object'); - this.client = options.client; - } - - if (options.feeRate != null) { - assert((options.feeRate >>> 0) === options.feeRate); - this.feeRate = options.feeRate; - } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.location = path.join(this.prefix, 'walletdb'); - } - - if (options.location != null) { - assert(typeof options.location === 'string'); - this.location = options.location; - } - - if (options.db != null) { - assert(typeof options.db === 'string'); - this.db = options.db; - } - - if (options.maxFiles != null) { - assert((options.maxFiles >>> 0) === options.maxFiles); - this.maxFiles = options.maxFiles; - } - - if (options.cacheSize != null) { - assert(Number.isSafeInteger(options.cacheSize) && options.cacheSize >= 0); - this.cacheSize = options.cacheSize; - } - - if (options.compression != null) { - assert(typeof options.compression === 'boolean'); - this.compression = options.compression; - } - - if (options.spv != null) { - assert(typeof options.spv === 'boolean'); - this.spv = options.spv; - } - - if (options.witness != null) { - assert(typeof options.witness === 'boolean'); - this.witness = options.witness; - } - - if (options.checkpoints != null) { - assert(typeof options.checkpoints === 'boolean'); - this.checkpoints = options.checkpoints; - } - - if (options.startHeight != null) { - assert(typeof options.startHeight === 'number'); - assert(options.startHeight >= 0); - this.startHeight = options.startHeight; - } - - if (options.wipeNoReally != null) { - assert(typeof options.wipeNoReally === 'boolean'); - this.wipeNoReally = options.wipeNoReally; - } - - return this; -}; - -/** - * Instantiate chain options from object. - * @param {Object} options - * @returns {WalletOptions} - */ - -WalletOptions.fromOptions = function fromOptions(options) { - return new WalletOptions().fromOptions(options); -}; - /* * Expose */ diff --git a/lib/wallet/walletkey.js b/lib/wallet/walletkey.js index fe0ac9cc..36a0bbee 100644 --- a/lib/wallet/walletkey.js +++ b/lib/wallet/walletkey.js @@ -36,79 +36,6 @@ class WalletKey extends KeyRing { this.index = -1; } - /** - * Instantiate key ring from options. - * @param {Object} options - * @returns {WalletKey} - */ - - static fromOptions(options) { - return new this().fromOptions(options); - } - - /** - * Instantiate wallet key from a private key. - * @param {Buffer} key - * @param {Boolean?} compressed - * @returns {WalletKey} - */ - - static fromPrivate(key, compressed) { - return new this().fromPrivate(key, compressed); - } - - /** - * Generate a wallet key. - * @param {Boolean?} compressed - * @returns {WalletKey} - */ - - static generate(compressed) { - return new this().generate(compressed); - } - - /** - * Instantiate wallet key from a public key. - * @param {Buffer} publicKey - * @returns {WalletKey} - */ - - static fromPublic(key) { - return new this().fromPublic(key); - } - - /** - * Instantiate wallet key from a public key. - * @param {Buffer} publicKey - * @returns {WalletKey} - */ - - static fromKey(key, compressed) { - return new this().fromKey(key, compressed); - } - - /** - * Instantiate wallet key from script. - * @param {Buffer} key - * @param {Script} script - * @returns {WalletKey} - */ - - static fromScript(key, script, compressed) { - return new this().fromScript(key, script, compressed); - } - - /** - * Instantiate a wallet key from a serialized CBitcoinSecret. - * @param {Base58String} secret - * @param {Network?} network - * @returns {WalletKey} - */ - - static fromSecret(data, network) { - return new this().fromSecret(data, network); - } - /** * Convert an WalletKey to a more json-friendly object. * @returns {Object} @@ -130,26 +57,6 @@ class WalletKey extends KeyRing { }; } - /** - * Instantiate an WalletKey from a jsonified transaction object. - * @param {Object} json - The jsonified transaction object. - * @returns {WalletKey} - */ - - static fromJSON(json) { - return new this().fromJSON(json); - } - - /** - * Instantiate a wallet key from serialized data. - * @param {Buffer} data - * @returns {WalletKey} - */ - - static fromRaw(data) { - return new this().fromRaw(data); - } - /** * Inject properties from hd key. * @private