/*! * walletdb.js - storage for wallets * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; /* * Database Layout: * p/[address] -> path data * w/[wid] -> wallet * l/[id] -> wid * a/[wid]/[index] -> account * i/[wid]/[name] -> account index * t/[wid]/* -> txdb * R -> tip * b/[hash] -> wallet block * e/[hash] -> tx->wid map */ var bcoin = require('./env'); var AsyncObject = require('./async'); var utils = require('./utils'); var pad32 = utils.pad32; var assert = utils.assert; var constants = bcoin.protocol.constants; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); var TXDB = require('./txdb'); var keyTypes = bcoin.keyring.types; /* String Keys var layout = { p: function(hash) { return 'p' + hash; }, pp: function(key) { return key.slice(1); }, w: function(wid) { return 'w' + pad32(wid); }, ww: function(key) { return +key.slice(1); }, l: function(id) { return 'l' + id; }, ll: function(key) { return key.slice(1); }, a: function a(wid, index) { return 'a' + pad32(wid) + pad32(index); }, i: function i(wid, name) { return 'i' + pad32(wid) + name; }, ii: function ii(key) { return [+key.slice(1, 11), key.slice(11)]; }, R: 'R', b: function b(hash) { return 'b' + hash; }, e: function e(hash) { return 'e' + hash; } }; */ var layout = { p: function(hash) { var key = new Buffer(1 + (hash.length / 2)); key[0] = 0x70; key.write(hash, 1, 'hex'); return key; }, pp: function(key) { return key.toString('hex', 1); }, w: function(wid) { var key = new Buffer(5); key[0] = 0x77; key.writeUInt32BE(wid, 1, true); return key; }, ww: function(key) { return key.readUInt32BE(1, true); }, l: function(id) { var key = new Buffer(1 + id.length); key[0] = 0x6c; key.write(id, 1, 'ascii'); return key; }, ll: function(key) { return key.toString('ascii', 1); }, a: function a(wid, index) { var key = new Buffer(9); key[0] = 0x61; key.writeUInt32BE(wid, 1, true); key.writeUInt32BE(index, 5, true); return key; }, i: function i(wid, name) { var key = new Buffer(5 + name.length); key[0] = 0x69; key.writeUInt32BE(wid, 1, true); key.write(name, 5, 'ascii'); return key; }, ii: function ii(key) { return [+key.readUInt32BE(1, true), key.toString('ascii', 5)]; }, R: 'R', b: function b(hash) { var key = new Buffer(33); key[0] = 0x62; key.write(hash, 1, 'hex'); return key; }, e: function e(hash) { var key = new Buffer(33); key[0] = 0x65; key.write(hash, 1, 'hex'); return key; } }; /** * WalletDB * @exports 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); if (!options) options = {}; AsyncObject.call(this); this.options = options; this.network = bcoin.network.get(options.network); this.fees = options.fees; this.logger = options.logger || bcoin.defaultLogger; this.batches = {}; this.wallets = {}; this.workerPool = null; this.tip = this.network.genesis.hash; this.height = 0; this.depth = 0; // We need one read lock for `get` and `create`. // It will hold locks specific to wallet ids. this.readLock = new MappedLock(this); this.writeLock = new MappedLock(this); this.txLock = new bcoin.locker(this); this.walletCache = new bcoin.lru(10000, 1); this.accountCache = new bcoin.lru(10000, 1); this.pathCache = new bcoin.lru(100000, 1); // Try to optimize for up to 1m addresses. // We use a regular bloom filter here // because we never want members to // lose membership, even if quality // degrades. // Memory used: 1.7mb this.filter = this.options.useFilter !== false ? bcoin.bloom.fromRate(1000000, 0.001, -1) : null; this.db = bcoin.ldb({ location: this.options.location, db: this.options.db, cacheSize: 8 << 20, writeBufferSize: 4 << 20, bufferKeys: true }); if (bcoin.useWorkers) this.workerPool = new bcoin.workers(); this._init(); } utils.inherits(WalletDB, AsyncObject); /** * Initialize wallet db. * @private */ WalletDB.prototype._init = function _init() { var self = this; if (bcoin.useWorkers) { this.workerPool.on('error', function(err) { self.emit('error', err); }); } }; /** * Open the walletdb, wait for the database to load. * @alias WalletDB#open * @param {Function} callback */ WalletDB.prototype._open = function open(callback) { var self = this; this.db.open(function(err) { if (err) return callback(err); self.db.checkVersion('V', 1, function(err) { if (err) return callback(err); self.writeGenesis(function(err) { if (err) return callback(err); self.getDepth(function(err, depth) { if (err) return callback(err); self.depth = depth; self.logger.info( 'WalletDB loaded (depth=%d, height=%d).', depth, self.height); self.loadFilter(callback); }); }); }); }); }; /** * Close the walletdb, wait for the database to close. * @alias WalletDB#close * @param {Function} callback */ WalletDB.prototype._close = function close(callback) { var self = this; var keys = Object.keys(this.wallets); var wallet; utils.forEachSerial(keys, function(key, next) { wallet = self.wallets[key]; wallet.destroy(next); }, function(err) { if (err) return callback(err); self.db.close(callback); }); }; /** * Get current wallet wid depth. * @private * @param {Function} callback */ WalletDB.prototype.getDepth = function getDepth(callback) { var iter, parts, depth; // 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. iter = this.db.iterator({ gte: layout.w(0), lte: layout.w(0xffffffff), reverse: true, keys: true, fillCache: false }); iter.next(function(err, key, value) { if (err) { return iter.end(function() { callback(err); }); } iter.end(function(err) { if (err) return callback(err); if (key === undefined) return callback(null, 1); depth = layout.ww(key); callback(null, depth + 1); }); }); }; /** * Start batch. * @private * @param {WalletID} wid */ WalletDB.prototype.start = function start(wid) { assert(utils.isNumber(wid), 'Bad ID for batch.'); assert(!this.batches[wid], 'Batch already started.'); this.batches[wid] = this.db.batch(); }; /** * Drop batch. * @private * @param {WalletID} wid */ WalletDB.prototype.drop = function drop(wid) { var batch = this.batch(wid); batch.clear(); delete this.batches[wid]; }; /** * Get batch. * @private * @param {WalletID} wid * @returns {Leveldown.Batch} */ WalletDB.prototype.batch = function batch(wid) { var batch; assert(utils.isNumber(wid), 'Bad ID for batch.'); batch = this.batches[wid]; assert(batch, 'Batch does not exist.'); return batch; }; /** * Save batch. * @private * @param {WalletID} wid * @param {Function} callback */ WalletDB.prototype.commit = function commit(wid, callback) { var batch = this.batch(wid); delete this.batches[wid]; batch.write(callback); }; /** * Load the bloom filter into memory. * @private * @param {Function} callback */ WalletDB.prototype.loadFilter = function loadFilter(callback) { var self = this; if (!this.filter) return callback(); this.db.iterate({ gte: layout.p(constants.NULL_HASH), lte: layout.p(constants.HIGH_HASH), transform: function(key) { key = layout.pp(key); self.filter.add(key, 'hex'); } }, callback); }; /** * Test the bloom filter against an array of address hashes. * @private * @param {Hash[]} addresses * @returns {Boolean} */ WalletDB.prototype.testFilter = function testFilter(addresses) { var i; if (!this.filter) return true; for (i = 0; i < addresses.length; i++) { if (this.filter.test(addresses[i], 'hex')) return true; } return false; }; /** * Dump database (for debugging). * @param {Function} callback - Returns [Error, Object]. */ WalletDB.prototype.dump = function dump(callback) { var records = {}; this.db.each({ gte: ' ', lte: '~', keys: true, values: true }, function(key, value, next) { records[key] = value; next(); }, function(err) { if (err) return callback(err); return callback(null, records); }); }; /** * Register an object with the walletdb. * @param {Object} object */ WalletDB.prototype.register = function register(wallet) { assert(!this.wallets[wallet.wid]); this.wallets[wallet.wid] = wallet; }; /** * Unregister a object with the walletdb. * @param {Object} object * @returns {Boolean} */ WalletDB.prototype.unregister = function unregister(wallet) { assert(this.wallets[wallet.wid]); delete this.wallets[wallet.wid]; }; /** * Map wallet label to wallet id. * @param {String} label * @param {Function} callback */ WalletDB.prototype.getWalletID = function getWalletID(id, callback) { var self = this; var wid; if (!id) return callback(); if (typeof id === 'number') return callback(null, id); wid = this.walletCache.get(id); if (wid) return callback(null, wid); this.db.fetch(layout.l(id), function(data) { wid = data.readUInt32LE(0, true); self.walletCache.set(id, wid); return wid; }, callback); }; /** * Get a wallet from the database, setup watcher. * @param {WalletID} wid * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype.get = function get(wid, callback) { var self = this; this.getWalletID(wid, function(err, wid) { if (err) return callback(err); if (!wid) return callback(); self._get(wid, function(err, wallet, watched) { if (err) return callback(err); if (!wallet) return callback(); if (watched) return callback(null, wallet); try { self.register(wallet); } catch (e) { return callback(e); } wallet.open(function(err) { if (err) return callback(err); return callback(null, wallet); }); }); }); }; /** * Get a wallet from the database, do not setup watcher. * @private * @param {WalletID} wid * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype._get = function get(wid, callback) { var self = this; var unlock, wallet; unlock = this.readLock.lock(wid, get, [wid, callback]); if (!unlock) return; callback = utils.wrap(callback, unlock); wallet = this.wallets[wid]; if (wallet) return callback(null, wallet, true); this.db.fetch(layout.w(wid), function(data) { return bcoin.wallet.fromRaw(self, data); }, callback); }; /** * Save a wallet to the database. * @param {Wallet} wallet * @param {Function} callback */ WalletDB.prototype.save = function save(wallet) { var batch = this.batch(wallet.wid); var wid = new Buffer(4); this.walletCache.set(wallet.id, wallet.wid); batch.put(layout.w(wallet.wid), wallet.toRaw()); wid.writeUInt32LE(wallet.wid, 0, true); batch.put(layout.l(wallet.id), wid); }; /** * Test an api key against a wallet's api key. * @param {WalletID} wid * @param {String} token * @param {Function} callback */ WalletDB.prototype.auth = function auth(wid, token, callback) { this.get(wid, function(err, wallet) { if (err) return callback(err); if (!wallet) return callback(); if (typeof token === 'string') { if (!utils.isHex(token)) return callback(new Error('Authentication error.')); token = new Buffer(token, 'hex'); } // Compare in constant time: if (!utils.ccmp(token, wallet.token)) return callback(new Error('Authentication error.')); return callback(null, wallet); }); }; /** * Create a new wallet, save to database, setup watcher. * @param {Object} options - See {@link Wallet}. * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype.create = function create(options, callback) { var self = this; var wallet, unlock; if (typeof options === 'function') { callback = options; options = {}; } unlock = this.writeLock.lock(options.id, create, [options, callback]); if (!unlock) return; callback = utils.wrap(callback, unlock); this.has(options.id, function(err, exists) { if (err) return callback(err); if (err) return callback(err); if (exists) return callback(new Error('Wallet already exists.')); try { wallet = bcoin.wallet.fromOptions(self, options); wallet.wid = self.depth++; } catch (e) { return callback(e); } try { self.register(wallet); } catch (e) { return callback(e); } wallet.init(options, function(err) { if (err) return callback(err); self.logger.info('Created wallet %s.', wallet.id); return callback(null, wallet); }); }); }; /** * Test for the existence of a wallet. * @param {WalletID} id * @param {Function} callback */ WalletDB.prototype.has = function has(id, callback) { this.getWalletID(id, function(err, wid) { if (err) return callback(err); return callback(null, wid != null); }); }; /** * Attempt to create wallet, return wallet if already exists. * @param {Object} options - See {@link Wallet}. * @param {Function} callback */ WalletDB.prototype.ensure = function ensure(options, callback) { var self = this; this.get(options.id, function(err, wallet) { if (err) return callback(err); if (wallet) return callback(null, wallet); self.create(options, callback); }); }; /** * Get an account from the database. * @param {WalletID} wid * @param {String|Number} name - Account name/index. * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype.getAccount = function getAccount(wid, name, callback) { var self = this; this.getAccountIndex(wid, name, function(err, index) { if (err) return callback(err); if (index === -1) return callback(); self._getAccount(wid, index, function(err, account) { if (err) return callback(err); if (!account) return callback(); account.open(function(err) { if (err) return callback(err); return callback(null, account); }); }); }); }; /** * Get an account from the database. Do not setup watcher. * @private * @param {WalletID} wid * @param {Number} index - Account index. * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype._getAccount = function getAccount(wid, index, callback) { var self = this; var key = wid + '/' + index; var account = this.accountCache.get(key); if (account) return callback(null, account); this.db.fetch(layout.a(wid, index), function(data) { account = bcoin.account.fromRaw(self, data); self.accountCache.set(key, account); return account; }, callback); }; /** * List account names and indexes from the db. * @param {WalletID} wid * @param {Function} callback - Returns [Error, Array]. */ WalletDB.prototype.getAccounts = function getAccounts(wid, callback) { var map = []; var i, accounts; this.db.iterate({ gte: layout.i(wid, ''), lte: layout.i(wid, '\xff'), values: true, parse: function(value, key) { var name = layout.ii(key)[1]; var index = value.readUInt32LE(0, true); map[index] = name; } }, function(err) { if (err) return callback(err); // Get it out of hash table mode. accounts = new Array(map.length); for (i = 0; i < map.length; i++) { assert(map[i] != null); accounts[i] = map[i]; } return callback(null, accounts); }); }; /** * Lookup the corresponding account name's index. * @param {WalletID} wid * @param {String|Number} name - Account name/index. * @param {Function} callback - Returns [Error, Number]. */ WalletDB.prototype.getAccountIndex = function getAccountIndex(wid, name, callback) { if (!wid) return callback(null, -1); if (name == null) return callback(null, -1); if (typeof name === 'number') return callback(null, name); this.db.get(layout.i(wid, name), function(err, index) { if (err) return callback(err); if (!index) return callback(null, -1); return callback(null, index.readUInt32LE(0, true)); }); }; /** * Save an account to the database. * @param {Account} account * @param {Function} callback */ WalletDB.prototype.saveAccount = function saveAccount(account) { var batch = this.batch(account.wid); var index = new Buffer(4); var key = account.wid + '/' + account.accountIndex; index.writeUInt32LE(account.accountIndex, 0, true); batch.put(layout.a(account.wid, account.accountIndex), account.toRaw()); batch.put(layout.i(account.wid, account.name), index); this.accountCache.set(key, account); }; /** * Create an account. * @param {Object} options - See {@link Account} options. * @param {Function} callback - Returns [Error, {@link Account}]. */ WalletDB.prototype.createAccount = function createAccount(options, callback) { var self = this; var account; this.hasAccount(options.wid, options.accountIndex, function(err, exists) { if (err) return callback(err); if (err) return callback(err); if (exists) return callback(new Error('Account already exists.')); try { account = bcoin.account.fromOptions(self, options); } catch (e) { return callback(e); } account.init(function(err) { if (err) return callback(err); self.logger.info('Created account %s/%s/%d.', account.id, account.name, account.accountIndex); return callback(null, account); }); }); }; /** * Test for the existence of an account. * @param {WalletID} wid * @param {String|Number} account * @param {Function} callback - Returns [Error, Boolean]. */ WalletDB.prototype.hasAccount = function hasAccount(wid, account, callback) { var self = this; var key; if (!wid) return callback(null, false); this.getAccountIndex(wid, account, function(err, index) { if (err) return callback(err); if (index === -1) return callback(null, false); key = wid + '/' + index; if (self.accountCache.has(key)) return callback(null, true); self.db.has(layout.a(wid, index), callback); }); }; /** * Save an address to the path map. * The path map exists in the form of: * `p/[address-hash] -> {walletid1=path1, walletid2=path2, ...}` * @param {WalletID} wid * @param {KeyRing[]} addresses * @param {Function} callback */ WalletDB.prototype.saveAddress = function saveAddress(wid, addresses, callback) { var self = this; var items = []; var batch = this.batch(wid); var i, address, path; for (i = 0; i < addresses.length; i++) { address = addresses[i]; path = Path.fromKeyRing(address); items.push([address.getAddress(), path]); if (address.witness) items.push([address.getProgramAddress(), path]); } utils.forEachSerial(items, function(item, next) { var address = item[0]; var path = item[1]; var hash = address.getHash('hex'); if (self.filter) self.filter.add(hash, 'hex'); self.emit('save address', address, path); self.getPaths(hash, function(err, paths) { if (err) return next(err); if (!paths) paths = {}; if (paths[wid]) return next(); paths[wid] = path; self.pathCache.set(hash, paths); batch.put(layout.p(hash), serializePaths(paths)); next(); }); }, callback); }; /** * Retrieve paths by hash. * @param {Hash} hash * @param {Function} callback */ WalletDB.prototype.getPaths = function getPaths(hash, callback) { var self = this; var paths; if (!hash) return callback(); paths = this.pathCache.get(hash); if (paths) return callback(null, paths); this.db.fetch(layout.p(hash), parsePaths, function(err, paths) { if (err) return callback(err); if (!paths) return callback(); self.pathCache.set(hash, paths); return callback(null, paths); }); }; /** * Test whether an address hash exists in the * path map and is relevant to the wallet id. * @param {WalletID} wid * @param {Hash} address * @param {Function} callback */ WalletDB.prototype.hasAddress = function hasAddress(wid, address, callback) { this.getPaths(address, function(err, paths) { if (err) return callback(err); if (!paths || !paths[wid]) return callback(null, false); return callback(null, true); }); }; /** * Get all address hashes. * @param {WalletID} wid * @param {Function} callback */ WalletDB.prototype.getAddresses = function getAddresses(wid, callback) { if (!callback) { callback = wid; wid = null; } this.db.iterate({ gte: layout.p(constants.NULL_HASH), lte: layout.p(constants.HIGH_HASH), values: true, parse: function(value, key) { var paths = parsePaths(value); if (wid && !paths[wid]) return; return layout.pp(key); } }, callback); }; /** * Get all wallet ids. * @param {Function} callback */ WalletDB.prototype.getWallets = function getWallets(callback) { this.db.iterate({ gte: layout.l(''), lte: layout.l('\xff'), transform: function(key) { return layout.ll(key); } }, callback); }; /** * Rescan the blockchain. * @param {ChainDB} chaindb * @param {Function} callback */ WalletDB.prototype.rescan = function rescan(chaindb, callback) { var self = this; this.getAddresses(function(err, hashes) { if (err) return callback(err); self.logger.info('Scanning for %d addresses.', hashes.length); chaindb.scan(self.height, hashes, function(block, txs, next) { self.addBlock(block, txs, next); }, callback); }); }; /** * Helper function to get a wallet. * @private * @param {WalletID} wid * @param {Function} callback * @param {Function} handler */ WalletDB.prototype.fetchWallet = function fetchWallet(wid, callback, handler) { this.get(wid, function(err, wallet) { if (err) return callback(err); if (!wallet) return callback(new Error('No wallet.')); handler(wallet, function(err, res1, res2) { if (err) return callback(err); callback(null, res1, res2); }); }); }; /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx * @param {Function} callback - Returns [Error, {@link PathInfo[]}]. */ WalletDB.prototype.mapWallets = function mapWallets(tx, callback) { var self = this; var addresses = tx.getHashes('hex'); var wallets; if (!this.testFilter(addresses)) return callback(); this.getTable(addresses, function(err, table) { if (err) return callback(err); if (!table) return callback(); wallets = PathInfo.map(self, tx, table); return callback(null, wallets); }); }; /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx * @param {Function} callback - Returns [Error, {@link PathInfo}]. */ WalletDB.prototype.getPathInfo = function getPathInfo(wallet, tx, callback) { var self = this; var addresses = tx.getHashes('hex'); var info; this.getTable(addresses, function(err, table) { if (err) return callback(err); if (!table) return callback(); info = new PathInfo(self, wallet.wid, tx, table); info.id = wallet.id; return callback(null, info); }); }; /** * Map address hashes to paths. * @param {Hash[]} address - Address hashes. * @param {Function} callback - Returns [Error, {@link AddressTable}]. */ WalletDB.prototype.getTable = function getTable(addresses, callback) { var self = this; var table = {}; var count = 0; var i, keys, values; utils.forEachSerial(addresses, function(address, next) { self.getPaths(address, function(err, paths) { if (err) return next(err); if (!paths) { assert(!table[address]); table[address] = []; return next(); } keys = Object.keys(paths); values = []; for (i = 0; i < keys.length; i++) values.push(paths[keys[i]]); assert(!table[address]); table[address] = values; count += values.length; return next(); }); }, function(err) { if (err) return callback(err); if (count === 0) return callback(); return callback(null, table); }); }; /** * Write the genesis block as the best hash. * @param {Function} callback */ WalletDB.prototype.writeGenesis = function writeGenesis(callback) { var self = this; this.getTip(function(err, block) { if (err) return callback(err); if (block) { self.tip = block.hash; self.height = block.height; return callback(); } self.setTip(self.network.genesis.hash, 0, callback); }); }; /** * Get the best block hash. * @param {Function} callback */ WalletDB.prototype.getTip = function getTip(callback) { this.db.fetch(layout.R, function(data) { return WalletBlock.fromTip(data); }, callback); }; /** * Write the best block hash. * @param {Hash} hash * @param {Number} height * @param {Function} callback */ WalletDB.prototype.setTip = function setTip(hash, height, callback) { var self = this; var block = new WalletBlock(hash, height); this.db.put(layout.R, block.toTip(), function(err) { if (err) return callback(err); self.tip = block.hash; self.height = block.height; return callback(); }); }; /** * Connect a block. * @param {WalletBlock} block * @param {Function} callback */ WalletDB.prototype.writeBlock = function writeBlock(block, matches, callback) { var self = this; var batch = this.db.batch(); var i, hash, wallets; batch.put(layout.R, block.toTip()); if (block.hashes.length > 0) { batch.put(layout.b(block.hash), block.toRaw()); for (i = 0; i < block.hashes.length; i++) { hash = block.hashes[i]; wallets = matches[i]; batch.put(layout.e(hash), serializeWallets(wallets)); } } batch.write(callback); }; /** * Disconnect a block. * @param {WalletBlock} block * @param {Function} callback */ WalletDB.prototype.unwriteBlock = function unwriteBlock(block, callback) { var self = this; var batch = this.db.batch(); var prev = new WalletBlock(block.prevBlock, block.height - 1); batch.put(layout.R, prev.toTip()); batch.del(layout.b(block.hash)); batch.write(callback); }; /** * Get a wallet block (with hashes). * @param {Hash} hash * @param {Function} callback */ WalletDB.prototype.getBlock = function getBlock(hash, callback) { this.db.fetch(layout.b(hash), function(err, data) { return WalletBlock.fromRaw(hash, data); }, callback); }; /** * Get a TX->Wallet map. * @param {Hash} hash * @param {Function} callback */ WalletDB.prototype.getWalletsByTX = function getWalletsByTX(hash, callback) { this.db.fetch(layout.e(hash), parseWallets, callback); }; /** * Add a block's transactions and write the new best hash. * @param {ChainEntry} entry * @param {Function} callback */ WalletDB.prototype.addBlock = function addBlock(entry, txs, callback, force) { var self = this; var block, matches, hash, unlock; unlock = this.txLock.lock(addBlock, [entry, txs, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); if (this.options.useCheckpoints) { if (entry.height <= this.network.checkpoints.lastHeight) return this.setTip(entry.hash, entry.height, callback); } block = WalletBlock.fromEntry(entry); matches = []; // Update these early so transactions // get correct confirmation calculations. this.tip = block.hash; this.height = block.height; // NOTE: Atomicity doesn't matter here. If we crash // during this loop, the automatic rescan will get // the database back into the correct state. utils.forEachSerial(txs, function(tx, next) { self.addTX(tx, function(err, wallets) { if (err) return next(err); if (!wallets) return next(); hash = tx.hash('hex'); block.hashes.push(hash); matches.push(wallets); next(); }, true); }, function(err) { if (err) return callback(err); self.writeBlock(block, matches, callback); }); }; /** * Unconfirm a block's transactions * and write the new best hash (SPV version). * @param {ChainEntry} entry * @param {Function} callback */ WalletDB.prototype.removeBlock = function removeBlock(entry, callback, force) { var self = this; var unlock; unlock = this.txLock.lock(removeBlock, [entry, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); // Note: // If we crash during a reorg, there's not much to do. // Reorgs cannot be rescanned. The database will be // in an odd state, with some txs being confirmed // when they shouldn't be. That being said, this // should eventually resolve itself when a new block // comes in. this.getBlock(entry.hash, function(err, block) { if (err) return callback(err); // Not a saved block, but we // still want to reset the tip. if (!block) block = WalletBlock.fromEntry(entry); // Unwrite the tip as fast as we can. self.unwriteBlock(block, function(err) { if (err) return callback(err); utils.forEachSerial(block.hashes, function(hash, next) { self.getWalletsByTX(hash, function(err, wallets) { if (err) return next(err); if (!wallets) return next(); utils.forEachSerial(wallets, function(wid, next) { self.get(wid, function(err, wallet) { if (err) return next(err); if (!wallet) return next(); wallet.tx.unconfirm(hash, next); }); }, function(err) { if (err) return callback(err); self.tip = block.hash; self.height = block.height; return callback(); }); }); }); }); }); }; /** * Add a transaction to the database, map addresses * to wallet IDs, potentially store orphans, resolve * orphans, or confirm a transaction. * @param {TX} tx * @param {Function} callback - Returns [Error]. */ WalletDB.prototype.addTX = function addTX(tx, callback, force) { var self = this; // Note: // Atomicity doesn't matter here. If we crash, // the automatic rescan will get the database // back in the correct state. this.mapWallets(tx, function(err, wallets) { if (err) return callback(err); if (!wallets) return callback(); self.logger.info( 'Incoming transaction for %d wallets (%s).', wallets.length, tx.rhash); utils.forEachSerial(wallets, function(info, next) { self.get(info.wid, function(err, wallet) { if (err) return next(err); if (!wallet) return next(); self.logger.debug('Adding tx to wallet: %s', info.wid); info.id = wallet.id; wallet.tx.add(tx, info, function(err) { if (err) return next(err); wallet.handleTX(info, next); }); }); }, function(err) { if (err) return callback(err); return callback(null, wallets); }); }); }; /** * Get the corresponding path for an address hash. * @param {WalletID} wid * @param {Hash} address * @param {Function} callback */ WalletDB.prototype.getPath = function getPath(wid, address, callback) { this.getPaths(address, function(err, paths) { if (err) return callback(err); if (!paths || !paths[wid]) return callback(); return callback(null, paths[wid]); }); }; /** * Path * @constructor * @private * @property {WalletID} wid * @property {String} name - Account name. * @property {Number} account - Account index. * @property {Number} change - Change index. * @property {Number} index - Address index. * @property {Address|null} address */ function Path() { if (!(this instanceof Path)) return new Path(); this.wid = null; this.name = null; this.account = 0; this.change = 0; this.index = 0; // NOTE: Passed in by caller. this.id = null; } /** * Inject properties from serialized data. * @private * @param {Buffer} data */ Path.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); this.wid = p.readU32(); this.name = p.readVarString('utf8'); this.account = p.readU32(); this.change = p.readU32(); this.index = p.readU32(); return this; }; /** * Instantiate path from serialized data. * @param {Buffer} data * @returns {Path} */ Path.fromRaw = function fromRaw(data) { return new Path().fromRaw(data); }; /** * Serialize path. * @returns {Buffer} */ Path.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); p.writeU32(this.wid); p.writeVarString(this.name, 'utf8'); p.writeU32(this.account); p.writeU32(this.change); p.writeU32(this.index); if (!writer) p = p.render(); return p; }; /** * Inject properties from keyring. * @private * @param {WalletID} wid * @param {KeyRing} address */ Path.prototype.fromKeyRing = function fromKeyRing(address) { this.wid = address.wid; this.id = address.id; this.name = address.name; this.account = address.account; this.change = address.change; this.index = address.index; return this; }; /** * Instantiate path from keyring. * @param {WalletID} wid * @param {KeyRing} address * @returns {Path} */ Path.fromKeyRing = function fromKeyRing(address) { return new Path().fromKeyRing(address); }; /** * Convert path object to string derivation path. * @returns {String} */ Path.prototype.toPath = function() { return 'm/' + this.account + '\'/' + this.change + '/' + this.index; }; /** * Convert path to a json-friendly object. * @returns {Object} */ Path.prototype.toJSON = function toJSON() { return { name: this.name, change: this.change === 1, path: this.toPath() }; }; /** * Inject properties from json object. * @private * @param {Object} json */ Path.prototype.fromJSON = function fromJSON(json) { var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); assert(indexes.length === 3); assert(indexes[0] >= 0); indexes[0] -= constants.hd.HARDENED; this.wid = json.wid; this.id = json.id; this.name = json.name; this.account = indexes[0]; this.change = indexes[1]; this.index = indexes[2]; return this; }; /** * Instantiate path from json object. * @param {Object} json * @returns {Path} */ Path.fromJSON = function fromJSON(json) { return new Path().fromJSON(json); }; /** * Inspect the path. * @returns {String} */ Path.prototype.inspect = function() { return ''; }; /** * Path Info */ function PathInfo(db, wid, tx, table) { if (!(this instanceof PathInfo)) return new PathInfo(db, wid, tx, table); // Reference to the walletdb. this.db = db; // All relevant Accounts for // inputs and outputs (for database indexing). this.accounts = []; // All output paths (for deriving during sync). this.paths = []; // Wallet ID this.wid = wid; // Wallet Label (passed in by caller). this.id = null; // Map of address hashes->paths (for everything). this.table = null; // Map of address hashes->paths (specific to wallet). this.pathMap = {}; // Current transaction. this.tx = null; // Wallet-specific details cache. this._details = null; this._json = null; if (tx) this.fromTX(tx, table); } PathInfo.map = function map(db, tx, table) { var hashes = Object.keys(table); var wallets = []; var info = []; var uniq = {}; var i, j, hash, paths, path, wid; for (i = 0; i < hashes.length; i++) { hash = hashes[i]; paths = table[hash]; for (j = 0; j < paths.length; j++) { path = paths[j]; if (!uniq[path.wid]) { uniq[path.wid] = true; wallets.push(path.wid); } } } if (wallets.length === 0) return; for (i = 0; i < wallets.length; i++) { wid = wallets[i]; info.push(new PathInfo(db, wid, tx, table)); } return info; }; PathInfo.prototype.fromTX = function fromTX(tx, table) { var uniq = {}; var i, j, hashes, hash, paths, path; this.tx = tx; this.table = table; hashes = Object.keys(table); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; paths = table[hash]; for (j = 0; j < paths.length; j++) { path = paths[j]; if (path.wid !== this.wid) continue; this.pathMap[hash] = path; if (!uniq[path.account]) { uniq[path.account] = true; this.accounts.push(path.account); } } } hashes = tx.getOutputHashes('hex'); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; paths = table[hash]; for (j = 0; j < paths.length; j++) { path = paths[j]; if (path.wid !== this.wid) continue; this.paths.push(path); } } return this; }; PathInfo.fromTX = function fromTX(db, wid, tx, table) { return new PathInfo(db, wid).fromTX(tx, table); }; /** * Test whether the map has paths * for a given address hash. * @param {Hash} address * @returns {Boolean} */ PathInfo.prototype.hasPath = function hasPath(address) { if (!address) return false; return this.pathMap[address] != null; }; /** * Get paths for a given address hash. * @param {Hash} address * @returns {Path[]|null} */ PathInfo.prototype.getPath = function getPath(address) { if (!address) return; return this.pathMap[address]; }; PathInfo.prototype.toDetails = function toDetails() { var details = this._details; if (!details) { details = new TXDB.Details(this); this._details = details; } return details; }; PathInfo.prototype.toJSON = function toJSON() { var json = this._json; if (!json) { json = this.toDetails().toJSON(); this._json = json; } return json; }; /* * Helpers */ function parsePaths(data) { var p = new BufferReader(data); var out = {}; var path; while (p.left()) { path = Path.fromRaw(p); out[path.wid] = path; } return out; } function serializePaths(out) { var p = new BufferWriter(); var keys = Object.keys(out); var i, wid, path; for (i = 0; i < keys.length; i++) { wid = keys[i]; path = out[wid]; path.toRaw(p); } return p.render(); } function serializeWallets(wallets) { var p = new BufferWriter(); var i, info; for (i = 0; i < wallets.length; i++) { info = wallets[i]; p.writeU32(info.wid); } return p.render(); } function parseWallets(data) { var p = new BufferReader(data); var wallets = []; while (p.left()) wallets.push(p.readU32()); return wallets; } function WalletBlock(hash, height) { if (!(this instanceof WalletBlock)) return new WalletBlock(hash, height); this.hash = hash || constants.NULL_HASH; this.height = height != null ? height : -1; this.prevBlock = constants.NULL_HASH; this.hashes = []; } WalletBlock.prototype.fromEntry = function fromEntry(entry) { this.hash = entry.hash; this.height = entry.height; this.prevBlock = entry.prevBlock; return this; }; WalletBlock.prototype.fromJSON = function fromJSON(json) { this.hash = utils.revHex(json.hash); this.height = json.height; if (json.prevBlock) this.prevBlock = utils.revHex(json.prevBlock); return this; }; WalletBlock.prototype.fromRaw = function fromRaw(hash, data) { var p = new BufferReader(data); this.hash = hash; this.height = p.readU32(); while (p.left()) this.hashes.push(p.readHash('hex')); return this; }; WalletBlock.prototype.fromTip = function fromTip(data) { var p = new BufferReader(data); this.hash = p.readHash('hex'); this.height = p.readU32(); return this; }; WalletBlock.fromEntry = function fromEntry(entry) { return new WalletBlock().fromEntry(entry); }; WalletBlock.fromJSON = function fromJSON(json) { return new WalletBlock().fromJSON(json); }; WalletBlock.fromRaw = function fromRaw(hash, data) { return new WalletBlock().fromTip(hash, data); }; WalletBlock.fromTip = function fromTip(data) { return new WalletBlock().fromTip(data); }; WalletBlock.prototype.toTip = function toTip(data) { var p = new BufferWriter(); p.writeHash(this.hash); p.writeU32(this.height); return p.render(); }; WalletBlock.prototype.toRaw = function toRaw(data) { var p = new BufferWriter(); var i; p.writeU32(this.height); for (i = 0; i < this.hashes.length; i++) p.writeHash(this.hashes[i]); return p.render(); }; WalletBlock.prototype.toJSON = function toJSON() { return { hash: utils.revHex(this.hash), height: this.height }; }; function MappedLock(parent) { if (!(this instanceof MappedLock)) return new MappedLock(parent); this.parent = parent; this.jobs = []; this.busy = {}; } MappedLock.prototype.lock = function lock(key, func, args, force) { var self = this; var called; if (force || key == null) { assert(key == null || this.busy[key]); return function unlock() { assert(!called); called = true; }; } if (this.busy[key]) { this.jobs.push([func, args]); return; } this.busy[key] = true; return function unlock() { var item; assert(!called); called = true; delete self.busy[key]; if (self.jobs.length === 0) return; item = self.jobs.shift(); item[0].apply(self.parent, item[1]); }; }; /* * Expose */ exports = WalletDB; exports.Path = Path; module.exports = exports;