2335 lines
48 KiB
JavaScript
2335 lines
48 KiB
JavaScript
/*!
|
|
* walletdb.js - storage for wallets
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const AsyncObject = require('../utils/asyncobject');
|
|
const util = require('../utils/util');
|
|
const Lock = require('../utils/lock');
|
|
const MappedLock = require('../utils/mappedlock');
|
|
const LRU = require('../utils/lru');
|
|
const encoding = require('../utils/encoding');
|
|
const ccmp = require('../crypto/ccmp');
|
|
const aes = require('../crypto/aes');
|
|
const Network = require('../protocol/network');
|
|
const Path = require('./path');
|
|
const common = require('./common');
|
|
const Wallet = require('./wallet');
|
|
const Account = require('./account');
|
|
const LDB = require('../db/ldb');
|
|
const Bloom = require('../utils/bloom');
|
|
const Logger = require('../node/logger');
|
|
const Outpoint = require('../primitives/outpoint');
|
|
const layouts = require('./layout');
|
|
const records = require('./records');
|
|
const HTTPServer = require('./http');
|
|
const RPC = require('./rpc');
|
|
const layout = layouts.walletdb;
|
|
const ChainState = records.ChainState;
|
|
const BlockMapRecord = records.BlockMapRecord;
|
|
const BlockMeta = records.BlockMeta;
|
|
const PathMapRecord = records.PathMapRecord;
|
|
const OutpointMapRecord = records.OutpointMapRecord;
|
|
const TXRecord = records.TXRecord;
|
|
const U32 = encoding.U32;
|
|
|
|
/**
|
|
* WalletDB
|
|
* @alias module:wallet.WalletDB
|
|
* @constructor
|
|
* @param {Object} options
|
|
* @param {String?} options.name - Database name.
|
|
* @param {String?} options.location - Database file location.
|
|
* @param {String?} options.db - Database backend (`"leveldb"` by default).
|
|
* @param {Boolean?} options.verify - Verify transactions as they
|
|
* come in (note that this will not happen on the worker pool).
|
|
* @property {Boolean} loaded
|
|
*/
|
|
|
|
function WalletDB(options) {
|
|
if (!(this instanceof WalletDB))
|
|
return new WalletDB(options);
|
|
|
|
AsyncObject.call(this);
|
|
|
|
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;
|
|
this.feeRate = this.options.feeRate;
|
|
|
|
this.db = LDB(this.options);
|
|
this.rpc = new RPC(this);
|
|
this.primary = null;
|
|
this.http = null;
|
|
|
|
if (!HTTPServer.unsupported) {
|
|
this.http = new HTTPServer({
|
|
walletdb: this,
|
|
network: this.network,
|
|
logger: this.logger,
|
|
prefix: this.options.prefix,
|
|
apiKey: this.options.apiKey,
|
|
walletAuth: this.options.walletAuth,
|
|
noAuth: this.options.noAuth,
|
|
host: this.options.host,
|
|
port: this.options.port,
|
|
ssl: this.options.ssl
|
|
});
|
|
}
|
|
|
|
this.state = new ChainState();
|
|
this.wallets = new Map();
|
|
this.depth = 0;
|
|
this.rescanning = false;
|
|
this.bound = false;
|
|
|
|
this.readLock = new MappedLock();
|
|
this.writeLock = new Lock();
|
|
this.txLock = new Lock();
|
|
|
|
this.widCache = new LRU(10000);
|
|
this.pathMapCache = new LRU(100000);
|
|
|
|
this.filter = new Bloom();
|
|
|
|
this._init();
|
|
}
|
|
|
|
Object.setPrototypeOf(WalletDB.prototype, AsyncObject.prototype);
|
|
|
|
/**
|
|
* Database layout.
|
|
* @type {Object}
|
|
*/
|
|
|
|
WalletDB.layout = layout;
|
|
|
|
/**
|
|
* Initialize walletdb.
|
|
* @private
|
|
*/
|
|
|
|
WalletDB.prototype._init = function _init() {
|
|
let items = 1000000;
|
|
let flag = -1;
|
|
|
|
// Highest number of items with an
|
|
// FPR of 0.001. We have to do this
|
|
// by hand because Bloom.fromRate's
|
|
// policy limit enforcing is fairly
|
|
// naive.
|
|
if (this.options.spv) {
|
|
items = 20000;
|
|
flag = Bloom.flags.ALL;
|
|
}
|
|
|
|
this.filter = Bloom.fromRate(items, 0.001, flag);
|
|
};
|
|
|
|
/**
|
|
* Open the walletdb, wait for the database to load.
|
|
* @alias WalletDB#open
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._open = async function _open() {
|
|
if (this.options.listen)
|
|
await this.logger.open();
|
|
|
|
await this.db.open();
|
|
await this.db.checkVersion('V', 6);
|
|
|
|
this.depth = await this.getDepth();
|
|
|
|
if (this.options.wipeNoReally)
|
|
await this.wipe();
|
|
|
|
await this.load();
|
|
|
|
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'
|
|
});
|
|
|
|
this.logger.info(
|
|
'Loaded primary wallet (id=%s, wid=%d, address=%s)',
|
|
wallet.id, wallet.wid, wallet.getAddress());
|
|
|
|
this.primary = wallet;
|
|
this.rpc.wallet = wallet;
|
|
|
|
if (this.http && this.options.listen)
|
|
await this.http.open();
|
|
};
|
|
|
|
/**
|
|
* Close the walletdb, wait for the database to close.
|
|
* @alias WalletDB#close
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._close = async function _close() {
|
|
await this.disconnect();
|
|
|
|
if (this.http && this.options.listen)
|
|
await this.http.close();
|
|
|
|
for (const wallet of this.wallets.values())
|
|
await wallet.destroy();
|
|
|
|
await this.db.close();
|
|
|
|
if (this.options.listen)
|
|
await this.logger.close();
|
|
};
|
|
|
|
/**
|
|
* Load the walletdb.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.load = async function load() {
|
|
const unlock = await this.txLock.lock();
|
|
try {
|
|
await this.connect();
|
|
await this.init();
|
|
await this.watch();
|
|
await this.sync();
|
|
await this.resend();
|
|
} finally {
|
|
unlock();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Bind to node events.
|
|
* @private
|
|
*/
|
|
|
|
WalletDB.prototype.bind = function bind() {
|
|
if (!this.client)
|
|
return;
|
|
|
|
if (this.bound)
|
|
return;
|
|
|
|
this.bound = true;
|
|
|
|
this.client.on('error', (err) => {
|
|
this.emit('error', err);
|
|
});
|
|
|
|
this.client.on('block connect', async (entry, txs) => {
|
|
try {
|
|
await this.addBlock(entry, txs);
|
|
} catch (e) {
|
|
this.emit('error', e);
|
|
}
|
|
});
|
|
|
|
this.client.on('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.on('tx', async (tx) => {
|
|
try {
|
|
await this.addTX(tx);
|
|
} catch (e) {
|
|
this.emit('error', e);
|
|
}
|
|
});
|
|
|
|
this.client.on('chain reset', async (tip) => {
|
|
try {
|
|
await this.resetChain(tip);
|
|
} catch (e) {
|
|
this.emit('error', e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Connect to the node server (client required).
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.connect = async function connect() {
|
|
if (!this.client)
|
|
return;
|
|
|
|
this.bind();
|
|
|
|
await this.client.open();
|
|
await this.setFilter();
|
|
};
|
|
|
|
/**
|
|
* Disconnect from node server (client required).
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.disconnect = async function disconnect() {
|
|
if (!this.client)
|
|
return;
|
|
|
|
await this.client.close();
|
|
};
|
|
|
|
/**
|
|
* Initialize and write initial sync state.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.init = async function init() {
|
|
const state = await this.getState();
|
|
const startHeight = this.options.startHeight;
|
|
|
|
if (state) {
|
|
this.state = state;
|
|
return;
|
|
}
|
|
|
|
let tip;
|
|
if (this.client) {
|
|
if (startHeight != null) {
|
|
tip = await this.client.getEntry(startHeight);
|
|
if (!tip)
|
|
throw new Error('WDB: Could not find start block.');
|
|
} else {
|
|
tip = await this.client.getTip();
|
|
}
|
|
tip = BlockMeta.fromEntry(tip);
|
|
} else {
|
|
tip = BlockMeta.fromEntry(this.network.genesis);
|
|
}
|
|
|
|
this.logger.info(
|
|
'Initializing WalletDB chain state at %s (%d).',
|
|
util.revHex(tip.hash), tip.height);
|
|
|
|
await this.resetState(tip, false);
|
|
};
|
|
|
|
/**
|
|
* Watch addresses and outpoints.
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.watch = async function watch() {
|
|
let iter = this.db.iterator({
|
|
gte: layout.p(encoding.NULL_HASH),
|
|
lte: layout.p(encoding.HIGH_HASH)
|
|
});
|
|
|
|
let hashes = 0;
|
|
let outpoints = 0;
|
|
|
|
for (;;) {
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
try {
|
|
const data = layout.pp(item.key);
|
|
this.filter.add(data, 'hex');
|
|
} catch (e) {
|
|
await iter.end();
|
|
throw e;
|
|
}
|
|
|
|
hashes++;
|
|
}
|
|
|
|
iter = this.db.iterator({
|
|
gte: layout.o(encoding.NULL_HASH, 0),
|
|
lte: layout.o(encoding.HIGH_HASH, 0xffffffff)
|
|
});
|
|
|
|
for (;;) {
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
try {
|
|
const [hash, index] = layout.oo(item.key);
|
|
const outpoint = new Outpoint(hash, index);
|
|
const data = outpoint.toRaw();
|
|
this.filter.add(data);
|
|
} catch (e) {
|
|
await iter.end();
|
|
throw e;
|
|
}
|
|
|
|
outpoints++;
|
|
}
|
|
|
|
this.logger.info('Added %d hashes to WalletDB filter.', hashes);
|
|
this.logger.info('Added %d outpoints to WalletDB filter.', outpoints);
|
|
|
|
await this.setFilter();
|
|
};
|
|
|
|
/**
|
|
* Connect and sync with the chain server.
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.sync = async function sync() {
|
|
if (!this.client)
|
|
return;
|
|
|
|
let height = this.state.height;
|
|
let entry;
|
|
|
|
while (height >= 0) {
|
|
const tip = await this.getBlock(height);
|
|
|
|
if (!tip)
|
|
break;
|
|
|
|
entry = await this.client.getEntry(tip.hash);
|
|
|
|
if (entry)
|
|
break;
|
|
|
|
height--;
|
|
}
|
|
|
|
if (!entry) {
|
|
height = this.state.startHeight;
|
|
entry = await this.client.getEntry(this.state.startHash);
|
|
|
|
if (!entry)
|
|
height = 0;
|
|
}
|
|
|
|
await this.scan(height);
|
|
};
|
|
|
|
/**
|
|
* Rescan blockchain from a given height.
|
|
* @private
|
|
* @param {Number?} height
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.scan = async function scan(height) {
|
|
if (!this.client)
|
|
return;
|
|
|
|
if (height == null)
|
|
height = this.state.startHeight;
|
|
|
|
assert(util.isU32(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 await this.scan(height);
|
|
};
|
|
|
|
/**
|
|
* Broadcast a transaction via chain server.
|
|
* @param {TX} tx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.send = async function send(tx) {
|
|
if (!this.client) {
|
|
this.emit('send', tx);
|
|
return;
|
|
}
|
|
|
|
await 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;
|
|
|
|
if (!this.client)
|
|
return this.network.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.setFilter = function setFilter() {
|
|
if (!this.client) {
|
|
this.emit('set filter', this.filter);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return this.client.setFilter(this.filter);
|
|
};
|
|
|
|
/**
|
|
* Add data to remote filter.
|
|
* @private
|
|
* @param {Buffer} data
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.addFilter = function addFilter(data) {
|
|
if (!this.client) {
|
|
this.emit('add filter', data);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return this.client.addFilter(data);
|
|
};
|
|
|
|
/**
|
|
* Reset remote filter.
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.resetFilter = function resetFilter() {
|
|
if (!this.client) {
|
|
this.emit('reset filter');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
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 batch = this.db.batch();
|
|
let total = 0;
|
|
|
|
for (;;) {
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
try {
|
|
switch (item.key[0]) {
|
|
case 0x62: // b
|
|
case 0x63: // c
|
|
case 0x65: // e
|
|
case 0x74: // t
|
|
case 0x6f: // o
|
|
case 0x68: // h
|
|
case 0x52: // R
|
|
batch.del(item.key);
|
|
total++;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
await iter.end();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
this.logger.warning('Wiped %d txdb records.', total);
|
|
|
|
await batch.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
|
|
});
|
|
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
return 1;
|
|
|
|
await iter.end();
|
|
|
|
const depth = layout.ww(item.key);
|
|
|
|
return depth + 1;
|
|
};
|
|
|
|
/**
|
|
* Start batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.start = function start(wallet) {
|
|
assert(!wallet.current, 'WDB: Batch already started.');
|
|
wallet.current = this.db.batch();
|
|
wallet.accountCache.start();
|
|
wallet.pathCache.start();
|
|
return wallet.current;
|
|
};
|
|
|
|
/**
|
|
* Drop batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.drop = function drop(wallet) {
|
|
const batch = this.batch(wallet);
|
|
wallet.current = null;
|
|
wallet.accountCache.drop();
|
|
wallet.pathCache.drop();
|
|
batch.clear();
|
|
};
|
|
|
|
/**
|
|
* Clear batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.clear = function clear(wallet) {
|
|
const batch = this.batch(wallet);
|
|
wallet.accountCache.clear();
|
|
wallet.pathCache.clear();
|
|
batch.clear();
|
|
};
|
|
|
|
/**
|
|
* Get batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @returns {Leveldown.Batch}
|
|
*/
|
|
|
|
WalletDB.prototype.batch = function batch(wallet) {
|
|
assert(wallet.current, 'WDB: Batch does not exist.');
|
|
return wallet.current;
|
|
};
|
|
|
|
/**
|
|
* Save batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.commit = async function commit(wallet) {
|
|
const batch = this.batch(wallet);
|
|
|
|
try {
|
|
await batch.write();
|
|
} catch (e) {
|
|
wallet.current = null;
|
|
wallet.accountCache.drop();
|
|
wallet.pathCache.drop();
|
|
throw e;
|
|
}
|
|
|
|
wallet.current = null;
|
|
wallet.accountCache.commit();
|
|
wallet.pathCache.commit();
|
|
};
|
|
|
|
/**
|
|
* 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.getWalletID = async function getWalletID(id) {
|
|
if (!id)
|
|
return null;
|
|
|
|
if (typeof id === 'number')
|
|
return id;
|
|
|
|
const cache = this.widCache.get(id);
|
|
|
|
if (cache)
|
|
return cache;
|
|
|
|
const data = await this.db.get(layout.l(id));
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
const wid = data.readUInt32LE(0, true);
|
|
|
|
this.widCache.set(id, wid);
|
|
|
|
return wid;
|
|
};
|
|
|
|
/**
|
|
* 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.getWalletID(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(wallet) {
|
|
const wid = wallet.wid;
|
|
const id = wallet.id;
|
|
const batch = this.batch(wallet);
|
|
|
|
this.widCache.set(id, wid);
|
|
|
|
batch.put(layout.w(wid), wallet.toRaw());
|
|
batch.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) {
|
|
const old = 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 batch = this.start(wallet);
|
|
batch.del(layout.l(old));
|
|
|
|
wallet.id = id;
|
|
|
|
this.save(wallet);
|
|
|
|
await this.commit(wallet);
|
|
|
|
this.widCache.remove(old);
|
|
|
|
const paths = wallet.pathCache.values();
|
|
|
|
for (const path of paths)
|
|
path.id = id;
|
|
};
|
|
|
|
/**
|
|
* Rename an account.
|
|
* @param {Account} account
|
|
* @param {String} name
|
|
*/
|
|
|
|
WalletDB.prototype.renameAccount = function renameAccount(account, name) {
|
|
const wallet = account.wallet;
|
|
const batch = this.batch(wallet);
|
|
|
|
// Remove old wid/name->account index.
|
|
batch.del(layout.i(account.wid, account.name));
|
|
|
|
account.name = name;
|
|
|
|
this.saveAccount(account);
|
|
};
|
|
|
|
/**
|
|
* Get a wallet with token auth first.
|
|
* @param {WalletID} wid
|
|
* @param {String|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;
|
|
|
|
if (typeof token === 'string') {
|
|
if (!util.isHex256(token))
|
|
throw new Error('WDB: Authentication error.');
|
|
token = Buffer.from(token, 'hex');
|
|
}
|
|
|
|
// 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) {
|
|
const exists = await this.has(options.id);
|
|
|
|
if (exists)
|
|
throw new Error('WDB: Wallet already exists.');
|
|
|
|
const wallet = Wallet.fromOptions(this, options);
|
|
wallet.wid = this.depth++;
|
|
|
|
await wallet.init(options);
|
|
|
|
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.getWalletID(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 await 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(account) {
|
|
const wid = account.wid;
|
|
const wallet = account.wallet;
|
|
const index = account.accountIndex;
|
|
const name = account.name;
|
|
const batch = this.batch(wallet);
|
|
|
|
// Account data
|
|
batch.put(layout.a(wid, index), account.toRaw());
|
|
|
|
// Name->Index lookups
|
|
batch.put(layout.i(wid, name), U32(index));
|
|
|
|
// Index->Name lookups
|
|
batch.put(layout.n(wid, index), Buffer.from(name, 'ascii'));
|
|
|
|
wallet.accountCache.push(index, account);
|
|
};
|
|
|
|
/**
|
|
* 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));
|
|
};
|
|
|
|
/**
|
|
* Lookup the corresponding account name's index.
|
|
* @param {WalletID} wid
|
|
* @param {String|Number} name - Account name/index.
|
|
* @returns {Promise} - Returns Number.
|
|
*/
|
|
|
|
WalletDB.prototype.getPathMap = async function getPathMap(hash) {
|
|
const cache = this.pathMapCache.get(hash);
|
|
|
|
if (cache)
|
|
return cache;
|
|
|
|
const data = await this.db.get(layout.p(hash));
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
const map = PathMapRecord.fromRaw(hash, data);
|
|
|
|
this.pathMapCache.set(hash, map);
|
|
|
|
return map;
|
|
};
|
|
|
|
/**
|
|
* Save an address to the path map.
|
|
* @param {Wallet} wallet
|
|
* @param {WalletKey} ring
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.saveKey = function saveKey(wallet, ring) {
|
|
return this.savePath(wallet, 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(wallet, path) {
|
|
const wid = wallet.wid;
|
|
const hash = path.hash;
|
|
const batch = this.batch(wallet);
|
|
|
|
await this.addHash(hash);
|
|
|
|
let map = await this.getPathMap(hash);
|
|
|
|
if (!map)
|
|
map = new PathMapRecord(hash);
|
|
|
|
if (!map.add(wid))
|
|
return;
|
|
|
|
this.pathMapCache.set(hash, map);
|
|
wallet.pathCache.push(hash, path);
|
|
|
|
// Address Hash -> Wallet Map
|
|
batch.put(layout.p(hash), map.toRaw());
|
|
|
|
// Wallet ID + Address Hash -> Path Data
|
|
batch.put(layout.P(wid, hash), path.toRaw());
|
|
|
|
// Wallet ID + Account Index + Address Hash -> Dummy
|
|
batch.put(layout.r(wid, path.account, hash), null);
|
|
};
|
|
|
|
/**
|
|
* Retrieve path by hash.
|
|
* @param {WalletID} wid
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getPath = async function getPath(wid, hash) {
|
|
const data = await this.db.get(layout.P(wid, hash));
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
const path = Path.fromRaw(data);
|
|
path.wid = wid;
|
|
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.wid = wid;
|
|
|
|
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(wallet, key) {
|
|
const wid = wallet.wid;
|
|
const paths = await wallet.getPaths();
|
|
const batch = this.batch(wallet);
|
|
|
|
for (let path of paths) {
|
|
if (!path.data)
|
|
continue;
|
|
|
|
assert(!path.encrypted);
|
|
|
|
const hash = Buffer.from(path.hash, 'hex');
|
|
const iv = hash.slice(0, 16);
|
|
|
|
path = path.clone();
|
|
path.data = aes.encipher(path.data, key, iv);
|
|
path.encrypted = true;
|
|
|
|
wallet.pathCache.push(path.hash, path);
|
|
|
|
batch.put(layout.P(wid, path.hash), path.toRaw());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Decrypt all imported keys for a wallet.
|
|
* @param {WalletID} wid
|
|
* @param {Buffer} key
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.decryptKeys = async function decryptKeys(wallet, key) {
|
|
const wid = wallet.wid;
|
|
const paths = await wallet.getPaths();
|
|
const batch = this.batch(wallet);
|
|
|
|
for (let path of paths) {
|
|
if (!path.data)
|
|
continue;
|
|
|
|
assert(path.encrypted);
|
|
|
|
const hash = Buffer.from(path.hash, 'hex');
|
|
const iv = hash.slice(0, 16);
|
|
|
|
path = path.clone();
|
|
path.data = aes.decipher(path.data, key, iv);
|
|
path.encrypted = false;
|
|
|
|
wallet.pathCache.push(path.hash, path);
|
|
|
|
batch.put(layout.P(wid, path.hash), path.toRaw());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Resend all pending transactions.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.resend = async function resend() {
|
|
const keys = await this.db.keys({
|
|
gte: layout.w(0x00000000),
|
|
lte: layout.w(0xffffffff)
|
|
});
|
|
|
|
for (const key of keys) {
|
|
const wid = layout.ww(key);
|
|
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 keys = await this.db.keys({
|
|
gte: layout.prefix(wid, layout.p(encoding.NULL_HASH)),
|
|
lte: layout.prefix(wid, layout.p(encoding.HIGH_HASH))
|
|
});
|
|
|
|
if (keys.length === 0)
|
|
return;
|
|
|
|
this.logger.info(
|
|
'Rebroadcasting %d transactions for %d.',
|
|
keys.length,
|
|
wid);
|
|
|
|
const txs = [];
|
|
|
|
for (const key of keys) {
|
|
const hash = layout.pp(key);
|
|
const tkey = layout.prefix(wid, layout.t(hash));
|
|
const data = await this.db.get(tkey);
|
|
|
|
if (!data)
|
|
continue;
|
|
|
|
const wtx = TXRecord.fromRaw(data);
|
|
|
|
if (wtx.tx.isCoinbase())
|
|
continue;
|
|
|
|
txs.push(wtx.tx);
|
|
}
|
|
|
|
const sorted = common.sortDeps(txs);
|
|
|
|
for (const tx of sorted)
|
|
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 hashes = tx.getOutputHashes('hex');
|
|
const result = new Set();
|
|
|
|
if (!tx.isCoinbase()) {
|
|
for (const input of tx.inputs) {
|
|
const prevout = input.prevout;
|
|
|
|
if (!this.testFilter(prevout.toRaw()))
|
|
continue;
|
|
|
|
const map = await this.getOutpointMap(prevout.hash, prevout.index);
|
|
|
|
if (!map)
|
|
continue;
|
|
|
|
for (const wid of map.wids)
|
|
result.add(wid);
|
|
}
|
|
}
|
|
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Reset the chain state to a tip/start-block.
|
|
* @param {BlockMeta} tip
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.resetState = async function resetState(tip, marked) {
|
|
const batch = this.db.batch();
|
|
const state = this.state.clone();
|
|
|
|
const iter = this.db.iterator({
|
|
gte: layout.h(0),
|
|
lte: layout.h(0xffffffff),
|
|
values: false
|
|
});
|
|
|
|
for (;;) {
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
try {
|
|
batch.del(item.key);
|
|
} catch (e) {
|
|
await iter.end();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
state.startHeight = tip.height;
|
|
state.startHash = tip.hash;
|
|
state.height = tip.height;
|
|
state.marked = marked;
|
|
|
|
batch.put(layout.h(tip.height), tip.toHash());
|
|
batch.put(layout.R, state.toRaw());
|
|
|
|
await batch.write();
|
|
|
|
this.state = state;
|
|
};
|
|
|
|
/**
|
|
* Sync the current chain state to tip.
|
|
* @param {BlockMeta} tip
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.syncState = async function syncState(tip) {
|
|
const batch = this.db.batch();
|
|
const state = this.state.clone();
|
|
|
|
if (tip.height < state.height) {
|
|
// Hashes ahead of our new tip
|
|
// that we need to delete.
|
|
let height = state.height;
|
|
let blocks = height - tip.height;
|
|
|
|
if (blocks > this.options.keepBlocks)
|
|
blocks = this.options.keepBlocks;
|
|
|
|
for (let i = 0; i < blocks; i++) {
|
|
batch.del(layout.h(height));
|
|
height--;
|
|
}
|
|
} else if (tip.height > state.height) {
|
|
// Prune old hashes.
|
|
const height = tip.height - this.options.keepBlocks;
|
|
|
|
assert(tip.height === state.height + 1, 'Bad chain sync.');
|
|
|
|
if (height >= 0)
|
|
batch.del(layout.h(height));
|
|
}
|
|
|
|
state.height = tip.height;
|
|
|
|
// Save tip and state.
|
|
batch.put(layout.h(tip.height), tip.toHash());
|
|
batch.put(layout.R, state.toRaw());
|
|
|
|
await batch.write();
|
|
|
|
this.state = state;
|
|
};
|
|
|
|
/**
|
|
* Mark the start block once a confirmed tx is seen.
|
|
* @param {BlockMeta} tip
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.maybeMark = async function maybeMark(tip) {
|
|
if (this.state.marked)
|
|
return;
|
|
|
|
this.logger.info('Marking WalletDB start block at %s (%d).',
|
|
util.revHex(tip.hash), tip.height);
|
|
|
|
await this.resetState(tip, true);
|
|
};
|
|
|
|
/**
|
|
* Get a block->wallet map.
|
|
* @param {Number} height
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getBlockMap = async function getBlockMap(height) {
|
|
const data = await this.db.get(layout.b(height));
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
return BlockMapRecord.fromRaw(height, data);
|
|
};
|
|
|
|
/**
|
|
* Add block to the global block map.
|
|
* @param {Wallet} wallet
|
|
* @param {Number} height
|
|
* @param {BlockMapRecord} block
|
|
*/
|
|
|
|
WalletDB.prototype.writeBlockMap = function writeBlockMap(wallet, height, block) {
|
|
const batch = this.batch(wallet);
|
|
batch.put(layout.b(height), block.toRaw());
|
|
};
|
|
|
|
/**
|
|
* Remove a block from the global block map.
|
|
* @param {Wallet} wallet
|
|
* @param {Number} height
|
|
*/
|
|
|
|
WalletDB.prototype.unwriteBlockMap = function unwriteBlockMap(wallet, height) {
|
|
const batch = this.batch(wallet);
|
|
batch.del(layout.b(height));
|
|
};
|
|
|
|
/**
|
|
* Get a Unspent->Wallet map.
|
|
* @param {Hash} hash
|
|
* @param {Number} index
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getOutpointMap = async function getOutpointMap(hash, index) {
|
|
const data = await this.db.get(layout.o(hash, index));
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
return OutpointMapRecord.fromRaw(hash, index, data);
|
|
};
|
|
|
|
/**
|
|
* Add an outpoint to global unspent map.
|
|
* @param {Wallet} wallet
|
|
* @param {Hash} hash
|
|
* @param {Number} index
|
|
* @param {OutpointMapRecord} map
|
|
*/
|
|
|
|
WalletDB.prototype.writeOutpointMap = function writeOutpointMap(wallet, hash, index, map) {
|
|
const batch = this.batch(wallet);
|
|
|
|
this.addOutpoint(hash, index);
|
|
|
|
batch.put(layout.o(hash, index), map.toRaw());
|
|
};
|
|
|
|
/**
|
|
* Remove an outpoint from global unspent map.
|
|
* @param {Wallet} wallet
|
|
* @param {Hash} hash
|
|
* @param {Number} index
|
|
*/
|
|
|
|
WalletDB.prototype.unwriteOutpointMap = function unwriteOutpointMap(wallet, hash, index) {
|
|
const batch = this.batch(wallet);
|
|
batch.del(layout.o(hash, index));
|
|
};
|
|
|
|
/**
|
|
* 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.debug('Rolled back to same height (%d).', height);
|
|
return true;
|
|
}
|
|
|
|
this.logger.info(
|
|
'Rolling back %d WalletDB blocks to height %d.',
|
|
this.state.height - height, height);
|
|
|
|
let tip = await this.getBlock(height);
|
|
let marked = false;
|
|
|
|
if (tip) {
|
|
await this.revert(tip.height);
|
|
await this.syncState(tip);
|
|
return true;
|
|
}
|
|
|
|
tip = new BlockMeta();
|
|
|
|
if (height >= this.state.startHeight) {
|
|
tip.height = this.state.startHeight;
|
|
tip.hash = this.state.startHash;
|
|
marked = this.state.marked;
|
|
|
|
this.logger.warning(
|
|
'Rolling back WalletDB to start block (%d).',
|
|
tip.height);
|
|
} else {
|
|
tip.height = 0;
|
|
tip.hash = this.network.genesis.hash;
|
|
marked = false;
|
|
|
|
this.logger.warning('Rolling back WalletDB to genesis block.');
|
|
}
|
|
|
|
await this.revert(tip.height);
|
|
await this.resetState(tip, marked);
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
for (;;) {
|
|
const item = await iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
try {
|
|
const height = layout.bb(item.key);
|
|
const block = BlockMapRecord.fromRaw(height, item.value);
|
|
const txs = block.toArray();
|
|
|
|
total += txs.length;
|
|
|
|
for (let i = txs.length - 1; i >= 0; i--) {
|
|
const tx = txs[i];
|
|
await this._unconfirm(tx);
|
|
}
|
|
} catch (e) {
|
|
await iter.end();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
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);
|
|
let total = 0;
|
|
|
|
if (tip.height < this.state.height) {
|
|
this.logger.warning(
|
|
'WalletDB is connecting low blocks (%d).',
|
|
tip.height);
|
|
return total;
|
|
}
|
|
|
|
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) {
|
|
throw new Error('WDB: Bad connection (height mismatch).');
|
|
}
|
|
|
|
// Sync the state to the new tip.
|
|
await this.syncState(tip);
|
|
|
|
if (this.options.checkpoints) {
|
|
if (tip.height <= this.network.lastCheckpoint)
|
|
return total;
|
|
}
|
|
|
|
for (const tx of txs) {
|
|
if (await this._insert(tx, tip))
|
|
total++;
|
|
}
|
|
|
|
if (total > 0) {
|
|
this.logger.info('Connected WalletDB block %s (tx=%d).',
|
|
util.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 > 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);
|
|
|
|
if (!prev)
|
|
throw new Error('WDB: Bad disconnection (no previous block).');
|
|
|
|
// Get the map of txids->wids.
|
|
const block = await this.getBlockMap(tip.height);
|
|
|
|
if (!block) {
|
|
await this.syncState(prev);
|
|
return 0;
|
|
}
|
|
|
|
const txs = block.toArray();
|
|
|
|
for (let i = txs.length - 1; i >= 0; i--) {
|
|
const tx = txs[i];
|
|
await this._unconfirm(tx);
|
|
}
|
|
|
|
// Sync the state to the previous tip.
|
|
await this.syncState(prev);
|
|
|
|
this.logger.warning('Disconnected wallet block %s (tx=%d).',
|
|
util.revHex(tip.hash), block.txs.size);
|
|
|
|
return block.txs.size;
|
|
};
|
|
|
|
/**
|
|
* 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: %s.', 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._insert(tx);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add a transaction to the database without a lock.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {BlockMeta} block
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._insert = async function _insert(tx, block) {
|
|
const wids = await this.getWalletsByTX(tx);
|
|
let result = false;
|
|
|
|
assert(!tx.mutable, 'WDB: Cannot add mutable TX.');
|
|
|
|
if (!wids)
|
|
return null;
|
|
|
|
this.logger.info(
|
|
'Incoming transaction for %d wallets in WalletDB (%s).',
|
|
wids.size, tx.txid());
|
|
|
|
// If this is our first transaction
|
|
// in a block, set the start block here.
|
|
if (block)
|
|
await this.maybeMark(block);
|
|
|
|
// 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;
|
|
};
|
|
|
|
/**
|
|
* Unconfirm a transaction from all
|
|
* relevant wallets without a lock.
|
|
* @private
|
|
* @param {TXMapRecord} tx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._unconfirm = async function _unconfirm(tx) {
|
|
for (const wid of tx.wids) {
|
|
const wallet = await this.get(wid);
|
|
assert(wallet);
|
|
await wallet.unconfirm(tx.hash);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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.');
|
|
|
|
// Try to rollback.
|
|
if (await this.rollback(entry.height))
|
|
return;
|
|
|
|
// If we rolled back to the
|
|
// start block, we need a rescan.
|
|
await this.scan();
|
|
};
|
|
|
|
/**
|
|
* WalletOptions
|
|
* @alias module:wallet.WalletOptions
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
function WalletOptions(options) {
|
|
if (!(this instanceof WalletOptions))
|
|
return new WalletOptions(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.spv = false;
|
|
this.witness = false;
|
|
this.checkpoints = false;
|
|
this.startHeight = 0;
|
|
this.keepBlocks = this.network.block.keepBlocks;
|
|
this.wipeNoReally = false;
|
|
this.apiKey = null;
|
|
this.walletAuth = false;
|
|
this.noAuth = false;
|
|
this.ssl = false;
|
|
this.host = '127.0.0.1';
|
|
this.port = this.network.rpcPort + 2;
|
|
this.listen = false;
|
|
|
|
if (options)
|
|
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);
|
|
this.keepBlocks = this.network.block.keepBlocks;
|
|
this.port = this.network.rpcPort + 2;
|
|
}
|
|
|
|
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(util.isU64(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(util.isU32(options.maxFiles));
|
|
this.maxFiles = options.maxFiles;
|
|
}
|
|
|
|
if (options.cacheSize != null) {
|
|
assert(util.isU64(options.cacheSize));
|
|
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;
|
|
}
|
|
|
|
if (options.apiKey != null) {
|
|
assert(typeof options.apiKey === 'string');
|
|
this.apiKey = options.apiKey;
|
|
}
|
|
|
|
if (options.walletAuth != null) {
|
|
assert(typeof options.walletAuth === 'boolean');
|
|
this.walletAuth = options.walletAuth;
|
|
}
|
|
|
|
if (options.noAuth != null) {
|
|
assert(typeof options.noAuth === 'boolean');
|
|
this.noAuth = options.noAuth;
|
|
}
|
|
|
|
if (options.ssl != null) {
|
|
assert(typeof options.ssl === 'boolean');
|
|
this.ssl = options.ssl;
|
|
}
|
|
|
|
if (options.host != null) {
|
|
assert(typeof options.host === 'string');
|
|
this.host = options.host;
|
|
}
|
|
|
|
if (options.port != null) {
|
|
assert(typeof options.port === 'number');
|
|
this.port = options.port;
|
|
}
|
|
|
|
if (options.listen != null) {
|
|
assert(typeof options.listen === 'boolean');
|
|
this.listen = options.listen;
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Instantiate chain options from object.
|
|
* @param {Object} options
|
|
* @returns {WalletOptions}
|
|
*/
|
|
|
|
WalletOptions.fromOptions = function fromOptions(options) {
|
|
return new WalletOptions().fromOptions(options);
|
|
};
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = WalletDB;
|