/*! * txdb.js - persistent transaction pool * 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: * t/[hash] -> extended tx * c/[hash]/[index] -> coin * s/[hash]/[index] -> spent by hash * o/[hash]/[index] -> orphan inputs * p/[hash] -> dummy (pending flag) * m/[time]/[hash] -> dummy (tx by time) * h/[height]/[hash] -> dummy (tx by height) * T/[id]/[name]/[hash] -> dummy (tx by wallet id) * P/[id]/[name]/[hash] -> dummy (pending tx by wallet/account id) * M/[id]/[name]/[time]/[hash] -> dummy (tx by time + id/account) * H/[id]/[name]/[height]/[hash] -> dummy (tx by height + id/account) * C/[id]/[name]/[hash]/[index] -> dummy (coin by id/account) */ var bcoin = require('./env'); var utils = require('./utils'); var assert = bcoin.utils.assert; var EventEmitter = require('events').EventEmitter; var DUMMY = new Buffer([0]); var pad32 = utils.pad32; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); /** * TXDB * @exports TXDB * @constructor * @param {LowlevelUp} db * @param {Object?} options * @param {Boolean?} options.mapAddress - Map addresses to IDs. * @param {Boolean?} options.indexAddress - Index addresses/IDs. * @param {Boolean?} options.indexExtra - Index timestamps, heights, etc. * @param {Boolean?} options.verify - Verify transactions as they * come in (note that this will not happen on the worker * pool -- only used for SPV). */ function TXDB(db, options) { if (!(this instanceof TXDB)) return new TXDB(db, options); EventEmitter.call(this); if (!options) options = {}; this.walletdb = db; this.db = db.db; this.logger = db.logger; this.network = db.network; this.options = options; this.network = bcoin.network.get(options.network); this.busy = false; this.jobs = []; this.locker = new bcoin.locker(this); this.coinCache = new bcoin.lru(10000, 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 ? bcoin.bloom.fromRate(1000000, 0.001, -1) : null; } utils.inherits(TXDB, EventEmitter); /** * Invoke the mutex lock. * @private * @returns {Function} unlock */ TXDB.prototype._lock = function _lock(func, args, force) { return this.locker.lock(func, args, force); }; /** * Load the bloom filter into memory. * @private * @param {Function} callback */ TXDB.prototype.loadFilter = function loadFilter(callback) { var self = this; if (!this.filter) return callback(); this.db.iterate({ gte: 'W', lte: 'W~', transform: function(key) { key = key.split('/')[1]; self.filter.add(key, 'hex'); } }, callback); }; /** * Test the bloom filter against an array of address hashes. * @private * @param {Hash[]} addresses * @returns {Boolean} */ TXDB.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; }; /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx * @param {Function} callback - Returns [Error, {@link WalletMap}]. */ TXDB.prototype.getMap = function getMap(tx, callback) { var addresses = tx.getHashes('hex'); var map; if (!this.testFilter(addresses)) return callback(); this.mapAddresses(addresses, function(err, table) { if (err) return callback(err); if (!table) return callback(); map = WalletMap.fromTX(table, tx); return callback(null, map); }); }; /** * Map address hashes to a wallet ID. * @param {Hash[]} address - Address hashes. * @param {Function} callback - Returns [Error, {@link AddressTable}]. */ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { var self = this; var table = {}; var count = 0; var i, keys, values; utils.forEachSerial(address, function(address, next) { self.walletdb.getAddress(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); }); }; /** * Add an orphan (tx hash + input index) * to orphan list. Stored by its required coin ID. * @private * @param {String} key - Required coin hash + index. * @param {Hash} hash - Orphan transaction hash. * @param {Number} index - Orphan input index. * @param {Function} callback - Returns [Error, Buffer]. */ TXDB.prototype._addOrphan = function _addOrphan(key, hash, index, callback) { var p; this.db.get('o/' + key, function(err, buf) { if (err) return callback(err); p = new BufferWriter(); if (buf) p.writeBytes(buf); p.writeHash(hash); p.writeU32(index); return callback(null, p.render()); }); }; /** * Retrieve orphan list by coin ID. * @private * @param {String} key * @param {Function} callback - Returns [Error, {@link Orphan}]. */ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { var self = this; this.db.fetch('o/' + key, function(buf) { var p = new BufferReader(buf); var orphans = []; while (p.left()) { orphans.push({ hash: p.readHash('hex'), index: p.readU32() }); } return orphans; }, function(err, orphans) { if (err) return callback(err); if (!orphans) return callback(); utils.forEachSerial(orphans, function(orphan, next) { self.getTX(orphan.hash, function(err, tx) { if (err) return next(err); orphan.tx = tx; next(); }); }, function(err) { if (err) return callback(err); return callback(null, orphans); }); }); }; /** * Write the genesis block as the best hash. * @param {Function} callback */ TXDB.prototype.writeGenesis = function writeGenesis(callback) { var self = this; var unlock, hash; unlock = this._lock(writeGenesis, [callback]); if (!unlock) return; callback = utils.wrap(callback, unlock); self.db.has('R', function(err, result) { if (err) return callback(err); if (result) return callback(); hash = new Buffer(self.network.genesis.hash, 'hex'); self.db.put('R', hash, callback); }); }; /** * Get the best block hash. * @param {Function} callback */ TXDB.prototype.getTip = function getTip(callback) { this.db.fetch('R', function(data) { return data.toString('hex'); }, callback); }; /** * Write the best block hash. * @param {Hash} hash * @param {Function} callback */ TXDB.prototype.writeTip = function writeTip(hash, callback) { if (typeof hash === 'string') hash = new Buffer(hash, 'hex'); this.db.put('R', hash, callback); }; /** * Add a block's transactions and write the new best hash. * @param {Block} block * @param {Function} callback */ TXDB.prototype.addBlock = function addBlock(block, txs, callback, force) { var self = this; var unlock; unlock = this._lock(addBlock, [block, txs, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); if (this.options.useCheckpoints) { if (block.height < this.network.checkpoints.lastHeight) return this.writeTip(block.hash, callback); } if (!Array.isArray(txs)) txs = [txs]; utils.forEachSerial(txs, function(tx, next) { self.add(tx, next, true); }, function(err) { if (err) return callback(err); self.writeTip(block.hash, callback); }); }; /** * Unconfirm a block's transactions * and write the new best hash (SPV version). * @param {Block} block * @param {Function} callback */ TXDB.prototype.removeBlock = function removeBlock(block, callback, force) { var self = this; var unlock; unlock = this._lock(removeBlock, [block, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); this.getHeightHashes(block.height, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(hash, next) { self.unconfirm(hash, next, true); }, function(err) { if (err) return callback(err); self.writeTip(block.prevBlock, 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]. */ TXDB.prototype.add = function add(tx, callback, force) { var self = this; return this.getMap(tx, function(err, map) { if (err) return callback(err); if (!map) return callback(null, false); self.logger.info( 'Incoming transaction for %d accounts.', map.outputs.length); self.logger.debug(map.outputs); return self._add(tx, map, callback, force); }); }; TXDB.prototype._add = function add(tx, map, callback, force) { var self = this; var updated = false; var batch, hash, i, j, unlock, path, paths, id; unlock = this._lock(add, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); if (tx.mutable) tx = tx.toTX(); // Attempt to confirm tx before adding it. this._confirm(tx, map, function(err, existing) { if (err) return callback(err); // Ignore if we already have this tx. if (existing) return callback(null, true, map); hash = tx.hash('hex'); batch = self.db.batch(); batch.put('t/' + hash, tx.toExtended()); if (tx.ts === 0) { batch.put('p/' + hash, DUMMY); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); } else { batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); } for (i = 0; i < map.accounts.length; i++) { path = map.accounts[i]; id = path.id + '/' + path.account; batch.put('T/' + id + '/' + hash, DUMMY); if (tx.ts === 0) { batch.put('P/' + id + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); } else { batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); } } // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { var prevout = input.prevout; var key, address; if (tx.isCoinbase()) return next(); address = input.getHash('hex'); // Only add orphans if this input is ours. if (!map.hasPaths(address)) return next(); self.getCoin(prevout.hash, prevout.index, function(err, coin) { if (err) return next(err); key = prevout.hash + '/' + prevout.index; batch.put('s/' + key, tx.hash()); if (coin) { // Add TX to inputs and spend money input.coin = coin; // Skip invalid transactions if (self.options.verify) { if (!tx.verifyInput(i)) return callback(null, false); } updated = true; paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; batch.del('C/' + id + '/' + key); } batch.del('c/' + key); self.coinCache.remove(key); return next(); } input.coin = null; self.isSpent(prevout.hash, prevout.index, function(err, spent) { if (err) return next(err); // Are we double-spending? // Replace older txs with newer ones. if (spent) { return self.getTX(prevout.hash, function(err, prev) { if (err) return next(err); if (!prev) return callback(new Error('Could not find double-spent coin.')); input.coin = bcoin.coin.fromTX(prev, prevout.index); // Skip invalid transactions if (self.options.verify) { if (!tx.verifyInput(i)) return callback(null, false, map); } return self._removeConflict(spent, tx, function(err, rtx, rmap) { if (err) return next(err); // Spender was not removed, the current // transaction is not elligible to be added. if (!rtx) return callback(null, false, map); self.emit('conflict', rtx, rmap); batch.clear(); self._add(tx, map, callback, true); }); }); } // Add orphan, if no parent transaction is yet known self._addOrphan(key, hash, i, function(err, orphans) { if (err) return next(err); batch.put('o/' + key, orphans); return next(); }); }); }); }, function(err) { if (err) return callback(err); // Add unspent outputs or resolve orphans utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getHash('hex'); var key = hash + '/' + i; var coin; // Do not add unspents for outputs that aren't ours. if (!map.hasPaths(address)) return next(); if (output.script.isUnspendable()) return next(); coin = bcoin.coin.fromTX(tx, i); self._getOrphans(key, function(err, orphans) { var some = false; if (err) return callback(err); if (!orphans) return finish(); // Add input to orphan utils.forEachSerial(orphans, function(orphan, next) { if (some) return next(); // Probably removed by some other means. if (!orphan.tx) return next(); orphan.tx.inputs[orphan.index].coin = coin; assert(orphan.tx.inputs[orphan.index].prevout.hash === hash); assert(orphan.tx.inputs[orphan.index].prevout.index === i); // Verify that input script is correct, if not - add // output to unspent and remove orphan from storage if (!self.options.verify) { some = true; return next(); } if (orphan.tx.verifyInput(orphan.index)) { some = true; return next(); } self.lazyRemove(orphan.tx, next, true); }, function(err) { if (err) return next(err); if (!some) orphans = null; self.db.del('o/' + key, finish); }); function finish(err) { if (err) return next(err); if (!orphans) { paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; batch.put('C/' + id + '/' + key, DUMMY); } coin = coin.toRaw(); batch.put('c/' + key, coin); self.coinCache.set(key, coin); updated = true; } next(); } }); }, function(err) { if (err) return callback(err); batch.write(function(err) { if (err) return callback(err); self.walletdb.handleTX(tx, map, function(err) { if (err) return callback(err); self.emit('tx', tx, map); if (updated) { if (tx.ts !== 0) self.emit('confirmed', tx, map); self.emit('updated', tx, map); } return callback(null, true, map); }); }); }); }); }, true); }; /** * Remove spenders that have not been confirmed. We do this in the * odd case of stuck transactions or when a coin is double-spent * by a newer transaction. All previously-spending transactions * of that coin that are _not_ confirmed will be removed from * the database. * @private * @param {Hash} hash * @param {TX} ref - Reference tx, the tx that double-spent. * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype._removeConflict = function _removeConflict(hash, ref, callback) { var self = this; this.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return callback(new Error('Could not find spender.')); if (tx.ts !== 0) { // If spender is confirmed and replacement // is not confirmed, do nothing. if (ref.ts === 0) return callback(); // If both are confirmed but replacement // is older than spender, do nothing. if (ref.ts < tx.ts) return callback(); } else { // If spender is unconfirmed and replacement // is confirmed, do nothing. if (ref.ts !== 0) return callback(); // If both are unconfirmed but replacement // is older than spender, do nothing. if (ref.ps < tx.ps) return callback(); } self._removeRecursive(tx, function(err, result, map) { if (err) return callback(err); return callback(null, tx, map); }); }); }; /** * Remove a transaction and recursively * remove all of its spenders. * @private * @param {TX} tx - Transaction to be removed. * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype._removeRecursive = function _removeRecursive(tx, callback) { var self = this; var hash = tx.hash('hex'); utils.forEachSerial(tx.outputs, function(output, next, i) { self.isSpent(hash, i, function(err, spent) { if (err) return next(err); // Remove all of the spender's spenders first. if (spent) { return self.getTX(spent, function(err, tx) { if (err) return callback(err); if (!tx) return callback(new Error('Could not find spender.')); return self._removeRecursive(tx, next); }); } next(); }); }, function(err) { if (err) return callback(err); // Remove the spender. return self.lazyRemove(tx, callback, true); }); }; /** * Test an entire transaction to see * if any of its outpoints are a double-spend. * @param {TX} tx * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) { var self = this; utils.everySerial(tx.inputs, function(input, next) { self.isSpent(input.prevout.hash, input.prevout.index, function(err, spent) { if (err) return next(err); return next(null, !spent); }); }, function(err, result) { if (err) return callback(err); return callback(null, !result); }); }; /** * Test a whether a coin has been spent. * @param {Hash} hash * @param {Number} index * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype.isSpent = function isSpent(hash, index, callback) { var key = 's/' + hash + '/' + index; return this.db.fetch(key, function(hash) { return hash.toString('hex'); }, callback); }; /** * Attempt to confirm a transaction. * @private * @param {TX} tx * @param {AddressMap} map * @param {Function} callback - Returns [Error, Boolean]. `false` if * the transaction should be added to the database, `true` if the * transaction was confirmed, or should be ignored. */ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { var self = this; var hash, batch, unlock, i, path, id; unlock = this._lock(_confirm, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); hash = tx.hash('hex'); this.getTX(hash, function(err, existing) { if (err) return callback(err); // Haven't seen this tx before, add it. if (!existing) return callback(null, false, map); // Existing tx is already confirmed. Ignore. if (existing.ts !== 0) return callback(null, true, map); // The incoming tx won't confirm the // existing one anyway. Ignore. if (tx.ts === 0) return callback(null, true, map); batch = self.db.batch(); // Tricky - update the tx and coin in storage, // and remove pending flag to mark as confirmed. assert(tx.height >= 0); assert(existing.ps > 0); batch.put('t/' + hash, tx.toExtended()); batch.del('p/' + hash); batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); batch.del('m/' + pad32(existing.ps) + '/' + hash); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); for (i = 0; i < map.accounts.length; i++) { path = map.accounts[i]; id = path.id + '/' + path.account; batch.del('P/' + id + '/' + hash); batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); } utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getHash('hex'); var key = hash + '/' + i; // Only update coins if this output is ours. if (!map.hasPaths(address)) return next(); self.getCoin(hash, i, function(err, coin) { if (err) return next(err); if (!coin) return next(); coin.height = tx.height; coin = coin.toRaw(); batch.put('c/' + key, coin); self.coinCache.set(key, coin); next(); }); }, function(err) { if (err) return callback(err); batch.write(function(err) { if (err) return callback(err); self.walletdb.syncOutputs(tx, map, function(err) { if (err) return callback(err); self.emit('confirmed', tx, map); self.emit('tx', tx, map); return callback(null, true, map); }); }); }); }); }; /** * Remove a transaction from the database. Disconnect inputs. * @param {Hash} hash * @param {Function} callback - Returns [Error]. */ TXDB.prototype.remove = function remove(hash, callback, force) { var self = this; if (hash.hash) hash = hash.hash('hex'); this.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return callback(null, true); assert(tx.hash('hex') === hash); return self.getMap(tx, function(err, map) { if (err) return callback(err); if (!map) return callback(null, false); return self._remove(tx, map, callback, force); }); }); }; /** * Remove a transaction from the database, but do not * look up the transaction. Use the passed-in transaction * to disconnect. * @param {TX} tx * @param {Function} callback - Returns [Error]. */ TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { var self = this; return this.getMap(tx, function(err, map) { if (err) return callback(err); if (!map) return callback(null, false); return self._remove(tx, map, callback, force); }); }; /** * Remove a transaction from the database. Disconnect inputs. * @private * @param {TX} tx * @param {AddressMap} map * @param {Function} callback - Returns [Error]. */ TXDB.prototype._remove = function remove(tx, map, callback, force) { var self = this; var unlock, hash, batch, i, j, path, id; var key, paths, address, input, output, coin; unlock = this._lock(remove, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); hash = tx.hash('hex'); batch = this.db.batch(); batch.del('t/' + hash); if (tx.ts === 0) { batch.del('p/' + hash); batch.del('m/' + pad32(tx.ps) + '/' + hash); } else { batch.del('h/' + pad32(tx.height) + '/' + hash); batch.del('m/' + pad32(tx.ts) + '/' + hash); } for (i = 0; i < map.accounts.length; i++) { path = map.accounts[i]; id = path.id + '/' + path.account; batch.del('T/' + id + '/' + hash); if (tx.ts === 0) { batch.del('P/' + id + '/' + hash); batch.del('M/' + id + '/' + pad32(tx.ps) + '/' + hash); } else { batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash); } } this.fillHistory(tx, function(err) { if (err) return callback(err); for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; key = input.prevout.hash + '/' + input.prevout.index; address = input.getHash('hex'); if (tx.isCoinbase()) break; if (!input.coin) continue; if (!map.hasPaths(address)) continue; paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; batch.put('C/' + id + '/' + key, DUMMY); } coin = input.coin.toRaw(); batch.put('c/' + key, coin); batch.del('s/' + key); batch.del('o/' + key); self.coinCache.set(key, coin); } for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; key = hash + '/' + i; address = output.getHash('hex'); if (!map.hasPaths(address)) continue; if (output.script.isUnspendable()) continue; paths = map.getPaths(address); for (j = 0; j < paths.length; j++) { path = paths[j]; id = path.id + '/' + path.account; batch.del('C/' + id + '/' + key); } batch.del('c/' + key); self.coinCache.remove(key); } batch.write(function(err) { if (err) return callback(err); self.emit('remove tx', tx, map); return callback(null, true, map); }); }); }; /** * Unconfirm a transaction. This is usually necessary after a reorg. * @param {Hash} hash * @param {Function} callback */ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { var self = this; if (hash.hash) hash = hash.hash('hex'); callback = utils.ensure(callback); this.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return callback(null, true); assert(tx.hash('hex') === hash); return self.getMap(tx, function(err, map) { if (err) return callback(err); if (!map) return callback(null, false); return self._unconfirm(tx, map, callback, force); }); }); }; /** * Unconfirm a transaction. This is usually necessary after a reorg. * @param {Hash} hash * @param {AddressMap} map * @param {Function} callback */ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { var self = this; var batch, unlock, hash, height, ts, i, path, id; unlock = this._lock(unconfirm, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); hash = tx.hash('hex'); height = tx.height; ts = tx.ts; batch = this.db.batch(); if (height !== -1) return callback(null, false, map); tx.height = -1; tx.ps = utils.now(); tx.ts = 0; tx.index = -1; tx.block = null; batch.put('t/' + hash, tx.toExtended()); batch.put('p/' + hash, DUMMY); batch.del('h/' + pad32(height) + '/' + hash); batch.del('m/' + pad32(ts) + '/' + hash); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); for (i = 0; i < map.accounts.length; i++) { path = map.accounts[i]; id = path.id + '/' + path.account; batch.put('P/' + id + '/' + hash, DUMMY); batch.del('H/' + id + '/' + pad32(height) + '/' + hash); batch.del('M/' + id + '/' + pad32(ts) + '/' + hash); batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); } utils.forEachSerial(tx.outputs, function(output, next, i) { var key = hash + '/' + i; self.getCoin(hash, i, function(err, coin) { if (err) return next(err); if (!coin) return next(); coin.height = tx.height; coin = coin.toRaw(); batch.put('c/' + key, coin); self.coinCache.set(key, coin); next(); }); }, function(err) { if (err) return callback(err); batch.write(function(err) { if (err) return callback(err); self.emit('unconfirmed', tx, map); return callback(null, true, map); }); }); }; /** * Get hashes of all transactions in the database. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getHistoryHashes = function getHistoryHashes(id, callback) { if (typeof id === 'function') { callback = id; id = null; } this.db.iterate({ gte: id ? 'T/' + id + '/' : 't', lte: id ? 'T/' + id + '/~' : 't~', transform: function(key) { key = key.split('/'); if (id) return key[3]; return key[1]; } }, callback); }; /** * Get hashes of all unconfirmed transactions in the database. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(id, callback) { if (typeof id === 'function') { callback = id; id = null; } this.db.iterate({ gte: id ? 'P/' + id + '/' : 'p', lte: id ? 'P/' + id + '/~' : 'p~', transform: function(key) { key = key.split('/'); if (id) return key[3]; return key[1]; } }, callback); }; /** * Get all coin hashes in the database. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getCoinHashes = function getCoinHashes(id, callback) { if (typeof id === 'function') { callback = id; id = null; } this.db.iterate({ gte: id ? 'C/' + id + '/' : 'c', lte: id ? 'C/' + id + '/~' : 'c~', transform: function(key) { key = key.split('/'); if (id) return [key[3], +key[4]]; return [key[1], +key[2]]; } }, callback); }; /** * Get TX hashes by height range. * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(id, options, callback) { if (typeof id !== 'string') { callback = options; options = id; id = null; } this.db.iterate({ gte: id ? 'H/' + id + '/' + pad32(options.start) + '/' : 'h/' + pad32(options.start) + '/', lte: id ? 'H/' + id + '/' + pad32(options.end) + '/~' : 'h/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); if (id) return key[4]; return key[2]; } }, callback); }; /** * Get TX hashes by height. * @param {Number} height * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { return this.getHeightRangeHashes({ start: height, end: height }, callback); }; /** * Get TX hashes by timestamp range. * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @param {Function} callback - Returns [Error, {@link Hash}[]]. */ TXDB.prototype.getRangeHashes = function getRangeHashes(id, options, callback) { if (typeof id === 'function') { callback = id; id = null; } this.db.iterate({ gte: id ? 'M/' + id + '/' + pad32(options.start) + '/' : 'm/' + pad32(options.start) + '/', lte: id ? 'M/' + id + '/' + pad32(options.end) + '/~' : 'm/' + pad32(options.end) + '/~', limit: options.limit, reverse: options.reverse, transform: function(key) { key = key.split('/'); if (id) return key[4]; return key[2]; } }, callback); }; /** * Get transactions by timestamp range. * @param {WalletID?} id * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @param {Function} callback - Returns [Error, {@link TX}[]]. */ TXDB.prototype.getRange = function getLast(id, options, callback) { var self = this; var txs = []; if (typeof id === 'function') { callback = id; id = null; } return this.getRangeHashes(id, options, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(hash, next) { self.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return next(); txs.push(tx); next(); }); }, function(err) { if (err) return callback(err); return callback(null, txs); }); }); }; /** * Get last N transactions. * @param {WalletID?} id * @param {Number} limit - Max number of transactions. * @param {Function} callback - Returns [Error, {@link TX}[]]. */ TXDB.prototype.getLast = function getLast(id, limit, callback) { if (typeof limit === 'function') { callback = limit; limit = id; id = null; } return this.getRange(id, { start: 0, end: 0xffffffff, reverse: true, limit: limit }, callback); }; /** * Get all transactions. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link TX}[]]. */ TXDB.prototype.getHistory = function getHistory(id, callback) { var self = this; var txs = []; if (typeof id === 'function') { callback = id; id = null; } return this.getHistoryHashes(id, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(hash, next) { self.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return next(); txs.push(tx); next(); }); }, function(err) { if (err) return callback(err); return callback(null, utils.sortTX(txs)); }); }); }; /** * Get last active timestamp and height. * @param {WalletID?} id * @param {Function} callback - Returns [Error, Number(ts), Number(height)]. */ TXDB.prototype.getLastTime = function getLastTime(id, callback) { var i, tx, lastTs, lastHeight; if (typeof id === 'function') { callback = id; id = null; } return this.getHistory(id, function(err, txs) { if (err) return callback(err); lastTs = 0; lastHeight = -1; for (i = 0; i < txs.length; i++) { tx = txs[i]; if (tx.ts > lastTs) lastTs = tx.ts; if (tx.height > lastHeight) lastHeight = tx.height; } return callback(null, lastTs, lastHeight); }); }; /** * Get unconfirmed transactions. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link TX}[]]. */ TXDB.prototype.getUnconfirmed = function getUnconfirmed(id, callback) { var self = this; var txs = []; if (typeof id === 'function') { callback = id; id = null; } return this.getUnconfirmedHashes(id, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(hash, next) { self.getTX(hash, function(err, tx) { if (err) return callback(err); if (!tx) return next(); txs.push(tx); next(); }); }, function(err) { if (err) return callback(err); return callback(null, txs); }); }); }; /** * Get coins. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Coin}[]]. */ TXDB.prototype.getCoins = function getCoins(id, callback) { var self = this; var coins = []; if (typeof id === 'function') { callback = id; id = null; } return this.getCoinHashes(id, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(key, next) { self.getCoin(key[0], key[1], function(err, coin) { if (err) return callback(err); if (!coin) return next(); coins.push(coin); next(); }); }, function(err) { if (err) return callback(err); return callback(null, coins); }); }); }; /** * Fill a transaction with coins (all historical coins). * @param {TX} tx * @param {Function} callback - Returns [Error, {@link TX}]. */ TXDB.prototype.fillHistory = function fillHistory(tx, callback) { var self = this; if (tx.isCoinbase()) { callback = utils.asyncify(callback); return callback(null, tx); } utils.forEachSerial(tx.inputs, function(input, next) { if (input.coin) return next(); self.getTX(input.prevout.hash, function(err, tx) { if (err) return next(err); if (tx) input.coin = bcoin.coin.fromTX(tx, input.prevout.index); next(); }); }, function(err) { if (err) return callback(err); return callback(null, tx); }); }; /** * Fill a transaction with coins. * @param {TX} tx * @param {Function} callback - Returns [Error, {@link TX}]. */ TXDB.prototype.fillCoins = function fillCoins(tx, callback) { var self = this; if (tx.isCoinbase()) { callback = utils.asyncify(callback); return callback(null, tx); } utils.forEachSerial(tx.inputs, function(input, next) { if (input.coin) return next(); self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { if (err) return callback(err); if (coin) input.coin = coin; next(); }); }, function(err) { if (err) return callback(err); return callback(null, tx); }); }; /** * Get transaction. * @param {Hash} hash * @param {Function} callback - Returns [Error, {@link TX}]. */ TXDB.prototype.getTX = function getTX(hash, callback) { this.db.fetch('t/' + hash, function(tx) { return bcoin.tx.fromExtended(tx); }, callback); }; /** * Test whether the database has a transaction. * @param {Hash} hash * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype.hasTX = function hasTX(hash, callback) { return this.db.has('t/' + hash, callback); }; /** * Get coin. * @param {Hash} hash * @param {Number} index * @param {Function} callback - Returns [Error, {@link Coin}]. */ TXDB.prototype.getCoin = function getCoin(hash, index, callback) { var self = this; var key = hash + '/' + index; var coin = this.coinCache.get(key); if (coin) { try { coin = bcoin.coin.fromRaw(coin); } catch (e) { return callback(e); } coin.hash = hash; coin.index = index; return callback(null, coin); } this.db.fetch('c/' + key, function(data) { var coin = bcoin.coin.fromRaw(data); coin.hash = hash; coin.index = index; self.coinCache.set(key, data); return coin; }, callback); }; /** * Test whether the database has a transaction. * @param {Hash} hash * @param {Function} callback - Returns [Error, Boolean]. */ TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) { var key = hash + '/' + index; if (this.coinCache.has(key)) return callback(null, true); return this.db.has('c/' + key, callback); }; /** * Calculate balance. * @param {WalletID?} id * @param {Function} callback - Returns [Error, {@link Balance}]. */ TXDB.prototype.getBalance = function getBalance(id, callback) { var self = this; var confirmed = 0; var unconfirmed = 0; var key, coin; if (typeof id === 'function') { callback = id; id = null; } function parse(data) { var height = data.readUInt32LE(4, true); var value = utils.read64N(data, 8); assert(data.length >= 16); if (height === 0x7fffffff) unconfirmed += value; else confirmed += value; } return this.getCoinHashes(id, function(err, hashes) { if (err) return callback(err); utils.forEachSerial(hashes, function(hash, next) { key = hash[0] + '/' + hash[1]; coin = self.coinCache.get(key); if (coin) { try { parse(coin); } catch (e) { return next(e); } return next(); } self.db.get('c/' + key, function(err, data) { if (err) return next(err); if (!data) return next(); try { parse(data); } catch (e) { return callback(e); } self.coinCache.set(key, data); next(); }); }, function(err) { if (err) return callback(err); return callback(null, { confirmed: confirmed, unconfirmed: unconfirmed, total: confirmed + unconfirmed }); }); }); }; /** * @param {WalletID?} id * @param {Number} age - Age delta (delete transactions older than `now - age`). * @param {Function} callback */ TXDB.prototype.zap = function zap(id, age, callback, force) { var self = this; var unlock; if (typeof age === 'function') { force = callback; callback = age; age = id; id = null; } unlock = this._lock(zap, [id, age, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); if (!utils.isNumber(age)) return callback(new Error('Age must be a number.')); return this.getRange(id, { start: 0, end: bcoin.now() - age }, function(err, txs) { if (err) return callback(err); self.fillHistory(txs, function(err) { if (err) return callback(err); utils.forEachSerial(txs, function(tx, next) { if (tx.ts !== 0) return next(); self.lazyRemove(tx, next, true); }, callback); }); }); }; /* * Address->Wallet Mapping */ // Each address can potentially map to multiple // accounts and wallets due to the fact that // multisig accounts can have shared addresses. // An address could map to 2 accounts on different // wallets, or 2 accounts on the same wallet! // In summary, bitcoin is hard. Use Bobchain instead. // // Table: // [address-hash] -> [array of Path objects] // '1edc6b6858fd12c64b26d8bd1e0e50d44b5bafb9': // [Path { // id: 'WLTZ3f5mMBsgWr1TcLzAdtLD8pkLcmWuBfPt', // name: 'default', // account: 0, // change: 0, // index: 0 // }] // /** * WalletMap * @constructor * @private * @property {WalletMember[]} inputs * @property {WalletMember[]} outputs * @property {WalletMember[]} accounts * @property {Object} table */ function WalletMap() { if (!(this instanceof WalletMap)) return new WalletMap(); this.inputs = []; this.outputs = []; this.accounts = []; this.table = {}; } /** * Inject properties from table and tx. * @private * @param {Object} table * @param {TX} tx */ WalletMap.prototype.fromTX = function fromTX(table, tx) { var i, members, input, output, key; // This is a scary function, but what it is // designed to do is uniqify inputs and // outputs by account. This is easier said // than done due to two facts: transactions // can have multiple outputs with the same // address, and wallets can have multiple // accounts with the same address. On top // of that, it will calculate the total // value sent to or received from each // account. function insert(vector, target) { var i, io, hash, members, member; var j, paths, path, key, address, hashes; // Keeps track of unique addresses. hashes = {}; // Maps address hashes to members. members = {}; for (i = 0; i < vector.length; i++) { io = vector[i]; address = io.getAddress(); if (!address) continue; hash = address.getHash('hex'); // Get all paths for this address. paths = table[hash]; for (j = 0; j < paths.length; j++) { path = paths[j]; key = path.toKey(); member = members[key]; // We no doubt already created a member // for this account, and not only that, // we're guaranteed to be on a different // input/output due to the fact that we // add the address hash after this loop // completes. Now we can update the value. if (hashes[hash]) { assert(member); if (io.coin) member.value += io.coin.value; else if (io.value) member.value += io.value; continue; } // Already have a member for this account. // i.e. Different address, but same account. if (member) { // Increment value. if (io.coin) member.value += io.coin.value; else if (io.value) member.value += io.value; // Set address and add path. path.address = address; member.paths.push(path); continue; } // Create a member for this account. assert(!member); member = MapMember.fromPath(path); // Set the _initial_ value. if (io.coin) member.value = io.coin.value; else if (io.value) member.value = io.value; // Add the address to the path object // and push onto the member's paths. // We only do this during instantiation, // since paths are just as unique as // addresses. path.address = address; member.paths.push(path); // Remember it by wallet id / account // name so we can update the value later. members[key] = member; // Push onto _our_ input/output array. target.push(member); } // Update this guy last so the above if // clause does not return true while // we're still iterating over paths. if (paths.length > 0) hashes[hash] = true; } } // Finally, we convert both inputs // and outputs to map members. insert(tx.inputs, this.inputs); insert(tx.outputs, this.outputs); // Combine both input and output map // members and uniqify them by account. members = {}; for (i = 0; i < this.inputs.length; i++) { input = this.inputs[i]; key = input.toKey(); if (!members[key]) { members[key] = true; this.accounts.push(input); } } for (i = 0; i < this.outputs.length; i++) { output = this.outputs[i]; key = output.toKey(); if (!members[key]) { members[key] = true; this.accounts.push(output); } } this.table = table; return this; }; /** * Instantiate wallet map from tx. * @param {Object} table * @param {TX} tx * @returns {WalletMap} */ WalletMap.fromTX = function fromTX(table, tx) { return new WalletMap().fromTX(table, tx); }; /** * Test whether the map has paths * for a given address hash. * @param {Hash} address * @returns {Boolean} */ WalletMap.prototype.hasPaths = function hasPaths(address) { var paths; if (!address) return false; paths = this.table[address]; return paths && paths.length !== 0; }; /** * Return a unique list of wallet IDs for the map. * @returns {WalletID[]} */ WalletMap.prototype.getWallets = function getWallets() { var ids = {}; var i, member; for (i = 0; i < this.accounts.length; i++) { member = this.accounts[i]; ids[member.id] = true; } return Object.keys(ids); }; /** * Return a unique list of wallet IDs for the map. * @returns {WalletID[]} */ WalletMap.prototype.getInputWallets = function getInputWallets() { var ids = {}; var i, member; for (i = 0; i < this.inputs.length; i++) { member = this.inputs[i]; ids[member.id] = true; } return Object.keys(ids); }; /** * Return a unique list of wallet IDs for the map. * @returns {WalletID[]} */ WalletMap.prototype.getOutputWallets = function getOutputWallets() { var ids = {}; var i, member; for (i = 0; i < this.outputs.length; i++) { member = this.outputs[i]; ids[member.id] = true; } return Object.keys(ids); }; /** * Get paths for a given address hash. * @param {Hash} address * @returns {Path[]|null} */ WalletMap.prototype.getPaths = function getPaths(address) { return this.table[address]; }; /** * Convert the map to a json-friendly object. * @returns {Object} */ WalletMap.prototype.toJSON = function toJSON() { return { inputs: this.inputs.map(function(input) { return input.toJSON(); }), outputs: this.outputs.map(function(output) { return output.toJSON(); }) }; }; /** * Inject properties from json object. * @private * @param {Object} */ WalletMap.prototype.fromJSON = function fromJSON(json) { var i, j, table, input, output, path; var hash, paths, hashes, accounts, values, key; table = {}; accounts = {}; for (i = 0; i < json.inputs.length; i++) { input = json.inputs[i]; input = MapMember.fromJSON(input); this.inputs.push(input); key = input.toKey(); if (!accounts[key]) { accounts[key] = true; this.accounts.push(input); } for (j = 0; j < input.paths.length; j++) { path = input.paths[j]; path.id = input.id; path.name = input.name; path.account = input.account; hash = path.address.getHash('hex'); if (!table[hash]) table[hash] = []; table[hash].push(path); } } for (i = 0; i < json.outputs.length; i++) { output = json.outputs[i]; output = MapMember.fromJSON(output); this.outputs.push(output); key = output.toKey(); if (!accounts[key]) { accounts[key] = true; this.accounts.push(output); } for (j = 0; j < output.paths.length; j++) { path = output.paths[j]; path.id = output.id; path.name = output.name; path.account = output.account; hash = path.address.getHash('hex'); if (!table[hash]) table[hash] = []; table[hash].push(path); } } // We need to rebuild to address->paths table. hashes = Object.keys(table); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; paths = table[hash]; values = []; accounts = {}; for (j = 0; j < paths.length; j++) { path = paths[j]; key = path.toKey(); if (!accounts[key]) { accounts[key] = true; values.push(path); } } table[hash] = values; } this.table = table; return this; }; /** * Instantiate map from json object. * @param {Object} * @returns {WalletMap} */ WalletMap.fromJSON = function fromJSON(json) { return new WalletMap().fromJSON(json); }; /** * MapMember * @constructor * @private * @property {WalletID} id * @property {String} name - Account name. * @property {Number} account - Account index. * @property {Path[]} paths * @property {Amount} value */ function MapMember() { if (!(this instanceof MapMember)) return new MapMember(); this.id = null; this.name = null; this.account = 0; this.paths = []; this.value = 0; } /** * Convert member to a key in the form of (id|account). * @returns {String} */ MapMember.prototype.toKey = function toKey() { return this.id + '/' + this.account; }; /** * Convert the member to a json-friendly object. * @returns {Object} */ MapMember.prototype.toJSON = function toJSON() { return { id: this.id, name: this.name, account: this.account, paths: this.paths.map(function(path) { return path.toCompact(); }), value: utils.btc(this.value) }; }; /** * Inject properties from json object. * @private * @param {Object} json */ MapMember.prototype.fromJSON = function fromJSON(json) { var i, path; this.id = json.id; this.name = json.name; this.account = json.account; for (i = 0; i < json.paths.length; i++) { path = json.paths[i]; this.paths.push(bcoin.path.fromCompact(path)); } this.value = utils.satoshi(json.value); return this; }; /** * Instantiate member from json object. * @param {Object} json * @returns {MapMember} */ MapMember.fromJSON = function fromJSON(json) { return new MapMember().fromJSON(json); }; /** * Inject properties from path. * @private * @param {Path} path */ MapMember.prototype.fromPath = function fromPath(path) { this.id = path.id; this.name = path.name; this.account = path.account; return this; }; /** * Instantiate member from path. * @param {Path} path * @returns {MapMember} */ MapMember.fromPath = function fromPath(path) { return new MapMember().fromPath(path); }; /* * Expose */ module.exports = TXDB;