UNPKG

hsd

Version:
2,059 lines (1,640 loc) 139 kB
/*! * wallet.js - wallet object for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const EventEmitter = require('events'); const {Lock} = require('bmutex'); const base58 = require('bcrypto/lib/encoding/base58'); const bio = require('bufio'); const blake2b = require('bcrypto/lib/blake2b'); const cleanse = require('bcrypto/lib/cleanse'); const bufmap = require('buffer-map'); const BufferSet = bufmap.BufferSet; const TXDB = require('./txdb'); const Path = require('./path'); const common = require('./common'); const Address = require('../primitives/address'); const MTX = require('../primitives/mtx'); const Script = require('../script/script'); const CoinView = require('../coins/coinview'); const WalletCoinView = require('./walletcoinview'); const WalletKey = require('./walletkey'); const HDPrivateKey = require('../hd/private'); const HDPublicKey = require('../hd/public'); const Mnemonic = require('../hd/mnemonic'); const HD = require('../hd/hd'); const Output = require('../primitives/output'); const Account = require('./account'); const MasterKey = require('./masterkey'); const policy = require('../protocol/policy'); const consensus = require('../protocol/consensus'); const rules = require('../covenants/rules'); const {Resource} = require('../dns/resource'); const Claim = require('../primitives/claim'); const reserved = require('../covenants/reserved'); const {ownership} = require('../covenants/ownership'); const {states} = require('../covenants/namestate'); const {types} = rules; const Coin = require('../primitives/coin'); const Outpoint = require('../primitives/outpoint'); const { AbstractCoinSource, CoinSelector } = require('../utils/coinselector'); /** @typedef {import('bdb').DB} DB */ /** @typedef {ReturnType<DB['batch']>} Batch */ /** @typedef {import('../types').Base58String} Base58String */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').Amount} Amount */ /** @typedef {import('../types').Rate} Rate */ /** @typedef {import('../covenants/namestate')} NameState */ /** @typedef {import('../primitives/tx')} TX */ /** @typedef {import('./records').BlockMeta} BlockMeta */ /** @typedef {import('./records').TXRecord} TXRecord */ /** @typedef {import('./txdb').BlockExtraInfo} BlockExtraInfo */ /** @typedef {import('./txdb').Details} Details */ /** @typedef {import('./txdb').Credit} Credit */ /** @typedef {import('./txdb').Balance} Balance */ /** @typedef {import('./txdb').BlindBid} BlindBid */ /** @typedef {import('./txdb').BidReveal} BidReveal */ /** @typedef {import('./txdb').BlindValue} BlindValue */ /** @typedef {import('./txdb').BlockRecord} BlockRecord */ /** @typedef {import('./walletdb')} WalletDB */ /* * Constants */ const EMPTY = Buffer.alloc(0); const DEFAULT_SELECTION = common.coinSelectionTypes.DB_VALUE; /** * @typedef {Object} AddResult * @property {Details} details * @property {WalletKey[]} derived */ /** * Wallet * @alias module:wallet.Wallet * @extends EventEmitter */ class Wallet extends EventEmitter { /** * Create a wallet. * @constructor * @param {WalletDB} wdb * @param {Object} options */ constructor(wdb, options) { super(); 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.wid = 0; /** @type {String|null} */ this.id = null; this.watchOnly = false; this.accountDepth = 0; this.token = consensus.ZERO_HASH; this.tokenDepth = 0; this.master = new MasterKey(); this.txdb = new TXDB(this.wdb); this.maxAncestors = policy.MEMPOOL_MAX_ANCESTORS; this.absurdFactor = policy.ABSURD_FEE_FACTOR; if (options) this.fromOptions(options); } /** * Inject properties from options object. * @param {Object} options */ fromOptions(options) { if (!options) return this; let key = options.master; let mnemonic = options.mnemonic; let id, token; if (key) { if (typeof key === 'string') key = HDPrivateKey.fromBase58(key, this.network); assert(HDPrivateKey.isHDPrivateKey(key), 'Must create wallet with hd private key.'); } else { if (typeof mnemonic === 'string') mnemonic = new Mnemonic({ phrase: mnemonic }); if (!mnemonic) mnemonic = new Mnemonic({ language: options.language }); key = HDPrivateKey.fromMnemonic(mnemonic, options.bip39Passphrase); } this.master.fromKey(key, mnemonic); 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.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 (options.maxAncestors != null) { assert((options.maxAncestors >>> 0) === options.maxAncestors); this.maxAncestors = options.maxAncestors; } if (options.absurdFactor != null) { assert((options.absurdFactor >>> 0) === options.absurdFactor); this.absurdFactor = options.absurdFactor; } if (!id) id = this.getID(); 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} */ static fromOptions(wdb, options) { return new this(wdb).fromOptions(options); } /** * Attempt to intialize the wallet (generating * the first addresses along with the lookahead * addresses). Called automatically from the * walletdb. * @param {Object} options * @param {(String|Buffer)?} [passphrase] * @returns {Promise} */ async init(options, passphrase) { if (passphrase) await this.master.encrypt(passphrase); const account = await this._createAccount(options, passphrase); assert(account); await this.txdb.open(this); this.logger.info('Wallet initialized (%s).', this.id); } /** * Open wallet (done after retrieval). * @returns {Promise} */ async open() { const account = await this.getAccount(0); if (!account) throw new Error('Default account not found.'); await this.txdb.open(this); this.logger.info('Wallet opened (%s).', this.id); } /** * 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(); } } /** * 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<Boolean>} */ async 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<Boolean>} */ 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; } /** * Remove a public account key from the wallet (multisig). * @param {(Number|String)} acct * @param {HDPublicKey} key * @returns {Promise<Boolean>} */ async 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<Boolean>} */ async _removeSharedKey(acct, key) { const account = await this.getAccount(acct); if (!account) throw new Error('Account not found.'); const b = this.db.batch(); const result = 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} */ async setPassphrase(passphrase, old) { if (old != null) await this.decrypt(old); await this.encrypt(passphrase); } /** * 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<Buffer>} */ 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<Buffer>} */ 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} 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} 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 BLAKE2b(m/44->public|magic, 20) * 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 = blake2b.digest(bw.render(), 20); const b58 = bio.write(23); b58.writeU8(0x03); b58.writeU8(0xbe); b58.writeU8(0x04); b58.writeBytes(hash); return base58.encode(b58.render()); } /** * Generate the wallet api key if none was passed in. * It is represented as BLAKE2b(m/44'->private|nonce). * @private * @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 blake2b.digest(bw.render()); } /** * Create an account. Requires passphrase if master key is encrypted. * @param {Object} options - See {@link Account} options. * @param {(String|Buffer)?} [passphrase] * @returns {Promise<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. * @param {(String|Buffer)?} [passphrase] * @returns {Promise<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.'); await this.unlock(passphrase); let key; if (this.watchOnly) { key = options.accountKey; if (typeof key === 'string') key = HDPublicKey.fromBase58(key, this.network); if (!HDPublicKey.isHDPublicKey(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, watchOnly: this.watchOnly, accountKey: key, accountIndex: this.accountDepth, type: options.type, m: options.m, n: options.n, keys: options.keys, lookahead: options.lookahead }; 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); if (this.accountDepth === 1) this.increment(b); await b.write(); return account; } /** * Modify an account. Requires passphrase if master key is encrypted. * @param {String|Number} acct * @param {Object} options * @param {String} [passphrase] * @returns {Promise<Account>} */ async modifyAccount(acct, options, passphrase) { const unlock = await this.writeLock.lock(); try { return await this._modifyAccount(acct, options, passphrase); } finally { unlock(); } } /** * Create an account without a lock. * @param {String|Number} acct * @param {Object} options * @param {(String|Buffer)?} [passphrase] * @returns {Promise<Account>} */ async _modifyAccount(acct, options, passphrase) { if (!await this.hasAccount(acct)) throw new Error(`Account ${acct} does not exist.`); await this.unlock(passphrase); const account = await this.getAccount(acct); assert(account); const b = this.db.batch(); if (options.lookahead != null) await account.setLookahead(b, options.lookahead); await b.write(); return account; } /** * Ensure an account. Requires passphrase if master key is encrypted. * @param {Object} options - See {@link Account} options. * @param {(String|Buffer)?} [passphrase] * @returns {Promise<Account>} */ async ensureAccount(options, passphrase) { const name = options.name; const account = await this.getAccount(name); if (account) return account; return this.createAccount(options, passphrase); } /** * List account names and indexes from the db. * @returns {Promise<String[]>} - Returns Array. */ getAccounts() { return this.wdb.getAccounts(this.wid); } /** * Get all wallet address hashes. * @param {(String|Number)?} acct * @returns {Promise<Hash[]>} */ getAddressHashes(acct) { if (acct != null) return this.getAccountHashes(acct); return this.wdb.getWalletHashes(this.wid); } /** * Get all account address hashes. * @param {String|Number} acct * @returns {Promise<Hash[]>} - Returns Array. * @throws on non-existent account */ async getAccountHashes(acct) { const index = await this.getAccountIndex(acct); if (index === -1) throw new Error('Account not found.'); return this.wdb.getAccountHashes(this.wid, index); } /** * Retrieve an account from the database. * @param {Number|String} acct * @returns {Promise<Account|null>} */ async getAccount(acct) { const index = await this.getAccountIndex(acct); if (index === -1) return null; const account = await this.wdb.getAccount(this.wid, index); if (!account) return null; account.wid = this.wid; account.id = this.id; account.watchOnly = this.watchOnly; return account; } /** * Lookup the corresponding account name's index. * @param {String|Number} acct - Account name/index. * @returns {Promise<Number>} */ async getAccountIndex(acct) { if (acct == null) return -1; if (typeof acct === 'number') return acct; return this.wdb.getAccountIndex(this.wid, acct); } /** * Lookup the corresponding account name's index. * @param {(String|Number)?} [acct] - Account name/index. * @returns {Promise<Number>} * @throws on non-existent account */ async ensureIndex(acct) { if (acct == null || acct === -1) return -1; const index = await this.getAccountIndex(acct); if (index === -1) throw new Error('Account not found.'); return index; } /** * Lookup the corresponding account index's name. * @param {(String|Number)} index - Account index. * @returns {Promise<String|null>} */ async 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<Boolean>} */ async hasAccount(acct) { const index = await this.getAccountIndex(acct); if (index === -1) return false; return this.wdb.hasAccount(this.wid, index); } /** * Create a new receiving address (increments receiveDepth). * @param {(Number|String)?} acct * @returns {Promise<WalletKey>} */ createReceive(acct = 0) { return this.createKey(acct, 0); } /** * Create a new change address (increments changeDepth). * @param {(Number|String)?} acct * @returns {Promise<WalletKey>} */ createChange(acct = 0) { return this.createKey(acct, 1); } /** * Create a new address (increments depth). * @param {(Number|String)?} acct * @param {Number} branch * @returns {Promise<WalletKey>} */ async 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} branch * @returns {Promise<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; } /** * Save the wallet to the database. Necessary * when address depth and keys change. * @param {Batch} b * @returns {void} */ save(b) { return this.wdb.save(b, this); } /** * Increment the wid depth. * @param {Batch} b * @returns {void} */ increment(b) { return this.wdb.increment(b, this.wid); } /** * Test whether the wallet possesses an address. * @param {Address|Hash} address * @returns {Promise<Boolean>} */ async hasAddress(address) { const hash = Address.getHash(address); const path = await this.getPath(hash); return path != null; } /** * Get path by address hash. * @param {Address|Hash} address * @returns {Promise<Path|null>} */ async getPath(address) { const hash = Address.getHash(address); return this.wdb.getPath(this.wid, hash); } /** * Get path by address hash (without account name). * @private * @param {Address|Hash} address * @returns {Promise<Path|null>} */ async readPath(address) { const hash = Address.getHash(address); return this.wdb.readPath(this.wid, hash); } /** * Test whether the wallet contains a path. * @param {Address|Hash} address * @returns {Promise<Boolean>} */ async hasPath(address) { const hash = Address.getHash(address); return this.wdb.hasPath(this.wid, hash); } /** * Get all wallet paths. * @param {(String|Number)?} acct * @returns {Promise<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<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); } return result; } /** * 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 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} */ 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 hash = ring.getHash(); 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 {Address} address * @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 {Address} address * @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(); } /** * Import a name. * Rescanning must be invoked manually. * @param {String} name * @returns {Promise} */ async importName(name) { const unlock = await this.writeLock.lock(); try { return await this._importName(name); } finally { unlock(); } } /** * Import a name without a lock. * @private * @param {String} name * @returns {Promise} */ async _importName(name) { const nameHash = rules.hashName(name); if (await this.txdb.hasNameState(nameHash)) throw new Error('Name already exists.'); const b = this.db.batch(); await this.wdb.addNameMap(b, nameHash, this.wid); 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 `random`, `age`, `db-age`, `value`, `db-value`, `all`, `db-all` * or `db-sweepdust` * (default=`db-value`) * @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. * @param {Number} [options.sweepdustMinValue=1] - Minimum value for * sweepdust value. * @param {Boolean} [force] */ async fund(mtx, options, force) { const unlock = await this.fundLock.lock(force); try { return await this.fill(mtx, options); } finally { unlock(); } } /** * Fill a transaction with inputs without a lock. * @param {MTX} mtx * @param {Object} [options] * @returns {Promise} */ async fill(mtx, options = {}) { const acct = options.account || 0; const change = await this.changeAddress(acct === -1 ? 0 : acct); if (!change) throw new Error('Account not found.'); let rate = options.rate; if (rate == null) rate = await this.wdb.estimateFee(options.blocks); const selected = await this.select(mtx, { // we use options.account to maintain the same behaviour account: await this.ensureIndex(options.account), smart: options.smart, selection: options.selection, // sweepdust options sweepdustMinValue: options.sweepdustMinValue, round: options.round, depth: options.depth, hardFee: options.hardFee, subtractFee: options.subtractFee, subtractIndex: options.subtractIndex, changeAddress: change, height: this.wdb.height, coinbaseMaturity: this.network.coinbaseMaturity, rate: rate, maxFee: options.maxFee, estimate: prev => this.estimateSize(prev) }); mtx.fill(selected); return; } /** * Select coins for the transaction. * @param {MTX} mtx * @param {Object} [options] * @returns {Promise<CoinSelector>} */ async select(mtx, options) { const selection = options.selection || DEFAULT_SELECTION; switch (selection) { case 'all': case 'random': case 'value': case 'age': { let coins = options.coins || []; assert(Array.isArray(coins)); if (options.smart) { const smartCoins = await this.getSmartCoins(options.account); coins = coins.concat(smartCoins); } else { let availableCoins = await this.getCoins(options.account); availableCoins = this.txdb.filterLocked(availableCoins); coins = coins.concat(availableCoins); } return mtx.selectCoins(coins, options); } } const source = new WalletCoinSource(this, options); await source.init(); if (selection === common.coinSelectionTypes.DB_ALL) options.selectAll = true; const selector = new CoinSelector(mtx, source, options); await selector.select(); await source.end(); return selector; } /** * Get public keys at index based on * address and value for nonce generation * @param {Address} address * @param {Amount} value * @returns {Promise<Buffer[]>} public keys */ async _getNoncePublicKeys(address, value) { const path = await this.getPath(address.hash); if (!path) throw new Error('Account not found.'); const account = await this.getAccount(path.account); if (!account) throw new Error('Account not found.'); const hi = (value * (1 / 0x100000000)) >>> 0; const lo = value >>> 0; const index = (hi ^ lo) & 0x7fffffff; const publicKeys = []; for (const accountKey of [account.accountKey, ...account.keys]) publicKeys.push(accountKey.derive(index).publicKey); // Use smallest public key publicKeys.sort(Buffer.compare); return publicKeys; } /** * Generate nonce deterministically * based on address (smallest pubkey), * name hash, and bid value. * @param {Buffer} nameHash * @param {Address} address * @param {Amount} value * @returns {Promise<Buffer>} */ async generateNonce(nameHash, address, value) { const publicKeys = await this._getNoncePublicKeys(address, value); return blake2b.multi(address.hash, publicKeys[0], nameHash); } /** * Generate nonces deterministically * for all keys (in multisig). * @param {Buffer} nameHash * @param {Address} address * @param {Amount} value * @returns {Promise<Buffer[]>} */ async generateNonces(nameHash, address, value) { const publicKeys = await this._getNoncePublicKeys(address, value); // Generate nonces for all public keys const nonces = []; for (const publicKey of publicKeys) nonces.push(blake2b.multi(address.hash, publicKey, nameHash)); return nonces; } /** * Generate nonce & blind, save nonce. * @param {Buffer} nameHash * @param {Address} address * @param {Amount} value * @returns {Promise<Buffer>} */ async generateBlind(nameHash, address, value) { const nonce = await this.generateNonce(nameHash, address, value); const blind = rules.blind(value, nonce); await this.txdb.saveBlind(blind, {value, nonce}); return blind; } /** * Generate all nonces & blinds, save nonces. * @param {Buffer} nameHash * @param {Address} address * @param {Amount} value * @returns {Promise<Buffer[]>} */ async generateBlinds(nameHash, address, value) { const nonces = await this.generateNonces(nameHash, address, value); const blinds = []; for (const nonce of nonces) { const blind = rules.blind(value, nonce); await this.txdb.saveBlind(blind, {value, nonce}); blinds.push(blind); } return blinds; } /** * Make a claim MTX. * @param {String} name * @param {Object?} [options] * @returns {Promise<Object>} */ async _createClaim(name, options) { if (options == null) options = {}; assert(typeof name === 'string'); assert(options && typeof options === 'object'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const height = this.wdb.height + 1; const network = this.network; // TODO: Handle expired behavior. if (!rules.isReserved(nameHash, height, network)) throw new Error('Name is not reserved.'); // Must get this from chain (not walletDB) in case // this name has already been claimed by an attacker // and we are trying to replace that claim. const ns = await this.wdb.getNameStatus(nameHash); if (!await this.wdb.isAvailable(nameHash)) throw new Error('Name is not available.'); const item = reserved.get(nameHash); assert(item); let rate = options.rate; if (rate == null) rate = await this.wdb.estimateFee(options.blocks); let size = 5 << 10; let vsize = size / consensus.WITNESS_SCALE_FACTOR | 0; let proof = null; try { proof = await ownership.prove(item.target, true); } catch (e) { ; } if (proof) { const zones = proof.zones; const zone = zones.length >= 2 ? zones[zones.length - 1] : null; let added = 0; // TXT record. added += item.target.length; // rrname added += 10; // header added += 1; // txt size added += 200; // max string size // RRSIG record size. if (!zone || zone.claim.length === 0) { added += item.target.length; // rrname added += 10; // header added += 275; // avg rsa sig size } const claim = Claim.fromProof(proof); size = claim.getSize() + added; added /= consensus.WITNESS_SCALE_FACTOR; added |= 0; vsize = claim.getVirtualSize() + added; } let minFee = options.fee; if (minFee == null) minFee = policy.getMinFee(vsize, rate); if (this.wdb.height < 1) throw new Error('Chain too immature for name claim.'); let commitHeight = 1; if (ns && ns.claimed) commitHeight = ns.claimed + 1; const commitHash = (await this.wdb.getBlock(commitHeight)).hash; let fee = Math.min(item.value, minFee); if (ns && !ns.owner.isNull()) { const coin = await this.wdb.getCoin(ns.owner.hash, ns.owner.index); assert(coin, 'Coin not found for name owner.'); fee = item.value - coin.value; } const acct = options.account || 0; const address = await this.receiveAddress(acct); const txt = ownership.createData(address, fee, commitHash, commitHeight, network); return { name, proof, target: item.target, value: item.value, size, fee, address, txt }; } /** * Create and send a claim MTX. * @param {String} name * @param {Object} options * @returns {Promise<Object>} */ async createClaim(name, options) { const unlock = await this.fundLock.lock(); try { return await this._createClaim(name, options); } finally { unlock(); } } /** * Make a claim proof. * @param {String} name * @param {Object?} [options] * @returns {Promise<Claim>} */ async makeFakeClaim(name, options) { if (options == null) options = {}; assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const height = this.wdb.height + 1; const network = this.network; // TODO: Handle expired behavior. if (!rules.isReserved(nameHash, height, network)) throw new Error('Name is not reserved.'); const {proof, txt} = await this._createClaim(name, options); if (!proof) throw new Error('Could not resolve name.'); proof.addData([txt]); const data = proof.getData(this.network); if (!data) throw new Error(`No valid DNS commitment found for ${name}.`); return Claim.fromProof(proof); } /** * Create and send a claim proof. * @param {String} name * @param {Object} options */ async _sendFakeClaim(name, options) { const claim = await this.makeFakeClaim(name, options); await this.wdb.sendClaim(claim); return claim; } /** * Create and send a claim proof. * @param {String} name * @param {Object} options */ async sendFakeClaim(name, options) { const unlock = await this.fundLock.lock(); try { return await this._sendFakeClaim(name, options); } finally { unlock(); } } /** * Make a claim proof. * @param {String} name * @param {Object} options * @returns {Promise<Claim>} */ async makeClaim(name, options) { if (options == null) options = {}; assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error(`Invalid name: ${name}.`); const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const height = this.wdb.height + 1; const network = this.network; // TODO: Handle expired behavior. if (!rules.isReserved(nameHash, height, network)) throw new Error(`Name is not reserved: ${name}.`); const ns = await this.getNameState(nameHash); if (ns) { if (!ns.isExpired(height, network)) throw new Error(`Name already claimed: ${name}.`); } else { if (!await this.wdb.isAvailable(nameHash)) throw new Error(`Name is not available: ${name}.`); } const item = reserved.get(nameHash); assert(item); const proof = await ownership.prove(item.target); const data = proof.getData(this.network); if (!data) throw new Error(`No valid DNS commitment found for ${name}.`); return Claim.fromProof(proof); } /** * Create and send a claim proof. * @param {String} name * @param {Object} options * @returns {Promise<Claim>} */ async _sendClaim(name, options) { const claim = await this.makeClaim(name, options); await this.wdb.sendClaim(claim); return claim; } /** * Create and send a claim proof. * @param {String} name * @param {Object} options * @returns {Promise<Claim>} */ async sendClaim(name, options) { const unlock = await this.fundLock.lock(); try { return await this._sendClaim(name, options); } finally { unlock(); } } /** * Make a open MTX. * @param {String} name * @param {Number|String} acct * @param {MTX?} [mtx] * @returns {Promise<MTX>} */ async makeOpen(name, acct, mtx) { assert(typeof name === 'string'); assert((acct >>> 0) === acct || typeof acct === 'string'); if (!rules.verifyName(name)) throw new Error(`Invalid name: ${name}.`); const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const height = this.wdb.height + 1; const network = this.network; const {icannlockup} = this.wdb.options; // TODO: Handle expired behavior. if (rules.isReserved(nameHash, height, network)) throw new Error(`Name is reserved: ${name}.`); if (icannlockup && rules.isLockedUp(nameHash, height, network)) throw new Error(`Name is locked up: ${name}.`); if (!rules.hasRollout(nameHash, height, network)) throw new Error(`Name not yet available: ${name}.`); let ns = await this.getNameState(nameHash); if (!ns) ns = await this.wdb.getNameStatus(nameHash); ns.maybeExpire(height, network); const start = ns.height; if (!ns.isOpening(height, network)) throw new Error(`Name is not available: ${name}.`); if (start !== 0 && start !== height) throw new Error(`Name is already opening: ${name}.`); const addr = await this.receiveAddress(acct); const output = new Output(); output.address = addr; output.value = 0; output.covenant.setOpen(nameHash, rawName); if (!mtx) mtx = new MTX(); mtx.outputs.push(output); if (await this.txdb.isDoubleOpen(mtx)) throw new Error(`Already sent an open for: ${name}.`); return mtx; } /** * Create and finalize an open * MTX without a lock. * @param {String} name * @param {Object} options * @returns {Promise<MTX>} */ async _createOpen(name, options) { const acct = options ? options.account || 0 : 0; const mtx = await this.makeOpen(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } /** * Create and finalize an open * MTX with a lock. * @param {String} name * @param {Object} options * @returns {Promise<MTX>} */ async createOpen(name, options) { const unlock = await this.fundLock.lock(); try { return await this._createOpen(name, options); } finally { unlock(); } } /** * Create and send an open * MTX without a lock. * @param {String} name * @param {Object} options * @returns {Promise<TX>} */ async _sendOpen(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createOpen(name, options); return this.sendMTX(mtx, passphrase); } /** * Create and send an open * MTX with a lock. * @param {String} name * @param {Object} options * @returns {Promise<TX>} */ async sendOpen(name, options) { const unlock = await this.fundLock.lock(); try { return await this._sendOpen(name, options); } finally { unlock(); } } /** * Make a bid MTX. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Number|String} acct * @param {MTX?} [mtx] * @param {Address?} [addr] * @returns {Promise<MTX>} */ async makeBid(name, value, lockup, acct, mtx, addr) { assert(typeof name === 'string'); assert(Number.isSafeInteger(value) && value >= 0); assert(Number.isSafeInteger(lockup) && lockup >= 0); assert((acct >>> 0) === acct || typeof acct === 'string'); assert(addr == null || addr instanceof Address); if (!rules.verifyName(name)) throw new Error(`Invalid name: ${name}.`); const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const height = this.wdb.height + 1; const network = this.network; let ns = await this.getNameState(nameHash); if (!ns) ns = await this.wdb.getNameStatus(nameHash); ns.maybeExpire(height, network); const start = ns.height; if (ns.isOpening(height, network)) throw new Error(`Name has not reached the bidding phase yet: ${name}.`); if (!ns.isBidding(height, network)) throw new Error(`Name is not available: ${name}.`); if (value > lockup) throw new Error( `Bid (${value}) exceeds lockup value (${lockup}): ${name}.` ); if (!addr) addr = await this.receiveAddress(acct); const blind = await this.generateBlind(nameHash, addr, value); const output = new Output(); output.address = addr; output.value = lockup; output.covenant.setBid(nameHash, start, rawName, blind); if (!mtx) mtx = new MTX(); mtx.outputs.push(output); return mtx; } /** * Create and finalize a bid * MTX without a lock. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options * @returns {Promise<MTX>} */ async _createBid(name, value, lockup, options) { const acct = options ? options.account || 0 : 0; const mtx = await this.makeBid(name, value, lockup, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } /** * Create and finalize a bid * MTX with a lock. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options * @returns {Promise<MTX>} */ async createBid(name, value, lockup, options) { const unlock = await this.fundLock.lock(); try { return await this._createBid(name, value, lockup, options); } finally { unlock(); } } /** * Create and send a bid MTX. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options */ async _sendBid(name, value, lockup, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createBid(name, value, lockup, options); return this.sendMTX(mtx, passphrase); } /** * Create and send a bid MTX. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options * @returns {Promise<TX>} */ async sendBid(name, value, lockup, options) { const unlock = await this.fundLock.lock(); try { return await this._sendBid(name, value, lockup, options); } finally { unlock(); } } /** * @typedef {Object} CreateAuctionResults * @param {MTX} bid * @param {MTX} reveal */ /** * Create and finalize a bid & a reveal (in advance) * MTX with a lock. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options * @returns {Promise<CreateAuctionResults>} */ async createAuctionTXs(name, value, lockup, options) { const unlock = await this.fundLock.lock(); try { return await this._createAuctionTXs(name, value, lockup, options); } finally { unlock(); } } /** * Create and finalize a bid & a reveal (in advance) * MTX without a lock. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options * @returns {Promise<CreateAuctionResults>} */ async _createAuctionTXs(name, value, lockup, options) { const bid = await this._createBid(name, value, lockup, options); const bidOuputIndex = bid.outputs.findIndex(o => o.covenant.isBid()); const bidOutput = bid.outputs[bidOuputIndex]; const bidCoin = Coin.fromTX(bid, bidOuputIndex, -1); // Prepare the data needed to make the reveal in advance const nameHash = bidOutput.covenant.getHash(0); const height = bidOutput.covenant.getU32(1); const blind = bidOutput.covenant.getHash(3); const bv = await this.getBlind(blind); if (!bv) throw new Error(`Blind value not found for name: ${name}.`); const { nonce } = bv; const reveal = new MTX(); const output = new Output(); output.address = bidCoin.address; output.value = value; output.covenant.setReveal(nameHash, height, nonce); reveal.addCoin(bidCoin); reveal.outputs.push(output); await this.fill(reveal, { ...options }); assert( reveal.inputs.length === 1, 'Pre-signed REVEAL must not require additional inputs' ); const finalReveal = await this.finalize(reveal, options); return { bid, reveal: finalReveal }; } /** * Make a reveal MTX. * @param {String} name * @param {(Number|String)?} [acct] * @param {MTX?} [mtx] * @returns {Promise<MTX>} */ async makeReveal(name, acct, mtx) { assert(typeof name === 'string'); let acctno; if (acct != null) { ass