hsd
Version:
Cryptocurrency bike-shed
2,059 lines (1,640 loc) • 139 kB
JavaScript
/*!
* 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