fcoin/lib/wallet/txdb.js
2017-07-17 14:26:39 -07:00

3237 lines
68 KiB
JavaScript

/*!
* txdb.js - persistent transaction pool
* 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 util = require('../utils/util');
const LRU = require('../utils/lru');
const assert = require('assert');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const Amount = require('../btc/amount');
const CoinView = require('../coins/coinview');
const Coin = require('../primitives/coin');
const Outpoint = require('../primitives/outpoint');
const records = require('./records');
const layout = require('./layout').txdb;
const encoding = require('../utils/encoding');
const policy = require('../protocol/policy');
const Script = require('../script/script');
const BlockMapRecord = records.BlockMapRecord;
const OutpointMapRecord = records.OutpointMapRecord;
const TXRecord = records.TXRecord;
const DUMMY = Buffer.from([0]);
/**
* TXDB
* @alias module:wallet.TXDB
* @constructor
* @param {Wallet} wallet
*/
function TXDB(wallet) {
if (!(this instanceof TXDB))
return new TXDB(wallet);
this.wallet = wallet;
this.walletdb = wallet.db;
this.db = wallet.db.db;
this.logger = wallet.db.logger;
this.network = wallet.db.network;
this.options = wallet.db.options;
this.coinCache = new LRU(10000);
this.locked = new Set();
this.state = null;
this.pending = null;
this.events = [];
}
/**
* Database layout.
* @type {Object}
*/
TXDB.layout = layout;
/**
* Open TXDB.
* @returns {Promise}
*/
TXDB.prototype.open = async function open() {
let state = await this.getState();
if (state) {
this.state = state;
this.logger.info('TXDB loaded for %s.', this.wallet.id);
} else {
this.state = new TXDBState(this.wallet.wid, this.wallet.id);
this.logger.info('TXDB created for %s.', this.wallet.id);
}
this.logger.info('TXDB State: tx=%d coin=%s.',
this.state.tx, this.state.coin);
this.logger.info(
'Balance: unconfirmed=%s confirmed=%s.',
Amount.btc(this.state.unconfirmed),
Amount.btc(this.state.confirmed));
};
/**
* Start batch.
* @private
*/
TXDB.prototype.start = function start() {
this.pending = this.state.clone();
this.coinCache.start();
return this.wallet.start();
};
/**
* Drop batch.
* @private
*/
TXDB.prototype.drop = function drop() {
this.pending = null;
this.events.length = 0;
this.coinCache.drop();
return this.wallet.drop();
};
/**
* Clear batch.
* @private
*/
TXDB.prototype.clear = function clear() {
this.pending = this.state.clone();
this.events.length = 0;
this.coinCache.clear();
return this.wallet.clear();
};
/**
* Save batch.
* @returns {Promise}
*/
TXDB.prototype.commit = async function commit() {
try {
await this.wallet.commit();
} catch (e) {
this.pending = null;
this.events.length = 0;
this.coinCache.drop();
throw e;
}
// Overwrite the entire state
// with our new committed state.
if (this.pending.committed) {
this.state = this.pending;
// Emit buffered events now that
// we know everything is written.
for (let [event, data, details] of this.events) {
this.walletdb.emit(event, this.wallet.id, data, details);
this.wallet.emit(event, data, details);
}
}
this.pending = null;
this.events.length = 0;
this.coinCache.commit();
};
/**
* Emit transaction event.
* @private
* @param {String} event
* @param {Object} data
* @param {Details} details
*/
TXDB.prototype.emit = function emit(event, data, details) {
this.events.push([event, data, details]);
};
/**
* Prefix a key.
* @param {Buffer} key
* @returns {Buffer} Prefixed key.
*/
TXDB.prototype.prefix = function prefix(key) {
assert(this.wallet.wid);
return layout.prefix(this.wallet.wid, key);
};
/**
* Put key and value to current batch.
* @param {String} key
* @param {Buffer} value
*/
TXDB.prototype.put = function put(key, value) {
assert(this.wallet.current);
this.wallet.current.put(this.prefix(key), value);
};
/**
* Delete key from current batch.
* @param {String} key
*/
TXDB.prototype.del = function del(key) {
assert(this.wallet.current);
this.wallet.current.del(this.prefix(key));
};
/**
* Get.
* @param {String} key
*/
TXDB.prototype.get = function get(key) {
return this.db.get(this.prefix(key));
};
/**
* Has.
* @param {String} key
*/
TXDB.prototype.has = function has(key) {
return this.db.has(this.prefix(key));
};
/**
* Iterate.
* @param {Object} options
* @returns {Promise}
*/
TXDB.prototype.range = function range(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.range(options);
};
/**
* Iterate.
* @param {Object} options
* @returns {Promise}
*/
TXDB.prototype.keys = function keys(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.keys(options);
};
/**
* Iterate.
* @param {Object} options
* @returns {Promise}
*/
TXDB.prototype.values = function values(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.values(options);
};
/**
* Get wallet path for output.
* @param {Output} output
* @returns {Promise} - Returns {@link Path}.
*/
TXDB.prototype.getPath = function getPath(output) {
let addr = output.getAddress();
if (!addr)
return Promise.resolve();
return this.wallet.getPath(addr);
};
/**
* Test whether path exists for output.
* @param {Output} output
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.hasPath = function hasPath(output) {
let addr = output.getAddress();
if (!addr)
return Promise.resolve(false);
return this.wallet.hasPath(addr);
};
/**
* Save credit.
* @param {Credit} credit
* @param {Path} path
*/
TXDB.prototype.saveCredit = async function saveCredit(credit, path) {
let coin = credit.coin;
let key = coin.toKey();
let raw = credit.toRaw();
await this.addOutpointMap(coin.hash, coin.index);
this.put(layout.c(coin.hash, coin.index), raw);
this.put(layout.C(path.account, coin.hash, coin.index), DUMMY);
this.coinCache.push(key, raw);
};
/**
* Remove credit.
* @param {Credit} credit
* @param {Path} path
*/
TXDB.prototype.removeCredit = async function removeCredit(credit, path) {
let coin = credit.coin;
let key = coin.toKey();
await this.removeOutpointMap(coin.hash, coin.index);
this.del(layout.c(coin.hash, coin.index));
this.del(layout.C(path.account, coin.hash, coin.index));
this.coinCache.unpush(key);
};
/**
* Spend credit.
* @param {Credit} credit
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.spendCredit = function spendCredit(credit, tx, index) {
let prevout = tx.inputs[index].prevout;
let spender = Outpoint.fromTX(tx, index);
this.put(layout.s(prevout.hash, prevout.index), spender.toRaw());
this.put(layout.d(spender.hash, spender.index), credit.coin.toRaw());
};
/**
* Unspend credit.
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.unspendCredit = function unspendCredit(tx, index) {
let prevout = tx.inputs[index].prevout;
let spender = Outpoint.fromTX(tx, index);
this.del(layout.s(prevout.hash, prevout.index));
this.del(layout.d(spender.hash, spender.index));
};
/**
* Write input record.
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.writeInput = function writeInput(tx, index) {
let prevout = tx.inputs[index].prevout;
let spender = Outpoint.fromTX(tx, index);
this.put(layout.s(prevout.hash, prevout.index), spender.toRaw());
};
/**
* Remove input record.
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.removeInput = function removeInput(tx, index) {
let prevout = tx.inputs[index].prevout;
this.del(layout.s(prevout.hash, prevout.index));
};
/**
* Resolve orphan input.
* @param {TX} tx
* @param {Number} index
* @param {Number} height
* @param {Path} path
* @returns {Boolean}
*/
TXDB.prototype.resolveInput = async function resolveInput(tx, index, height, path, own) {
let hash = tx.hash('hex');
let spent = await this.getSpent(hash, index);
let stx, credit;
if (!spent)
return false;
// If we have an undo coin, we
// already knew about this input.
if (await this.hasSpentCoin(spent))
return false;
// Get the spending transaction so
// we can properly add the undo coin.
stx = await this.getTX(spent.hash);
assert(stx);
// Crete the credit and add the undo coin.
credit = Credit.fromTX(tx, index, height);
credit.own = own;
this.spendCredit(credit, stx.tx, spent.index);
// If the spender is unconfirmed, save
// the credit as well, and mark it as
// unspent in the mempool. This is the
// same behavior `insert` would have
// done for inputs. We're just doing
// it retroactively.
if (stx.height === -1) {
credit.spent = true;
await this.saveCredit(credit, path);
if (height !== -1)
this.pending.confirmed += credit.coin.value;
}
return true;
};
/**
* Test an entire transaction to see
* if any of its outpoints are a double-spend.
* @param {TX} tx
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.isDoubleSpend = async function isDoubleSpend(tx) {
for (let input of tx.inputs) {
let prevout = input.prevout;
let spent = await this.isSpent(prevout.hash, prevout.index);
if (spent)
return true;
}
return false;
};
/**
* Test an entire transaction to see
* if any of its outpoints are replace by fee.
* @param {TX} tx
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.isRBF = async function isRBF(tx) {
if (tx.isRBF())
return true;
for (let input of tx.inputs) {
let prevout = input.prevout;
if (await this.has(layout.r(prevout.hash)))
return true;
}
return false;
};
/**
* Test a whether a coin has been spent.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.getSpent = async function getSpent(hash, index) {
let data = await this.get(layout.s(hash, index));
if (!data)
return;
return Outpoint.fromRaw(data);
};
/**
* Test a whether a coin has been spent.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.isSpent = function isSpent(hash, index) {
return this.has(layout.s(hash, index));
};
/**
* Append to the global unspent record.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise}
*/
TXDB.prototype.addOutpointMap = async function addOutpointMap(hash, i) {
let map = await this.walletdb.getOutpointMap(hash, i);
if (!map)
map = new OutpointMapRecord(hash, i);
if (!map.add(this.wallet.wid))
return;
this.walletdb.writeOutpointMap(this.wallet, hash, i, map);
};
/**
* Remove from the global unspent record.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise}
*/
TXDB.prototype.removeOutpointMap = async function removeOutpointMap(hash, i) {
let map = await this.walletdb.getOutpointMap(hash, i);
if (!map)
return;
if (!map.remove(this.wallet.wid))
return;
if (map.wids.length === 0) {
this.walletdb.unwriteOutpointMap(this.wallet, hash, i);
return;
}
this.walletdb.writeOutpointMap(this.wallet, hash, i, map);
};
/**
* Append to the global block record.
* @param {Hash} hash
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.addBlockMap = async function addBlockMap(hash, height) {
let block = await this.walletdb.getBlockMap(height);
if (!block)
block = new BlockMapRecord(height);
if (!block.add(hash, this.wallet.wid))
return;
this.walletdb.writeBlockMap(this.wallet, height, block);
};
/**
* Remove from the global block record.
* @param {Hash} hash
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.removeBlockMap = async function removeBlockMap(hash, height) {
let block = await this.walletdb.getBlockMap(height);
if (!block)
return;
if (!block.remove(hash, this.wallet.wid))
return;
if (block.txs.length === 0) {
this.walletdb.unwriteBlockMap(this.wallet, height);
return;
}
this.walletdb.writeBlockMap(this.wallet, height, block);
};
/**
* List block records.
* @returns {Promise}
*/
TXDB.prototype.getBlocks = function getBlocks() {
return this.keys({
gte: layout.b(0),
lte: layout.b(0xffffffff),
parse: key => layout.bb(key)
});
};
/**
* Get block record.
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.getBlock = async function getBlock(height) {
let data = await this.get(layout.b(height));
if (!data)
return;
return BlockRecord.fromRaw(data);
};
/**
* Append to the global block record.
* @param {Hash} hash
* @param {BlockMeta} meta
* @returns {Promise}
*/
TXDB.prototype.addBlock = async function addBlock(hash, meta) {
let key = layout.b(meta.height);
let data = await this.get(key);
let block, size;
if (!data) {
block = BlockRecord.fromMeta(meta);
data = block.toRaw();
}
block = Buffer.allocUnsafe(data.length + 32);
data.copy(block, 0);
size = block.readUInt32LE(40, true);
block.writeUInt32LE(size + 1, 40, true);
hash.copy(block, data.length);
this.put(key, block);
};
/**
* Remove from the global block record.
* @param {Hash} hash
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.removeBlock = async function removeBlock(hash, height) {
let key = layout.b(height);
let data = await this.get(key);
let block, size;
if (!data)
return;
size = data.readUInt32LE(40, true);
assert(size > 0);
assert(data.slice(-32).equals(hash));
if (size === 1) {
this.del(key);
return;
}
block = data.slice(0, -32);
block.writeUInt32LE(size - 1, 40, true);
this.put(key, block);
};
/**
* Append to the global block record.
* @param {Hash} hash
* @param {BlockMeta} meta
* @returns {Promise}
*/
TXDB.prototype.addBlockSlow = async function addBlockSlow(hash, meta) {
let block = await this.getBlock(meta.height);
if (!block)
block = BlockRecord.fromMeta(meta);
if (!block.add(hash))
return;
this.put(layout.b(meta.height), block.toRaw());
};
/**
* Remove from the global block record.
* @param {Hash} hash
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.removeBlockSlow = async function removeBlockSlow(hash, height) {
let block = await this.getBlock(height);
if (!block)
return;
if (!block.remove(hash))
return;
if (block.hashes.length === 0) {
this.del(layout.b(height));
return;
}
this.put(layout.b(height), block.toRaw());
};
/**
* Add transaction, potentially runs
* `confirm()` and `removeConflicts()`.
* @param {TX} tx
* @param {BlockMeta} block
* @returns {Promise}
*/
TXDB.prototype.add = async function add(tx, block) {
let result;
this.start();
try {
result = await this._add(tx, block);
} catch (e) {
this.drop();
throw e;
}
await this.commit();
return result;
};
/**
* Add transaction without a batch.
* @private
* @param {TX} tx
* @returns {Promise}
*/
TXDB.prototype._add = async function add(tx, block) {
let hash = tx.hash('hex');
let existing = await this.getTX(hash);
let wtx;
assert(!tx.mutable, 'Cannot add mutable TX to wallet.');
if (existing) {
// Existing tx is already confirmed. Ignore.
if (existing.height !== -1)
return;
// The incoming tx won't confirm the
// existing one anyway. Ignore.
if (!block)
return;
// Confirm transaction.
return await this._confirm(existing, block);
}
wtx = TXRecord.fromTX(tx, block);
if (!block) {
// We ignore any unconfirmed txs
// that are replace-by-fee.
if (await this.isRBF(tx)) {
// We need to index every spender
// hash to detect "passive"
// replace-by-fee.
this.put(layout.r(hash), DUMMY);
return;
}
// Potentially remove double-spenders.
// Only remove if they're not confirmed.
if (!(await this.removeConflicts(tx, true)))
return;
} else {
// Potentially remove double-spenders.
await this.removeConflicts(tx, false);
// Delete the replace-by-fee record.
this.del(layout.r(hash));
}
// Finally we can do a regular insertion.
return await this.insert(wtx, block);
};
/**
* Insert transaction.
* @private
* @param {TXRecord} wtx
* @param {BlockMeta} block
* @returns {Promise}
*/
TXDB.prototype.insert = async function insert(wtx, block) {
let tx = wtx.tx;
let hash = wtx.hash;
let height = block ? block.height : -1;
let details = new Details(this, wtx, block);
let own = false;
let updated = false;
if (!tx.isCoinbase()) {
// We need to potentially spend some coins here.
for (let i = 0; i < tx.inputs.length; i++) {
let input = tx.inputs[i];
let prevout = input.prevout;
let credit = await this.getCredit(prevout.hash, prevout.index);
let coin, path;
if (!credit) {
// Maintain an stxo list for every
// spent input (even ones we don't
// recognize). This is used for
// detecting double-spends (as best
// we can), as well as resolving
// inputs we didn't know were ours
// at the time. This built-in error
// correction is not technically
// necessary assuming no messages
// are ever missed from the mempool,
// but shit happens.
this.writeInput(tx, i);
continue;
}
coin = credit.coin;
// Do some verification.
if (!block) {
if (!(await this.verifyInput(tx, i, coin))) {
this.clear();
return;
}
}
path = await this.getPath(coin);
assert(path);
// Build the tx details object
// as we go, for speed.
details.setInput(i, path, coin);
// Write an undo coin for the credit
// and add it to the stxo set.
this.spendCredit(credit, tx, i);
// Unconfirmed balance should always
// be updated as it reflects the on-chain
// balance _and_ mempool balance assuming
// everything in the mempool were to confirm.
this.pending.coin--;
this.pending.unconfirmed -= coin.value;
if (!block) {
// If the tx is not mined, we do not
// disconnect the coin, we simply mark
// a `spent` flag on the credit. This
// effectively prevents the mempool
// from altering our utxo state
// permanently. It also makes it
// possible to compare the on-chain
// state vs. the mempool state.
credit.spent = true;
await this.saveCredit(credit, path);
} else {
// If the tx is mined, we can safely
// remove the coin being spent. This
// coin will be indexed as an undo
// coin so it can be reconnected
// later during a reorg.
this.pending.confirmed -= coin.value;
await this.removeCredit(credit, path);
}
updated = true;
own = true;
}
}
// Potentially add coins to the utxo set.
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
let path = await this.getPath(output);
let credit;
if (!path)
continue;
details.setOutput(i, path);
// Attempt to resolve an input we
// did not know was ours at the time.
if (await this.resolveInput(tx, i, height, path, own)) {
updated = true;
continue;
}
credit = Credit.fromTX(tx, i, height);
credit.own = own;
this.pending.coin++;
this.pending.unconfirmed += output.value;
if (block)
this.pending.confirmed += output.value;
await this.saveCredit(credit, path);
updated = true;
}
// If this didn't update any coins,
// it's not our transaction.
if (!updated) {
// Clear the spent list inserts.
this.clear();
return;
}
// Save and index the transaction record.
this.put(layout.t(hash), wtx.toRaw());
this.put(layout.m(wtx.ps, hash), DUMMY);
if (!block)
this.put(layout.p(hash), DUMMY);
else
this.put(layout.h(height, hash), DUMMY);
// Do some secondary indexing for account-based
// queries. This saves us a lot of time for
// queries later.
for (let account of details.accounts) {
this.put(layout.T(account, hash), DUMMY);
this.put(layout.M(account, wtx.ps, hash), DUMMY);
if (!block)
this.put(layout.P(account, hash), DUMMY);
else
this.put(layout.H(account, height, hash), DUMMY);
}
// Update block records.
if (block) {
await this.addBlockMap(hash, height);
await this.addBlock(tx.hash(), block);
}
// Update the transaction counter and
// commit the new state. This state will
// only overwrite the best state once
// the batch has actually been written
// to disk.
this.pending.tx++;
this.put(layout.R, this.pending.commit());
// This transaction may unlock some
// coins now that we've seen it.
this.unlockTX(tx);
// Emit events for potential local and
// websocket listeners. Note that these
// will only be emitted if the batch is
// successfully written to disk.
this.emit('tx', tx, details);
this.emit('balance', this.pending.toBalance(), details);
return details;
};
/**
* Attempt to confirm a transaction.
* @private
* @param {TX} tx
* @param {BlockMeta} block
* @returns {Promise}
*/
TXDB.prototype.confirm = async function confirm(hash, block) {
let wtx = await this.getTX(hash);
let details;
if (!wtx)
return;
if (wtx.height !== -1)
throw new Error('TX is already confirmed.');
assert(block);
this.start();
try {
details = await this._confirm(wtx, block);
} catch (e) {
this.drop();
throw e;
}
await this.commit();
return details;
};
/**
* Attempt to confirm a transaction.
* @private
* @param {TXRecord} wtx
* @param {BlockMeta} block
* @returns {Promise}
*/
TXDB.prototype._confirm = async function confirm(wtx, block) {
let tx = wtx.tx;
let hash = wtx.hash;
let height = block.height;
let details = new Details(this, wtx, block);
wtx.setBlock(block);
if (!tx.isCoinbase()) {
let credits = await this.getSpentCredits(tx);
// Potentially spend coins. Now that the tx
// is mined, we can actually _remove_ coins
// from the utxo state.
for (let i = 0; i < tx.inputs.length; i++) {
let input = tx.inputs[i];
let prevout = input.prevout;
let credit = credits[i];
let coin, path;
// There may be new credits available
// that we haven't seen yet.
if (!credit) {
credit = await this.getCredit(prevout.hash, prevout.index);
if (!credit)
continue;
// Add a spend record and undo coin
// for the coin we now know is ours.
// We don't need to remove the coin
// since it was never added in the
// first place.
this.spendCredit(credit, tx, i);
this.pending.coin--;
this.pending.unconfirmed -= credit.coin.value;
}
coin = credit.coin;
assert(coin.height !== -1);
path = await this.getPath(coin);
assert(path);
details.setInput(i, path, coin);
// We can now safely remove the credit
// entirely, now that we know it's also
// been removed on-chain.
this.pending.confirmed -= coin.value;
await this.removeCredit(credit, path);
}
}
// Update credit heights, including undo coins.
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
let path = await this.getPath(output);
let credit, coin;
if (!path)
continue;
details.setOutput(i, path);
credit = await this.getCredit(hash, i);
assert(credit);
// Credits spent in the mempool add an
// undo coin for ease. If this credit is
// spent in the mempool, we need to
// update the undo coin's height.
if (credit.spent)
await this.updateSpentCoin(tx, i, height);
// Update coin height and confirmed
// balance. Save once again.
coin = credit.coin;
coin.height = height;
this.pending.confirmed += output.value;
await this.saveCredit(credit, path);
}
// Remove the RBF index if we have one.
this.del(layout.r(hash));
// Save the new serialized transaction as
// the block-related properties have been
// updated. Also reindex for height.
this.put(layout.t(hash), wtx.toRaw());
this.del(layout.p(hash));
this.put(layout.h(height, hash), DUMMY);
// Secondary indexing also needs to change.
for (let account of details.accounts) {
this.del(layout.P(account, hash));
this.put(layout.H(account, height, hash), DUMMY);
}
if (block) {
await this.addBlockMap(hash, height);
await this.addBlock(tx.hash(), block);
}
// Commit the new state. The balance has updated.
this.put(layout.R, this.pending.commit());
this.unlockTX(tx);
this.emit('confirmed', tx, details);
this.emit('balance', this.pending.toBalance(), details);
return details;
};
/**
* Recursively remove a transaction
* from the database.
* @param {Hash} hash
* @returns {Promise}
*/
TXDB.prototype.remove = async function remove(hash) {
let wtx = await this.getTX(hash);
if (!wtx)
return;
return await this.removeRecursive(wtx);
};
/**
* Remove a transaction from the
* database. Disconnect inputs.
* @private
* @param {TXRecord} wtx
* @returns {Promise}
*/
TXDB.prototype.erase = async function erase(wtx, block) {
let tx = wtx.tx;
let hash = wtx.hash;
let height = block ? block.height : -1;
let details = new Details(this, wtx, block);
if (!tx.isCoinbase()) {
// We need to undo every part of the
// state this transaction ever touched.
// Start by getting the undo coins.
let credits = await this.getSpentCredits(tx);
for (let i = 0; i < tx.inputs.length; i++) {
let credit = credits[i];
let coin, path;
if (!credit) {
// This input never had an undo
// coin, but remove it from the
// stxo set.
this.removeInput(tx, i);
continue;
}
coin = credit.coin;
path = await this.getPath(coin);
assert(path);
details.setInput(i, path, coin);
// Recalculate the balance, remove
// from stxo set, remove the undo
// coin, and resave the credit.
this.pending.coin++;
this.pending.unconfirmed += coin.value;
if (block)
this.pending.confirmed += coin.value;
this.unspendCredit(tx, i);
await this.saveCredit(credit, path);
}
}
// We need to remove all credits
// this transaction created.
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
let path = await this.getPath(output);
let credit;
if (!path)
continue;
details.setOutput(i, path);
credit = Credit.fromTX(tx, i, height);
this.pending.coin--;
this.pending.unconfirmed -= output.value;
if (block)
this.pending.confirmed -= output.value;
await this.removeCredit(credit, path);
}
// Remove the RBF index if we have one.
this.del(layout.r(hash));
// Remove the transaction data
// itself as well as unindex.
this.del(layout.t(hash));
this.del(layout.m(wtx.ps, hash));
if (!block)
this.del(layout.p(hash));
else
this.del(layout.h(height, hash));
// Remove all secondary indexing.
for (let account of details.accounts) {
this.del(layout.T(account, hash));
this.del(layout.M(account, wtx.ps, hash));
if (!block)
this.del(layout.P(account, hash));
else
this.del(layout.H(account, height, hash));
}
// Update block records.
if (block) {
await this.removeBlockMap(hash, height);
await this.removeBlockSlow(hash, height);
}
// Update the transaction counter
// and commit new state due to
// balance change.
this.pending.tx--;
this.put(layout.R, this.pending.commit());
this.emit('remove tx', tx, details);
this.emit('balance', this.pending.toBalance(), details);
return details;
};
/**
* Remove a transaction and recursively
* remove all of its spenders.
* @private
* @param {TXRecord} wtx
* @returns {Promise}
*/
TXDB.prototype.removeRecursive = async function removeRecursive(wtx) {
let tx = wtx.tx;
let hash = wtx.hash;
let details;
for (let i = 0; i < tx.outputs.length; i++) {
let spent = await this.getSpent(hash, i);
let stx;
if (!spent)
continue;
// Remove all of the spender's spenders first.
stx = await this.getTX(spent.hash);
assert(stx);
await this.removeRecursive(stx);
}
this.start();
// Remove the spender.
details = await this.erase(wtx, wtx.getBlock());
assert(details);
await this.commit();
return details;
};
/**
* Unconfirm a transaction. Necessary after a reorg.
* @param {Hash} hash
* @returns {Promise}
*/
TXDB.prototype.unconfirm = async function unconfirm(hash) {
let details;
this.start();
try {
details = await this._unconfirm(hash);
} catch (e) {
this.drop();
throw e;
}
await this.commit();
return details;
};
/**
* Unconfirm a transaction without a batch.
* @private
* @param {Hash} hash
* @returns {Promise}
*/
TXDB.prototype._unconfirm = async function unconfirm(hash) {
let wtx = await this.getTX(hash);
if (!wtx)
return;
if (wtx.height === -1)
return;
return await this.disconnect(wtx, wtx.getBlock());
};
/**
* Unconfirm a transaction. Necessary after a reorg.
* @param {TXRecord} wtx
* @returns {Promise}
*/
TXDB.prototype.disconnect = async function disconnect(wtx, block) {
let tx = wtx.tx;
let hash = wtx.hash;
let height = block.height;
let details = new Details(this, wtx, block);
assert(block);
wtx.unsetBlock();
if (!tx.isCoinbase()) {
// We need to reconnect the coins. Start
// by getting all of the undo coins we know
// about.
let credits = await this.getSpentCredits(tx);
for (let i = 0; i < tx.inputs.length; i++) {
let credit = credits[i];
let path, coin;
if (!credit)
continue;
coin = credit.coin;
assert(coin.height !== -1);
path = await this.getPath(coin);
assert(path);
details.setInput(i, path, coin);
this.pending.confirmed += coin.value;
// Resave the credit and mark it
// as spent in the mempool instead.
credit.spent = true;
await this.saveCredit(credit, path);
}
}
// We need to remove heights on
// the credits and undo coins.
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
let path = await this.getPath(output);
let credit, coin;
if (!path)
continue;
credit = await this.getCredit(hash, i);
// Potentially update undo coin height.
if (!credit) {
await this.updateSpentCoin(tx, i, height);
continue;
}
if (credit.spent)
await this.updateSpentCoin(tx, i, height);
details.setOutput(i, path);
// Update coin height and confirmed
// balance. Save once again.
coin = credit.coin;
coin.height = -1;
this.pending.confirmed -= output.value;
await this.saveCredit(credit, path);
}
await this.removeBlockMap(hash, height);
await this.removeBlock(tx.hash(), height);
// We need to update the now-removed
// block properties and reindex due
// to the height change.
this.put(layout.t(hash), wtx.toRaw());
this.put(layout.p(hash), DUMMY);
this.del(layout.h(height, hash));
// Secondary indexing also needs to change.
for (let account of details.accounts) {
this.put(layout.P(account, hash), DUMMY);
this.del(layout.H(account, height, hash));
}
// Commit state due to unconfirmed
// vs. confirmed balance change.
this.put(layout.R, this.pending.commit());
this.emit('unconfirmed', tx, details);
this.emit('balance', this.pending.toBalance(), details);
return details;
};
/**
* 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.
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.removeConflict = async function removeConflict(wtx) {
let tx = wtx.tx;
let details;
this.logger.warning('Handling conflicting tx: %s.', tx.txid());
this.drop();
details = await this.removeRecursive(wtx);
this.start();
this.logger.warning('Removed conflict: %s.', tx.txid());
// Emit the _removed_ transaction.
this.emit('conflict', tx, details);
return details;
};
/**
* Retrieve coins for own inputs, remove
* double spenders, and verify inputs.
* @private
* @param {TX} tx
* @returns {Promise}
*/
TXDB.prototype.removeConflicts = async function removeConflicts(tx, conf) {
let hash = tx.hash('hex');
let spends = [];
if (tx.isCoinbase())
return true;
// Gather all spent records first.
for (let i = 0; i < tx.inputs.length; i++) {
let input = tx.inputs[i];
let prevout = input.prevout;
let spent, spender, block;
// Is it already spent?
spent = await this.getSpent(prevout.hash, prevout.index);
if (!spent)
continue;
// Did _we_ spend it?
if (spent.hash === hash)
continue;
spender = await this.getTX(spent.hash);
assert(spender);
block = spender.getBlock();
if (conf && block)
return false;
spends[i] = spender;
}
// Once we know we're not going to
// screw things up, remove the double
// spenders.
for (let spender of spends) {
if (!spender)
continue;
// Remove the double spender.
await this.removeConflict(spender);
}
return true;
};
/**
* Attempt to verify an input.
* @private
* @param {TX} tx
* @param {Number} index
* @param {Coin} coin
* @returns {Promise}
*/
TXDB.prototype.verifyInput = async function verifyInput(tx, index, coin) {
let flags = Script.flags.MANDATORY_VERIFY_FLAGS;
if (!this.options.verify)
return true;
return await tx.verifyInputAsync(index, coin, flags);
};
/**
* Lock all coins in a transaction.
* @param {TX} tx
*/
TXDB.prototype.lockTX = function lockTX(tx) {
if (tx.isCoinbase())
return;
for (let input of tx.inputs)
this.lockCoin(input.prevout);
};
/**
* Unlock all coins in a transaction.
* @param {TX} tx
*/
TXDB.prototype.unlockTX = function unlockTX(tx) {
if (tx.isCoinbase())
return;
for (let input of tx.inputs)
this.unlockCoin(input.prevout);
};
/**
* Lock a single coin.
* @param {Coin|Outpoint} coin
*/
TXDB.prototype.lockCoin = function lockCoin(coin) {
let key = coin.toKey();
this.locked.add(key);
};
/**
* Unlock a single coin.
* @param {Coin|Outpoint} coin
*/
TXDB.prototype.unlockCoin = function unlockCoin(coin) {
let key = coin.toKey();
return this.locked.delete(key);
};
/**
* Test locked status of a single coin.
* @param {Coin|Outpoint} coin
*/
TXDB.prototype.isLocked = function isLocked(coin) {
let key = coin.toKey();
return this.locked.has(key);
};
/**
* Filter array of coins or outpoints
* for only unlocked ones.
* @param {Coin[]|Outpoint[]}
* @returns {Array}
*/
TXDB.prototype.filterLocked = function filterLocked(coins) {
let out = [];
for (let coin of coins) {
if (!this.isLocked(coin))
out.push(coin);
}
return out;
};
/**
* Return an array of all locked outpoints.
* @returns {Outpoint[]}
*/
TXDB.prototype.getLocked = function getLocked() {
let outpoints = [];
for (let key of this.locked.keys())
outpoints.push(Outpoint.fromKey(key));
return outpoints;
};
/**
* Get hashes of all transactions in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getAccountHistoryHashes = function getHistoryHashes(account) {
return this.keys({
gte: layout.T(account, encoding.NULL_HASH),
lte: layout.T(account, encoding.HIGH_HASH),
parse: (key) => {
let [, hash] = layout.Tt(key);
return hash;
}
});
};
/**
* Get hashes of all transactions in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) {
if (account != null)
return this.getAccountHistoryHashes(account);
return this.keys({
gte: layout.t(encoding.NULL_HASH),
lte: layout.t(encoding.HIGH_HASH),
parse: key => layout.tt(key)
});
};
/**
* Get hashes of all unconfirmed transactions in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(account) {
return this.keys({
gte: layout.P(account, encoding.NULL_HASH),
lte: layout.P(account, encoding.HIGH_HASH),
parse: (key) => {
let [, hash] = layout.Pp(key);
return hash;
}
});
};
/**
* Get hashes of all unconfirmed transactions in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getPendingHashes = function getPendingHashes(account) {
if (account != null)
return this.getAccountPendingHashes(account);
return this.keys({
gte: layout.p(encoding.NULL_HASH),
lte: layout.p(encoding.HIGH_HASH),
parse: key => layout.pp(key)
});
};
/**
* Get all coin hashes in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(account) {
return this.keys({
gte: layout.C(account, encoding.NULL_HASH, 0),
lte: layout.C(account, encoding.HIGH_HASH, 0xffffffff),
parse: (key) => {
let [, hash, index] = layout.Cc(key);
return new Outpoint(hash, index);
}
});
};
/**
* Get all coin hashes in the database.
* @param {Number?} account
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getOutpoints = function getOutpoints(account) {
if (account != null)
return this.getAccountOutpoints(account);
return this.keys({
gte: layout.c(encoding.NULL_HASH, 0),
lte: layout.c(encoding.HIGH_HASH, 0xffffffff),
parse: (key) => {
let [hash, index] = layout.cc(key);
return new Outpoint(hash, index);
}
});
};
/**
* Get TX hashes by height range.
* @param {Number?} account
* @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.
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(account, options) {
let start = options.start || 0;
let end = options.end || 0xffffffff;
return this.keys({
gte: layout.H(account, start, encoding.NULL_HASH),
lte: layout.H(account, end, encoding.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: (key) => {
let [,, hash] = layout.Hh(key);
return hash;
}
});
};
/**
* Get TX hashes by height range.
* @param {Number?} account
* @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.
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options) {
let start, end;
if (account && typeof account === 'object') {
options = account;
account = null;
}
if (account != null)
return this.getAccountHeightRangeHashes(account, options);
start = options.start || 0;
end = options.end || 0xffffffff;
return this.keys({
gte: layout.h(start, encoding.NULL_HASH),
lte: layout.h(end, encoding.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: (key) => {
let [, hash] = layout.hh(key);
return hash;
}
});
};
/**
* Get TX hashes by height.
* @param {Number} height
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getHeightHashes = function getHeightHashes(height) {
return this.getHeightRangeHashes({ start: height, end: height });
};
/**
* Get TX hashes by timestamp range.
* @param {Number?} account
* @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.
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(account, options) {
let start = options.start || 0;
let end = options.end || 0xffffffff;
return this.keys({
gte: layout.M(account, start, encoding.NULL_HASH),
lte: layout.M(account, end, encoding.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: (key) => {
let [,, hash] = layout.Mm(key);
return hash;
}
});
};
/**
* Get TX hashes by timestamp range.
* @param {Number?} account
* @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.
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) {
let start, end;
if (account && typeof account === 'object') {
options = account;
account = null;
}
if (account != null)
return this.getAccountRangeHashes(account, options);
start = options.start || 0;
end = options.end || 0xffffffff;
return this.keys({
gte: layout.m(start, encoding.NULL_HASH),
lte: layout.m(end, encoding.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: (key) => {
let [, hash] = layout.mm(key);
return hash;
}
});
};
/**
* Get transactions by timestamp range.
* @param {Number?} account
* @param {Object} options
* @param {Number} options.start - Start time.
* @param {Number} options.end - End time.
* @param {Number?} options.limit - Max number of records.
* @param {Boolean?} options.reverse - Reverse order.
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getRange = async function getRange(account, options) {
let txs = [];
let hashes;
if (account && typeof account === 'object') {
options = account;
account = null;
}
hashes = await this.getRangeHashes(account, options);
for (let hash of hashes) {
let tx = await this.getTX(hash);
assert(tx);
txs.push(tx);
}
return txs;
};
/**
* Get last N transactions.
* @param {Number?} account
* @param {Number} limit - Max number of transactions.
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getLast = function getLast(account, limit) {
return this.getRange(account, {
start: 0,
end: 0xffffffff,
reverse: true,
limit: limit || 10
});
};
/**
* Get all transactions.
* @param {Number?} account
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getHistory = function getHistory(account) {
// Slow case
if (account != null)
return this.getAccountHistory(account);
// Fast case
return this.values({
gte: layout.t(encoding.NULL_HASH),
lte: layout.t(encoding.HIGH_HASH),
parse: TXRecord.fromRaw
});
};
/**
* Get all account transactions.
* @param {Number?} account
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getAccountHistory = async function getAccountHistory(account) {
let hashes = await this.getHistoryHashes(account);
let txs = [];
for (let hash of hashes) {
let tx = await this.getTX(hash);
assert(tx);
txs.push(tx);
}
return txs;
};
/**
* Get unconfirmed transactions.
* @param {Number?} account
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getPending = async function getPending(account) {
let hashes = await this.getPendingHashes(account);
let txs = [];
for (let hash of hashes) {
let tx = await this.getTX(hash);
assert(tx);
txs.push(tx);
}
return txs;
};
/**
* Get coins.
* @param {Number?} account
* @returns {Promise} - Returns {@link Coin}[].
*/
TXDB.prototype.getCredits = function getCredits(account) {
// Slow case
if (account != null)
return this.getAccountCredits(account);
// Fast case
return this.range({
gte: layout.c(encoding.NULL_HASH, 0x00000000),
lte: layout.c(encoding.HIGH_HASH, 0xffffffff),
parse: (key, value) => {
let [hash, index] = layout.cc(key);
let credit = Credit.fromRaw(value);
let ckey = Outpoint.toKey(hash, index);
credit.coin.hash = hash;
credit.coin.index = index;
this.coinCache.set(ckey, value);
return credit;
}
});
};
/**
* Get coins by account.
* @param {Number} account
* @returns {Promise} - Returns {@link Coin}[].
*/
TXDB.prototype.getAccountCredits = async function getAccountCredits(account) {
let outpoints = await this.getOutpoints(account);
let credits = [];
for (let prevout of outpoints) {
let credit = await this.getCredit(prevout.hash, prevout.index);
assert(credit);
credits.push(credit);
}
return credits;
};
/**
* Fill a transaction with coins (all historical coins).
* @param {TX} tx
* @returns {Promise} - Returns {@link TX}.
*/
TXDB.prototype.getSpentCredits = async function getSpentCredits(tx) {
let credits = [];
let hash;
for (let i = 0; i < tx.inputs.length; i++)
credits.push(null);
if (tx.isCoinbase())
return credits;
hash = tx.hash('hex');
await this.range({
gte: layout.d(hash, 0x00000000),
lte: layout.d(hash, 0xffffffff),
parse: (key, value) => {
let [, index] = layout.dd(key);
let coin = Coin.fromRaw(value);
let input = tx.inputs[index];
assert(input);
coin.hash = input.prevout.hash;
coin.index = input.prevout.index;
credits[index] = new Credit(coin);
}
});
return credits;
};
/**
* Get coins.
* @param {Number?} account
* @returns {Promise} - Returns {@link Coin}[].
*/
TXDB.prototype.getCoins = async function getCoins(account) {
let credits = await this.getCredits(account);
let coins = [];
for (let credit of credits) {
if (credit.spent)
continue;
coins.push(credit.coin);
}
return coins;
};
/**
* Get coins by account.
* @param {Number} account
* @returns {Promise} - Returns {@link Coin}[].
*/
TXDB.prototype.getAccountCoins = async function getAccountCoins(account) {
let credits = await this.getAccountCredits(account);
let coins = [];
for (let credit of credits) {
if (credit.spent)
continue;
coins.push(credit.coin);
}
return coins;
};
/**
* Get historical coins for a transaction.
* @param {TX} tx
* @returns {Promise} - Returns {@link TX}.
*/
TXDB.prototype.getSpentCoins = async function getSpentCoins(tx) {
let coins = [];
let credits;
if (tx.isCoinbase())
return coins;
credits = await this.getSpentCredits(tx);
for (let credit of credits) {
if (!credit) {
coins.push(null);
continue;
}
coins.push(credit.coin);
}
return coins;
};
/**
* Get a coin viewpoint.
* @param {TX} tx
* @returns {Promise} - Returns {@link CoinView}.
*/
TXDB.prototype.getCoinView = async function getCoinView(tx) {
let view = new CoinView();
if (tx.isCoinbase())
return view;
for (let input of tx.inputs) {
let prevout = input.prevout;
let coin = await this.getCoin(prevout.hash, prevout.index);
if (!coin)
continue;
view.addCoin(coin);
}
return view;
};
/**
* Get historical coin viewpoint.
* @param {TX} tx
* @returns {Promise} - Returns {@link CoinView}.
*/
TXDB.prototype.getSpentView = async function getSpentView(tx) {
let view = new CoinView();
let coins;
if (tx.isCoinbase())
return view;
coins = await this.getSpentCoins(tx);
for (let coin of coins) {
if (!coin)
continue;
view.addCoin(coin);
}
return view;
};
/**
* Get TXDB state.
* @returns {Promise}
*/
TXDB.prototype.getState = async function getState() {
let data = await this.get(layout.R);
if (!data)
return;
return TXDBState.fromRaw(this.wallet.wid, this.wallet.id, data);
};
/**
* Get transaction.
* @param {Hash} hash
* @returns {Promise} - Returns {@link TX}.
*/
TXDB.prototype.getTX = async function getTX(hash) {
let raw = await this.get(layout.t(hash));
if (!raw)
return;
return TXRecord.fromRaw(raw);
};
/**
* Get transaction details.
* @param {Hash} hash
* @returns {Promise} - Returns {@link TXDetails}.
*/
TXDB.prototype.getDetails = async function getDetails(hash) {
let wtx = await this.getTX(hash);
if (!wtx)
return;
return await this.toDetails(wtx);
};
/**
* Convert transaction to transaction details.
* @param {TXRecord[]} wtxs
* @returns {Promise}
*/
TXDB.prototype.toDetails = async function toDetails(wtxs) {
let out = [];
if (!Array.isArray(wtxs))
return await this._toDetails(wtxs);
for (let wtx of wtxs) {
let details = await this._toDetails(wtx);
if (!details)
continue;
out.push(details);
}
return out;
};
/**
* Convert transaction to transaction details.
* @private
* @param {TXRecord} wtx
* @returns {Promise}
*/
TXDB.prototype._toDetails = async function _toDetails(wtx) {
let tx = wtx.tx;
let block = wtx.getBlock();
let details = new Details(this, wtx, block);
let coins = await this.getSpentCoins(tx);
for (let i = 0; i < tx.inputs.length; i++) {
let coin = coins[i];
let path = null;
if (coin)
path = await this.getPath(coin);
details.setInput(i, path, coin);
}
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
let path = await this.getPath(output);
details.setOutput(i, path);
}
return details;
};
/**
* Test whether the database has a transaction.
* @param {Hash} hash
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.hasTX = function hasTX(hash) {
return this.has(layout.t(hash));
};
/**
* Get coin.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise} - Returns {@link Coin}.
*/
TXDB.prototype.getCoin = async function getCoin(hash, index) {
let credit = await this.getCredit(hash, index);
if (!credit)
return;
return credit.coin;
};
/**
* Get coin.
* @param {Hash} hash
* @param {Number} index
* @returns {Promise} - Returns {@link Coin}.
*/
TXDB.prototype.getCredit = async function getCredit(hash, index) {
let state = this.state;
let key = Outpoint.toKey(hash, index);
let data = this.coinCache.get(key);
let credit;
if (data) {
credit = Credit.fromRaw(data);
credit.coin.hash = hash;
credit.coin.index = index;
return credit;
}
data = await this.get(layout.c(hash, index));
if (!data)
return;
credit = Credit.fromRaw(data);
credit.coin.hash = hash;
credit.coin.index = index;
if (state === this.state)
this.coinCache.set(key, data);
return credit;
};
/**
* Get spender coin.
* @param {Outpoint} spent
* @param {Outpoint} prevout
* @returns {Promise} - Returns {@link Coin}.
*/
TXDB.prototype.getSpentCoin = async function getSpentCoin(spent, prevout) {
let data = await this.get(layout.d(spent.hash, spent.index));
let coin;
if (!data)
return;
coin = Coin.fromRaw(data);
coin.hash = prevout.hash;
coin.index = prevout.index;
return coin;
};
/**
* Test whether the database has a spent coin.
* @param {Outpoint} spent
* @returns {Promise} - Returns {@link Coin}.
*/
TXDB.prototype.hasSpentCoin = function hasSpentCoin(spent) {
return this.has(layout.d(spent.hash, spent.index));
};
/**
* Update spent coin height in storage.
* @param {TX} tx - Sending transaction.
* @param {Number} index
* @param {Number} height
* @returns {Promise}
*/
TXDB.prototype.updateSpentCoin = async function updateSpentCoin(tx, index, height) {
let prevout = Outpoint.fromTX(tx, index);
let spent = await this.getSpent(prevout.hash, prevout.index);
let coin;
if (!spent)
return;
coin = await this.getSpentCoin(spent, prevout);
if (!coin)
return;
coin.height = height;
this.put(layout.d(spent.hash, spent.index), coin.toRaw());
};
/**
* Test whether the database has a transaction.
* @param {Hash} hash
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.hasCoin = function hasCoin(hash, index) {
let key = Outpoint.toKey(hash, index);
if (this.coinCache.has(key))
return Promise.resolve(true);
return this.has(layout.c(hash, index));
};
/**
* Calculate balance.
* @param {Number?} account
* @returns {Promise} - Returns {@link Balance}.
*/
TXDB.prototype.getBalance = async function getBalance(account) {
// Slow case
if (account != null)
return await this.getAccountBalance(account);
// Fast case
return this.state.toBalance();
};
/**
* Calculate balance.
* @param {Number?} account
* @returns {Promise} - Returns {@link Balance}.
*/
TXDB.prototype.getWalletBalance = async function getWalletBalance() {
let credits = await this.getCredits();
let balance = new Balance(this.wallet.wid, this.wallet.id, -1);
for (let credit of credits) {
let coin = credit.coin;
if (coin.height !== -1)
balance.confirmed += coin.value;
if (!credit.spent)
balance.unconfirmed += coin.value;
}
return balance;
};
/**
* Calculate balance by account.
* @param {Number} account
* @returns {Promise} - Returns {@link Balance}.
*/
TXDB.prototype.getAccountBalance = async function getAccountBalance(account) {
let credits = await this.getAccountCredits(account);
let balance = new Balance(this.wallet.wid, this.wallet.id, account);
for (let credit of credits) {
let coin = credit.coin;
if (coin.height !== -1)
balance.confirmed += coin.value;
if (!credit.spent)
balance.unconfirmed += coin.value;
}
return balance;
};
/**
* Zap pending transactions older than `age`.
* @param {Number?} account
* @param {Number} age - Age delta (delete transactions older than `now - age`).
* @returns {Promise}
*/
TXDB.prototype.zap = async function zap(account, age) {
let hashes = [];
let now = util.now();
let txs;
assert(util.isUInt32(age));
txs = await this.getRange(account, {
start: 0,
end: now - age
});
for (let wtx of txs) {
if (wtx.height !== -1)
continue;
assert(now - wtx.ps >= age);
this.logger.debug('Zapping TX: %s (%s)',
wtx.tx.txid(), this.wallet.id);
await this.remove(wtx.hash);
hashes.push(wtx.hash);
}
return hashes;
};
/**
* Abandon transaction.
* @param {Hash} hash
* @returns {Promise}
*/
TXDB.prototype.abandon = async function abandon(hash) {
let result = await this.has(layout.p(hash));
if (!result)
throw new Error('TX not eligible.');
return await this.remove(hash);
};
/**
* Balance
* @alias module:wallet.Balance
* @constructor
* @param {WalletID} wid
* @param {String} id
* @param {Number} account
*/
function Balance(wid, id, account) {
if (!(this instanceof Balance))
return new Balance(wid, id, account);
this.wid = wid;
this.id = id;
this.account = account;
this.unconfirmed = 0;
this.confirmed = 0;
}
/**
* Test whether a balance is equal.
* @param {Balance} balance
* @returns {Boolean}
*/
Balance.prototype.equal = function equal(balance) {
return this.wid === balance.wid
&& this.confirmed === balance.confirmed
&& this.unconfirmed === balance.unconfirmed;
};
/**
* Convert balance to a more json-friendly object.
* @param {Boolean?} minimal
* @returns {Object}
*/
Balance.prototype.toJSON = function toJSON(minimal) {
return {
wid: !minimal ? this.wid : undefined,
id: !minimal ? this.id : undefined,
account: !minimal ? this.account : undefined,
unconfirmed: Amount.btc(this.unconfirmed),
confirmed: Amount.btc(this.confirmed)
};
};
/**
* Convert balance to human-readable string.
* @returns {String}
*/
Balance.prototype.toString = function toString() {
return '<Balance'
+ ` unconfirmed=${Amount.btc(this.unconfirmed)}`
+ ` confirmed=${Amount.btc(this.confirmed)}`
+ '>';
};
/**
* Inspect balance.
* @param {String}
*/
Balance.prototype.inspect = function inspect() {
return this.toString();
};
/**
* Chain State
* @alias module:wallet.ChainState
* @constructor
* @param {WalletID} wid
* @param {String} id
*/
function TXDBState(wid, id) {
this.wid = wid;
this.id = id;
this.tx = 0;
this.coin = 0;
this.unconfirmed = 0;
this.confirmed = 0;
this.committed = false;
}
/**
* Clone the state.
* @returns {TXDBState}
*/
TXDBState.prototype.clone = function clone() {
let state = new TXDBState(this.wid, this.id);
state.tx = this.tx;
state.coin = this.coin;
state.unconfirmed = this.unconfirmed;
state.confirmed = this.confirmed;
return state;
};
/**
* Commit and serialize state.
* @returns {Buffer}
*/
TXDBState.prototype.commit = function commit() {
this.committed = true;
return this.toRaw();
};
/**
* Convert state to a balance object.
* @returns {Balance}
*/
TXDBState.prototype.toBalance = function toBalance() {
let balance = new Balance(this.wid, this.id, -1);
balance.unconfirmed = this.unconfirmed;
balance.confirmed = this.confirmed;
return balance;
};
/**
* Serialize state.
* @returns {Buffer}
*/
TXDBState.prototype.toRaw = function toRaw() {
let bw = new StaticWriter(32);
bw.writeU64(this.tx);
bw.writeU64(this.coin);
bw.writeU64(this.unconfirmed);
bw.writeU64(this.confirmed);
return bw.render();
};
/**
* Inject properties from serialized data.
* @private
* @param {Buffer} data
* @returns {TXDBState}
*/
TXDBState.prototype.fromRaw = function fromRaw(data) {
let br = new BufferReader(data);
this.tx = br.readU53();
this.coin = br.readU53();
this.unconfirmed = br.readU53();
this.confirmed = br.readU53();
return this;
};
/**
* Instantiate txdb state from serialized data.
* @param {Buffer} data
* @returns {TXDBState}
*/
TXDBState.fromRaw = function fromRaw(wid, id, data) {
return new TXDBState(wid, id).fromRaw(data);
};
/**
* Convert state to a more json-friendly object.
* @param {Boolean?} minimal
* @returns {Object}
*/
TXDBState.prototype.toJSON = function toJSON(minimal) {
return {
wid: !minimal ? this.wid : undefined,
id: !minimal ? this.id : undefined,
tx: this.tx,
coin: this.coin,
unconfirmed: Amount.btc(this.unconfirmed),
confirmed: Amount.btc(this.confirmed)
};
};
/**
* Inspect the state.
* @returns {Object}
*/
TXDBState.prototype.inspect = function inspect() {
return this.toJSON();
};
/**
* Credit (wrapped coin)
* @alias module:wallet.Credit
* @constructor
* @param {Coin} coin
* @param {Boolean?} spent
* @property {Coin} coin
* @property {Boolean} spent
*/
function Credit(coin, spent) {
if (!(this instanceof Credit))
return new Credit(coin, spent);
this.coin = coin || new Coin();
this.spent = spent || false;
this.own = false;
}
/**
* Inject properties from serialized data.
* @private
* @param {Buffer} data
*/
Credit.prototype.fromRaw = function fromRaw(data) {
let br = new BufferReader(data);
this.coin.fromReader(br);
this.spent = br.readU8() === 1;
this.own = true;
// Note: soft-fork
if (br.left() > 0)
this.own = br.readU8() === 1;
return this;
};
/**
* Instantiate credit from serialized data.
* @param {Buffer} data
* @returns {Credit}
*/
Credit.fromRaw = function fromRaw(data) {
return new Credit().fromRaw(data);
};
/**
* Get serialization size.
* @returns {Number}
*/
Credit.prototype.getSize = function getSize() {
return this.coin.getSize() + 2;
};
/**
* Serialize credit.
* @returns {Buffer}
*/
Credit.prototype.toRaw = function toRaw() {
let size = this.getSize();
let bw = new StaticWriter(size);
this.coin.toWriter(bw);
bw.writeU8(this.spent ? 1 : 0);
bw.writeU8(this.own ? 1 : 0);
return bw.render();
};
/**
* Inject properties from tx object.
* @private
* @param {TX} tx
* @param {Number} index
* @returns {Credit}
*/
Credit.prototype.fromTX = function fromTX(tx, index, height) {
this.coin.fromTX(tx, index, height);
this.spent = false;
this.own = false;
return this;
};
/**
* Instantiate credit from transaction.
* @param {TX} tx
* @param {Number} index
* @returns {Credit}
*/
Credit.fromTX = function fromTX(tx, index, height) {
return new Credit().fromTX(tx, index, height);
};
/**
* Transaction Details
* @alias module:wallet.Details
* @constructor
* @param {TXDB} txdb
* @param {TX} tx
*/
function Details(txdb, wtx, block) {
if (!(this instanceof Details))
return new Details(txdb, wtx, block);
this.wallet = txdb.wallet;
this.network = this.wallet.network;
this.wid = this.wallet.wid;
this.id = this.wallet.id;
this.chainHeight = txdb.walletdb.state.height;
this.hash = wtx.hash;
this.tx = wtx.tx;
this.ps = wtx.ps;
this.size = this.tx.getSize();
this.vsize = this.tx.getVirtualSize();
this.block = null;
this.height = -1;
this.ts = 0;
this.index = -1;
if (block) {
this.block = block.hash;
this.height = block.height;
this.ts = block.ts;
}
this.inputs = [];
this.outputs = [];
this.accounts = [];
this.init();
}
/**
* Initialize transaction details.
* @private
*/
Details.prototype.init = function init() {
for (let input of this.tx.inputs) {
let member = new DetailsMember();
member.address = input.getAddress();
this.inputs.push(member);
}
for (let output of this.tx.outputs) {
let member = new DetailsMember();
member.value = output.value;
member.address = output.getAddress();
this.outputs.push(member);
}
};
/**
* Add necessary info to input member.
* @param {Number} i
* @param {Path} path
* @param {Coin} coin
*/
Details.prototype.setInput = function setInput(i, path, coin) {
let member = this.inputs[i];
if (coin) {
member.value = coin.value;
member.address = coin.getAddress();
}
if (path) {
member.path = path;
util.binaryInsert(this.accounts, path.account, cmp, true);
}
};
/**
* Add necessary info to output member.
* @param {Number} i
* @param {Path} path
*/
Details.prototype.setOutput = function setOutput(i, path) {
let member = this.outputs[i];
if (path) {
member.path = path;
util.binaryInsert(this.accounts, path.account, cmp, true);
}
};
/**
* Calculate confirmations.
* @returns {Number}
*/
Details.prototype.getDepth = function getDepth() {
let depth;
if (this.height === -1)
return 0;
depth = this.chainHeight - this.height;
if (depth < 0)
return 0;
return depth + 1;
};
/**
* Calculate fee. Only works if wallet
* owns all inputs. Returns 0 otherwise.
* @returns {Amount}
*/
Details.prototype.getFee = function getFee() {
let inputValue = 0;
let outputValue = 0;
for (let input of this.inputs) {
if (!input.path)
return 0;
inputValue += input.value;
}
for (let output of this.outputs)
outputValue += output.value;
return inputValue - outputValue;
};
/**
* Calculate fee rate. Only works if wallet
* owns all inputs. Returns 0 otherwise.
* @param {Amount} fee
* @returns {Rate}
*/
Details.prototype.getRate = function getRate(fee) {
return policy.getRate(this.vsize, fee);
};
/**
* Convert details to a more json-friendly object.
* @returns {Object}
*/
Details.prototype.toJSON = function toJSON() {
let fee = this.getFee();
let rate = this.getRate(fee);
// Rate can exceed 53 bits in testing.
if (!util.isSafeInteger(rate))
rate = 0;
return {
wid: this.wid,
id: this.id,
hash: util.revHex(this.hash),
height: this.height,
block: this.block ? util.revHex(this.block) : null,
ts: this.ts,
ps: this.ps,
date: util.date(this.ts || this.ps),
index: this.index,
size: this.size,
virtualSize: this.vsize,
fee: Amount.btc(fee),
rate: Amount.btc(rate),
confirmations: this.getDepth(),
inputs: this.inputs.map((input) => {
return input.getJSON(this.network);
}),
outputs: this.outputs.map((output) => {
return output.getJSON(this.network);
}),
tx: this.tx.toRaw().toString('hex')
};
};
/**
* Transaction Details Member
* @alias module:wallet.DetailsMember
* @constructor
* @property {Number} value
* @property {Address} address
* @property {Path} path
*/
function DetailsMember() {
if (!(this instanceof DetailsMember))
return new DetailsMember();
this.value = 0;
this.address = null;
this.path = null;
}
/**
* Convert the member to a more json-friendly object.
* @returns {Object}
*/
DetailsMember.prototype.toJSON = function toJSON() {
return this.getJSON();
};
/**
* Convert the member to a more json-friendly object.
* @param {Network} network
* @returns {Object}
*/
DetailsMember.prototype.getJSON = function getJSON(network) {
return {
value: Amount.btc(this.value),
address: this.address
? this.address.toString(network)
: null,
path: this.path
? this.path.toJSON()
: null
};
};
/**
* Block Record
* @alias module:wallet.BlockRecord
* @constructor
* @param {Hash} hash
* @param {Number} height
* @param {Number} ts
*/
function BlockRecord(hash, height, ts) {
if (!(this instanceof BlockRecord))
return new BlockRecord(hash, height, ts);
this.hash = hash || encoding.NULL_HASH;
this.height = height != null ? height : -1;
this.ts = ts || 0;
this.hashes = [];
this.index = {};
}
/**
* Add transaction to block record.
* @param {Hash} hash
* @returns {Boolean}
*/
BlockRecord.prototype.add = function add(hash) {
if (this.index[hash])
return false;
this.index[hash] = true;
this.hashes.push(hash);
return true;
};
/**
* Remove transaction from block record.
* @param {Hash} hash
* @returns {Boolean}
*/
BlockRecord.prototype.remove = function remove(hash) {
let index;
if (!this.index[hash])
return false;
delete this.index[hash];
// Fast case
if (this.hashes[this.hashes.length - 1] === hash) {
this.hashes.pop();
return true;
}
index = this.hashes.indexOf(hash);
assert(index !== -1);
this.hashes.splice(index, 1);
return true;
};
/**
* Instantiate wallet block from serialized tip data.
* @private
* @param {Buffer} data
*/
BlockRecord.prototype.fromRaw = function fromRaw(data) {
let br = new BufferReader(data);
let count;
this.hash = br.readHash('hex');
this.height = br.readU32();
this.ts = br.readU32();
count = br.readU32();
for (let i = 0; i < count; i++) {
let hash = br.readHash('hex');
this.index[hash] = true;
this.hashes.push(hash);
}
return this;
};
/**
* Instantiate wallet block from serialized data.
* @param {Buffer} data
* @returns {BlockRecord}
*/
BlockRecord.fromRaw = function fromRaw(data) {
return new BlockRecord().fromRaw(data);
};
/**
* Get serialization size.
* @returns {Number}
*/
BlockRecord.prototype.getSize = function getSize() {
return 44 + this.hashes.length * 32;
};
/**
* Serialize the wallet block as a tip (hash and height).
* @returns {Buffer}
*/
BlockRecord.prototype.toRaw = function toRaw() {
let size = this.getSize();
let bw = new StaticWriter(size);
bw.writeHash(this.hash);
bw.writeU32(this.height);
bw.writeU32(this.ts);
bw.writeU32(this.hashes.length);
for (let hash of this.hashes)
bw.writeHash(hash);
return bw.render();
};
/**
* Convert the block to a more json-friendly object.
* @returns {Object}
*/
BlockRecord.prototype.toJSON = function toJSON() {
return {
hash: util.revHex(this.hash),
height: this.height,
ts: this.ts,
hashes: this.hashes.map(util.revHex)
};
};
/**
* Instantiate wallet block from block meta.
* @private
* @param {BlockMeta} block
*/
BlockRecord.prototype.fromMeta = function fromMeta(block) {
this.hash = block.hash;
this.height = block.height;
this.ts = block.ts;
return this;
};
/**
* Instantiate wallet block from block meta.
* @param {BlockMeta} block
* @returns {BlockRecord}
*/
BlockRecord.fromMeta = function fromMeta(block) {
return new BlockRecord().fromMeta(block);
};
/*
* Helpers
*/
function cmp(a, b) {
return a - b;
}
/*
* Expose
*/
module.exports = TXDB;