2406 lines
53 KiB
JavaScript
2406 lines
53 KiB
JavaScript
/*!
|
|
* 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';
|
|
|
|
var bcoin = require('./env');
|
|
var utils = require('./utils');
|
|
var assert = bcoin.utils.assert;
|
|
var constants = bcoin.protocol.constants;
|
|
var DUMMY = new Buffer([0]);
|
|
var pad32 = utils.pad32;
|
|
var BufferReader = require('./reader');
|
|
var BufferWriter = require('./writer');
|
|
|
|
/*
|
|
* Database Layout:
|
|
* t/[hash] -> extended tx
|
|
* c/[hash]/[index] -> coin
|
|
* d/[hash]/[index] -> undo 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/[account]/[hash] -> dummy (tx by account)
|
|
* P/[account]/[hash] -> dummy (pending tx by account)
|
|
* M/[account]/[time]/[hash] -> dummy (tx by time + account)
|
|
* H/[account]/[height]/[hash] -> dummy (tx by height + account)
|
|
* C/[account]/[hash]/[index] -> dummy (coin by account)
|
|
*/
|
|
|
|
function Layout(wallet) {
|
|
this.wallet = wallet;
|
|
}
|
|
|
|
/* String Keys
|
|
Layout.prototype.prefix = function prefix(key) {
|
|
assert(this.wallet.wid);
|
|
return 't' + pad32(this.wallet.wid) + key;
|
|
};
|
|
|
|
Layout.prototype.hi = function hi(ch, hash, index) {
|
|
return this.prefix(ch + hash + pad32(index));
|
|
};
|
|
|
|
Layout.prototype.hii = function hii(key) {
|
|
key = key.slice(12);
|
|
return [key.slice(0, 64), +key.slice(64)];
|
|
};
|
|
|
|
Layout.prototype.ih = function ih(ch, index, hash) {
|
|
return this.prefix(ch + pad32(index) + hash);
|
|
};
|
|
|
|
Layout.prototype.ihh = function ihh(key) {
|
|
key = key.slice(12);
|
|
return [+key.slice(0, 10), key.slice(10)];
|
|
};
|
|
|
|
Layout.prototype.iih = function iih(ch, index, num, hash) {
|
|
return this.prefix(ch + pad32(index) + pad32(num) + hash);
|
|
};
|
|
|
|
Layout.prototype.iihh = function iihh(key) {
|
|
key = key.slice(12);
|
|
return [+key.slice(0, 10), +key.slice(10, 20), key.slice(20)];
|
|
};
|
|
|
|
Layout.prototype.ihi = function ihi(ch, index, hash, num) {
|
|
return this.prefix(ch + pad32(index) + hash + pad32(num));
|
|
};
|
|
|
|
Layout.prototype.ihii = function ihii(key) {
|
|
key = key.slice(12);
|
|
return [+key.slice(0, 10), key.slice(10, 74), +key.slice(74)];
|
|
};
|
|
|
|
Layout.prototype.ha = function ha(ch, hash) {
|
|
return this.prefix(ch + hash);
|
|
};
|
|
|
|
Layout.prototype.haa = function haa(key) {
|
|
key = key.slice(12);
|
|
return key;
|
|
};
|
|
*/
|
|
|
|
Layout.prototype.prefix = function prefix(key) {
|
|
var out = new Buffer(5 + key.length);
|
|
assert(this.wallet.wid);
|
|
out[0] = 0x74;
|
|
out.writeUInt32BE(this.wallet.wid, 1);
|
|
key.copy(out, 5);
|
|
return out;
|
|
};
|
|
|
|
Layout.prototype.hi = function hi(ch, hash, index) {
|
|
var key = new Buffer(37);
|
|
key[0] = ch.charCodeAt(0);
|
|
key.write(hash, 1, 'hex');
|
|
key.writeUInt32BE(index, 33, true);
|
|
return this.prefix(key);
|
|
};
|
|
|
|
Layout.prototype.hii = function hii(key) {
|
|
key = key.slice(6);
|
|
return [key.toString('hex', 0, 32), key.readUInt32BE(32, true)];
|
|
};
|
|
|
|
Layout.prototype.ih = function ih(ch, index, hash) {
|
|
var key = new Buffer(37);
|
|
key[0] = ch.charCodeAt(0);
|
|
key.writeUInt32BE(index, 1, true);
|
|
key.write(hash, 5, 'hex');
|
|
return this.prefix(key);
|
|
};
|
|
|
|
Layout.prototype.ihh = function ihh(key) {
|
|
key = key.slice(6);
|
|
return [key.readUInt32BE(0, true), key.toString('hex', 4, 36)];
|
|
};
|
|
|
|
Layout.prototype.iih = function iih(ch, index, num, hash) {
|
|
var key = new Buffer(41);
|
|
key[0] = ch.charCodeAt(0);
|
|
key.writeUInt32BE(index, 1, true);
|
|
key.writeUInt32BE(num, 5, true);
|
|
key.write(hash, 9, 'hex');
|
|
return this.prefix(key);
|
|
};
|
|
|
|
Layout.prototype.iihh = function iihh(key) {
|
|
key = key.slice(6);
|
|
return [
|
|
key.readUInt32BE(0, true),
|
|
key.readUInt32BE(4, true),
|
|
key.toString('hex', 8, 40)
|
|
];
|
|
};
|
|
|
|
Layout.prototype.ihi = function ihi(ch, index, hash, num) {
|
|
var key = new Buffer(41);
|
|
key[0] = ch.charCodeAt(0);
|
|
key.writeUInt32BE(index, 1, true);
|
|
key.write(hash, 5, 'hex');
|
|
key.writeUInt32BE(num, 37, true);
|
|
return this.prefix(key);
|
|
};
|
|
|
|
Layout.prototype.ihii = function ihii(key) {
|
|
key = key.slice(6);
|
|
return [
|
|
key.readUInt32BE(0, true),
|
|
key.toString('hex', 4, 36),
|
|
key.readUInt32BE(36, true)
|
|
];
|
|
};
|
|
|
|
Layout.prototype.ha = function ha(ch, hash) {
|
|
var key = new Buffer(33);
|
|
key[0] = ch.charCodeAt(0);
|
|
key.write(hash, 1, 'hex');
|
|
return this.prefix(key);
|
|
};
|
|
|
|
Layout.prototype.haa = function haa(key) {
|
|
return key.toString('hex', 1);
|
|
};
|
|
|
|
Layout.prototype.t = function t(hash) {
|
|
// 0x74
|
|
return this.ha('t', hash);
|
|
};
|
|
|
|
Layout.prototype.tt = function tt(key) {
|
|
return this.haa(key);
|
|
};
|
|
|
|
Layout.prototype.c = function c(hash, index) {
|
|
// 0x63
|
|
return this.hi('c', hash, index);
|
|
};
|
|
|
|
Layout.prototype.cc = function cc(key) {
|
|
return this.hii(key);
|
|
};
|
|
|
|
Layout.prototype.d = function d(hash, index) {
|
|
// 0x64
|
|
return this.hi('d', hash, index);
|
|
};
|
|
|
|
Layout.prototype.dd = function dd(key) {
|
|
return this.hii(key);
|
|
};
|
|
|
|
Layout.prototype.s = function s(hash, index) {
|
|
// 0x73
|
|
return this.hi('s', hash, index);
|
|
};
|
|
|
|
Layout.prototype.ss = function ss(key) {
|
|
return this.hii(key);
|
|
};
|
|
|
|
Layout.prototype.o = function o(hash, index) {
|
|
// 0x6f
|
|
return this.hi('o', hash, index);
|
|
};
|
|
|
|
Layout.prototype.oo = function oo(key) {
|
|
return this.hii(key);
|
|
};
|
|
|
|
Layout.prototype.p = function p(hash) {
|
|
// 0x70
|
|
return this.ha('p', hash);
|
|
};
|
|
|
|
Layout.prototype.pp = function pp(key) {
|
|
return this.haa(key);
|
|
};
|
|
|
|
Layout.prototype.m = function m(time, hash) {
|
|
// 0x6d
|
|
return this.ih('m', time, hash);
|
|
};
|
|
|
|
Layout.prototype.mm = function mm(key) {
|
|
return this.ihh(key);
|
|
};
|
|
|
|
Layout.prototype.h = function h(height, hash) {
|
|
// 0x68
|
|
return this.ih('h', height, hash);
|
|
};
|
|
|
|
Layout.prototype.hh = function hh(key) {
|
|
return this.ihh(key);
|
|
};
|
|
|
|
Layout.prototype.T = function T(account, hash) {
|
|
// 0x54
|
|
return this.ih('T', account, hash);
|
|
};
|
|
|
|
Layout.prototype.Tt = function Tt(key) {
|
|
return this.ihh(key);
|
|
};
|
|
|
|
Layout.prototype.P = function P(account, hash) {
|
|
// 0x50
|
|
return this.ih('P', account, hash);
|
|
};
|
|
|
|
Layout.prototype.Pp = function Pp(key) {
|
|
return this.ihh(key);
|
|
};
|
|
|
|
Layout.prototype.M = function M(account, time, hash) {
|
|
// 0x4d
|
|
return this.iih('M', account, time, hash);
|
|
};
|
|
|
|
Layout.prototype.Mm = function Mm(key) {
|
|
return this.iihh(key);
|
|
};
|
|
|
|
Layout.prototype.H = function H(account, height, hash) {
|
|
// 0x48
|
|
return this.iih('H', account, height, hash);
|
|
};
|
|
|
|
Layout.prototype.Hh = function Hh(key) {
|
|
return this.iihh(key);
|
|
};
|
|
|
|
Layout.prototype.C = function C(account, hash, index) {
|
|
// 0x43
|
|
return this.ihi('C', account, hash, index);
|
|
};
|
|
|
|
Layout.prototype.Cc = function Cc(key) {
|
|
return this.ihii(key);
|
|
};
|
|
|
|
/**
|
|
* TXDB
|
|
* @exports 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.key = new Layout(wallet);
|
|
|
|
this.locker = new bcoin.locker(this);
|
|
this.coinCache = new bcoin.lru(10000, 1);
|
|
|
|
this.current = null;
|
|
this.balance = null;
|
|
}
|
|
|
|
/**
|
|
* Open TXDB.
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.open = function open(callback) {
|
|
var self = this;
|
|
|
|
this.getBalance(function(err, balance) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.logger.info('TXDB loaded for %s.', self.wallet.id);
|
|
self.logger.info(
|
|
'Balance: unconfirmed=%s confirmed=%s total=%s.',
|
|
utils.btc(balance.unconfirmed),
|
|
utils.btc(balance.confirmed),
|
|
utils.btc(balance.total));
|
|
|
|
self.balance = balance;
|
|
|
|
return callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Emit transaction event.
|
|
* @private
|
|
* @param {String} event
|
|
* @param {TX} tx
|
|
* @param {PathInfo} info
|
|
*/
|
|
|
|
TXDB.prototype.emit = function emit(event, tx, info) {
|
|
this.walletdb.emit(event, info.id, tx, info);
|
|
this.wallet.emit(event, tx, info);
|
|
};
|
|
|
|
/**
|
|
* Invoke the mutex lock.
|
|
* @private
|
|
* @returns {Function} unlock
|
|
*/
|
|
|
|
TXDB.prototype._lock = function _lock(func, args, force) {
|
|
return this.locker.lock(func, args, force);
|
|
};
|
|
|
|
/**
|
|
* Start a batch.
|
|
* @returns {Batch}
|
|
*/
|
|
|
|
TXDB.prototype.start = function start() {
|
|
assert(!this.current);
|
|
this.current = this.db.batch();
|
|
return this.current;
|
|
};
|
|
|
|
/**
|
|
* Put key and value to current batch.
|
|
* @param {String} key
|
|
* @param {Buffer} value
|
|
*/
|
|
|
|
TXDB.prototype.put = function put(key, value) {
|
|
assert(this.current);
|
|
this.current.put(key, value);
|
|
};
|
|
|
|
/**
|
|
* Delete key from current batch.
|
|
* @param {String} key
|
|
*/
|
|
|
|
TXDB.prototype.del = function del(key) {
|
|
assert(this.current);
|
|
this.current.del(key);
|
|
};
|
|
|
|
/**
|
|
* Get current batch.
|
|
* @returns {Batch}
|
|
*/
|
|
|
|
TXDB.prototype.batch = function batch() {
|
|
assert(this.current);
|
|
return this.current;
|
|
};
|
|
|
|
/**
|
|
* Drop current batch.
|
|
* @returns {Batch}
|
|
*/
|
|
|
|
TXDB.prototype.drop = function drop() {
|
|
assert(this.current);
|
|
this.current.clear();
|
|
this.current = null;
|
|
};
|
|
|
|
/**
|
|
* Fetch.
|
|
* @param {String} key
|
|
*/
|
|
|
|
TXDB.prototype.fetch = function fetch(key, parse, callback) {
|
|
this.db.fetch(key, parse, callback);
|
|
};
|
|
|
|
/**
|
|
* Get.
|
|
* @param {String} key
|
|
*/
|
|
|
|
TXDB.prototype.get = function get(key, callback) {
|
|
this.db.get(key, callback);
|
|
};
|
|
|
|
/**
|
|
* Has.
|
|
* @param {String} key
|
|
*/
|
|
|
|
TXDB.prototype.has = function has(key, callback) {
|
|
this.db.has(key, callback);
|
|
};
|
|
|
|
/**
|
|
* Iterate.
|
|
* @param {Object} options
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.iterate = function iterate(options, callback) {
|
|
this.db.iterate(options, callback);
|
|
};
|
|
|
|
/**
|
|
* Commit current batch.
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.commit = function commit(callback) {
|
|
var self = this;
|
|
assert(this.current);
|
|
this.current.write(function(err) {
|
|
if (err) {
|
|
self.current = null;
|
|
return callback(err);
|
|
}
|
|
self.current = null;
|
|
return callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Map a transactions' addresses to wallet IDs.
|
|
* @param {TX} tx
|
|
* @param {Function} callback - Returns [Error, {@link PathInfo}].
|
|
*/
|
|
|
|
TXDB.prototype.getInfo = function getInfo(tx, callback) {
|
|
this.walletdb.getPathInfo(this.wallet, tx, callback);
|
|
};
|
|
|
|
/**
|
|
* 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(hash, index, outpoint, callback) {
|
|
var self = this;
|
|
var p = new BufferWriter();
|
|
var key = this.key.o(hash, index);
|
|
|
|
this.get(key, function(err, data) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (data)
|
|
p.writeBytes(data);
|
|
|
|
p.writeBytes(outpoint);
|
|
|
|
self.put(key, p.render());
|
|
|
|
return callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Retrieve orphan list by coin ID.
|
|
* @private
|
|
* @param {String} key
|
|
* @param {Function} callback - Returns [Error, {@link Orphan}].
|
|
*/
|
|
|
|
TXDB.prototype._getOrphans = function _getOrphans(hash, index, callback) {
|
|
var self = this;
|
|
var items = [];
|
|
|
|
this.fetch(this.key.o(hash, index), function(data) {
|
|
var p = new BufferReader(data);
|
|
var orphans = [];
|
|
|
|
while (p.left())
|
|
orphans.push(bcoin.outpoint.fromRaw(p));
|
|
|
|
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);
|
|
|
|
items.push([orphan, tx]);
|
|
|
|
next();
|
|
});
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, items);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Retrieve coins for own inputs, remove
|
|
* double spenders, and verify inputs.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {PathInfo} info
|
|
* @param {Function} callback - Returns [Error].
|
|
*/
|
|
|
|
TXDB.prototype._verify = function _verify(tx, info, callback) {
|
|
var self = this;
|
|
|
|
utils.forEachSerial(tx.inputs, function(input, next, i) {
|
|
var prevout = input.prevout;
|
|
var address;
|
|
|
|
if (tx.isCoinbase())
|
|
return next();
|
|
|
|
address = input.getHash('hex');
|
|
|
|
// Only bother if this input is ours.
|
|
if (!info.hasPath(address))
|
|
return next();
|
|
|
|
self.getCoin(prevout.hash, prevout.index, function(err, coin) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
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);
|
|
}
|
|
|
|
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 next();
|
|
|
|
self.getTX(prevout.hash, function(err, prev) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!prev)
|
|
return callback(new Error('Could not find double-spent coin.'));
|
|
|
|
// NOTE: Could use d/spent.hash/spent.index
|
|
// here instead of getting a tx.
|
|
input.coin = bcoin.coin.fromTX(prev, prevout.index);
|
|
|
|
// Skip invalid transactions
|
|
if (self.options.verify) {
|
|
if (!tx.verifyInput(i))
|
|
return callback(null, false);
|
|
}
|
|
|
|
self._removeConflict(spent.hash, tx, function(err, rtx, rinfo) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
// Spender was not removed, the current
|
|
// transaction is not elligible to be added.
|
|
if (!rtx)
|
|
return callback(null, false);
|
|
|
|
self.emit('conflict', rtx, rinfo);
|
|
|
|
next();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, true);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Attempt to resolve orphans for an output.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {Number} index
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) {
|
|
var self = this;
|
|
var hash = tx.hash('hex');
|
|
var coin;
|
|
|
|
this._getOrphans(hash, index, function(err, orphans) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!orphans)
|
|
return callback(null, false);
|
|
|
|
self.del(self.key.o(hash, index));
|
|
|
|
coin = bcoin.coin.fromTX(tx, index);
|
|
|
|
// Add input to orphan
|
|
utils.forEachSerial(orphans, function(item, next) {
|
|
var input = item[0];
|
|
var orphan = item[1];
|
|
|
|
// Probably removed by some other means.
|
|
if (!orphan)
|
|
return next();
|
|
|
|
orphan.inputs[input.index].coin = coin;
|
|
|
|
assert(orphan.inputs[input.index].prevout.hash === hash);
|
|
assert(orphan.inputs[input.index].prevout.index === index);
|
|
|
|
// Verify that input script is correct, if not - add
|
|
// output to unspent and remove orphan from storage
|
|
if (!self.options.verify || orphan.verifyInput(input.index)) {
|
|
self.put(self.key.d(input.hash, input.index), coin.toRaw());
|
|
return callback(null, true);
|
|
}
|
|
|
|
self._lazyRemove(orphan, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
// Just going to be added again outside.
|
|
self.balance.sub(coin);
|
|
|
|
return callback(null, false);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Add transaction, runs _confirm (separate batch) and
|
|
* verify (separate batch for double spenders).
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {PathInfo} info
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.add = function add(tx, info, callback) {
|
|
var self = this;
|
|
var unlock = this._lock(add, [tx, info, callback]);
|
|
var hash, i, path, account;
|
|
|
|
if (!unlock)
|
|
return;
|
|
|
|
callback = utils.wrap(callback, unlock);
|
|
|
|
if (tx.mutable)
|
|
tx = tx.toTX();
|
|
|
|
// Attempt to confirm tx before adding it.
|
|
this._confirm(tx, info, function(err, existing) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
// Ignore if we already have this tx.
|
|
if (existing)
|
|
return callback(null, true, info);
|
|
|
|
self._verify(tx, info, function(err, result) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!result)
|
|
return callback(null, result, info);
|
|
|
|
hash = tx.hash('hex');
|
|
|
|
self.start();
|
|
self.put(self.key.t(hash), tx.toExtended());
|
|
|
|
if (tx.ts === 0)
|
|
self.put(self.key.p(hash), DUMMY);
|
|
else
|
|
self.put(self.key.h(tx.height, hash), DUMMY);
|
|
|
|
self.put(self.key.m(tx.ps, hash), DUMMY);
|
|
|
|
for (i = 0; i < info.accounts.length; i++) {
|
|
account = info.accounts[i];
|
|
self.put(self.key.T(account, hash), DUMMY);
|
|
if (tx.ts === 0)
|
|
self.put(self.key.P(account, hash), DUMMY);
|
|
else
|
|
self.put(self.key.H(account, tx.height, hash), DUMMY);
|
|
self.put(self.key.M(account, tx.ps, hash), DUMMY);
|
|
}
|
|
|
|
// Consume unspent money or add orphans
|
|
utils.forEachSerial(tx.inputs, function(input, next, i) {
|
|
var prevout = input.prevout;
|
|
var key, address, outpoint;
|
|
|
|
if (tx.isCoinbase())
|
|
return next();
|
|
|
|
address = input.getHash('hex');
|
|
path = info.getPath(address);
|
|
|
|
// Only bother if this input is ours.
|
|
if (!path)
|
|
return next();
|
|
|
|
key = prevout.hash + prevout.index;
|
|
|
|
// s/[outpoint-key] -> [spender-hash]|[spender-input-index]
|
|
outpoint = bcoin.outpoint.fromTX(tx, i).toRaw();
|
|
self.put(self.key.s(prevout.hash, prevout.index), outpoint);
|
|
|
|
// Add orphan, if no parent transaction is yet known
|
|
if (!input.coin)
|
|
return self._addOrphan(prevout.hash, prevout.index, outpoint, next);
|
|
|
|
self.del(self.key.c(prevout.hash, prevout.index));
|
|
self.del(self.key.C(path.account, prevout.hash, prevout.index));
|
|
self.put(self.key.d(hash, i), input.coin.toRaw());
|
|
self.balance.sub(input.coin);
|
|
|
|
self.coinCache.remove(key);
|
|
|
|
next();
|
|
}, function(err) {
|
|
if (err) {
|
|
self.drop();
|
|
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;
|
|
|
|
path = info.getPath(address);
|
|
|
|
// Do not add unspents for outputs that aren't ours.
|
|
if (!path)
|
|
return next();
|
|
|
|
self._resolveOrphans(tx, i, function(err, orphans) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (orphans)
|
|
return next();
|
|
|
|
coin = bcoin.coin.fromTX(tx, i);
|
|
self.balance.add(coin);
|
|
coin = coin.toRaw();
|
|
|
|
self.put(self.key.c(hash, i), coin);
|
|
self.put(self.key.C(path.account, hash, i), DUMMY);
|
|
|
|
self.coinCache.set(key, coin);
|
|
|
|
next();
|
|
});
|
|
}, function(err) {
|
|
if (err) {
|
|
self.drop();
|
|
return callback(err);
|
|
}
|
|
|
|
self.commit(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.emit('tx', tx, info);
|
|
|
|
if (tx.ts !== 0)
|
|
self.emit('confirmed', tx, info);
|
|
|
|
return callback(null, true, info);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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, info) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, tx, info);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
if (!spent)
|
|
return next();
|
|
|
|
// Remove all of the spender's spenders first.
|
|
self.getTX(spent.hash, function(err, tx) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!tx)
|
|
return next(new Error('Could not find spender.'));
|
|
|
|
self._removeRecursive(tx, next);
|
|
});
|
|
});
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.start();
|
|
|
|
// Remove the spender.
|
|
self._lazyRemove(tx, function(err, result, info) {
|
|
if (err) {
|
|
self.drop();
|
|
return callback(err);
|
|
}
|
|
|
|
self.commit(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, result, info);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
var prevout = input.prevout;
|
|
self.isSpent(prevout.hash, 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 = this.key.s(hash, index);
|
|
this.fetch(key, function(data) {
|
|
return bcoin.outpoint.fromRaw(data);
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Attempt to confirm a transaction.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {AddressMap} info
|
|
* @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, info, callback) {
|
|
var self = this;
|
|
var hash = tx.hash('hex');
|
|
var i, account;
|
|
|
|
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, info);
|
|
|
|
// Existing tx is already confirmed. Ignore.
|
|
if (existing.ts !== 0)
|
|
return callback(null, true, info);
|
|
|
|
// The incoming tx won't confirm the
|
|
// existing one anyway. Ignore.
|
|
if (tx.ts === 0)
|
|
return callback(null, true, info);
|
|
|
|
// Tricky - update the tx and coin in storage,
|
|
// and remove pending flag to mark as confirmed.
|
|
assert(tx.height >= 0);
|
|
|
|
// Save the original received time.
|
|
tx.ps = existing.ps;
|
|
|
|
self.start();
|
|
|
|
self.put(self.key.t(hash), tx.toExtended());
|
|
|
|
self.del(self.key.p(hash));
|
|
self.put(self.key.h(tx.height, hash), DUMMY);
|
|
|
|
for (i = 0; i < info.accounts.length; i++) {
|
|
account = info.accounts[i];
|
|
self.del(self.key.P(account, hash));
|
|
self.put(self.key.H(account, tx.height, 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 (!info.hasPath(address))
|
|
return next();
|
|
|
|
self.getCoin(hash, i, function(err, coin) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coin) {
|
|
// TODO: Update spent coin here!
|
|
return next();
|
|
}
|
|
|
|
self.balance.confirm(coin.value);
|
|
|
|
coin.height = tx.height;
|
|
coin = coin.toRaw();
|
|
|
|
self.put(self.key.c(hash, i), coin);
|
|
|
|
self.coinCache.set(key, coin);
|
|
|
|
next();
|
|
});
|
|
}, function(err) {
|
|
if (err) {
|
|
self.drop();
|
|
return callback(err);
|
|
}
|
|
|
|
self.commit(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.emit('tx', tx, info);
|
|
self.emit('confirmed', tx, info);
|
|
|
|
return callback(null, true, info);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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 unlock = this._lock(remove, [hash, callback], force);
|
|
|
|
if (!unlock)
|
|
return;
|
|
|
|
callback = utils.wrap(callback, unlock);
|
|
|
|
this._removeRecursive(hash, function(err, result, info) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, !!result, info);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
var self = this;
|
|
this.getInfo(tx, function(err, info) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!info)
|
|
return callback(null, false);
|
|
|
|
self._remove(tx, info, callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Remove a transaction from the database. Disconnect inputs.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @param {AddressMap} info
|
|
* @param {Function} callback - Returns [Error].
|
|
*/
|
|
|
|
TXDB.prototype._remove = function remove(tx, info, callback) {
|
|
var self = this;
|
|
var hash = tx.hash('hex');
|
|
var i, path, account, key, address, input, output, coin;
|
|
|
|
this.del(this.key.t(hash));
|
|
|
|
if (tx.ts === 0)
|
|
this.del(this.key.p(hash));
|
|
else
|
|
this.del(this.key.h(tx.height, hash));
|
|
|
|
this.del(this.key.m(tx.ps, hash));
|
|
|
|
for (i = 0; i < info.accounts.length; i++) {
|
|
account = info.accounts[i];
|
|
this.del(this.key.T(account, hash));
|
|
if (tx.ts === 0)
|
|
this.del(this.key.P(account, hash));
|
|
else
|
|
this.del(this.key.H(account, tx.height, hash));
|
|
this.del(this.key.M(account, tx.ps, 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;
|
|
var prevout = input.prevout;
|
|
address = input.getHash('hex');
|
|
|
|
if (tx.isCoinbase())
|
|
break;
|
|
|
|
if (!input.coin)
|
|
continue;
|
|
|
|
path = info.getPath(address);
|
|
|
|
if (!path)
|
|
continue;
|
|
|
|
self.balance.add(input.coin);
|
|
|
|
coin = input.coin.toRaw();
|
|
|
|
self.put(self.key.c(prevout.hash, prevout.index), coin);
|
|
self.put(self.key.C(path.account, prevout.hash, prevout.index), DUMMY);
|
|
self.del(self.key.d(hash, i));
|
|
self.del(self.key.s(prevout.hash, prevout.index));
|
|
self.del(self.key.o(prevout.hash, prevout.index));
|
|
|
|
self.coinCache.set(key, coin);
|
|
}
|
|
|
|
for (i = 0; i < tx.outputs.length; i++) {
|
|
output = tx.outputs[i];
|
|
key = hash + i;
|
|
address = output.getHash('hex');
|
|
|
|
path = info.getPath(address);
|
|
|
|
if (!path)
|
|
continue;
|
|
|
|
coin = bcoin.coin.fromTX(tx, i);
|
|
|
|
self.balance.sub(coin);
|
|
|
|
self.del(self.key.c(hash, i));
|
|
self.del(self.key.C(path.account, hash, i));
|
|
|
|
self.coinCache.remove(key);
|
|
}
|
|
|
|
self.emit('remove tx', tx, info);
|
|
|
|
return callback(null, true, info);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
var unlock = this._lock(unconfirm, [hash, callback], force);
|
|
|
|
if (!unlock)
|
|
return;
|
|
|
|
callback = utils.wrap(callback, unlock);
|
|
|
|
this.getTX(hash, function(err, tx) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!tx)
|
|
return callback(null, true);
|
|
|
|
self.getInfo(tx, function(err, info) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!info)
|
|
return callback(null, false);
|
|
|
|
self.start();
|
|
|
|
self._unconfirm(tx, info, function(err, result, info) {
|
|
if (err) {
|
|
self.drop();
|
|
return callback(err);
|
|
}
|
|
|
|
self.commit(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, result, info);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Unconfirm a transaction. This is usually necessary after a reorg.
|
|
* @param {Hash} hash
|
|
* @param {AddressMap} info
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) {
|
|
var self = this;
|
|
var hash = tx.hash('hex');
|
|
var height = tx.height;
|
|
var i, account;
|
|
|
|
if (height !== -1)
|
|
return callback(null, false, info);
|
|
|
|
tx.height = -1;
|
|
tx.ts = 0;
|
|
tx.index = -1;
|
|
tx.block = null;
|
|
|
|
this.put(this.key.t(hash), tx.toExtended());
|
|
|
|
this.put(this.key.p(hash), DUMMY);
|
|
this.del(this.key.h(height, hash));
|
|
|
|
for (i = 0; i < info.accounts.length; i++) {
|
|
account = info.accounts[i];
|
|
this.put(this.key.P(account, hash), DUMMY);
|
|
this.del(this.key.H(account, height, hash));
|
|
}
|
|
|
|
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) {
|
|
// TODO: Update spent coin here
|
|
return next();
|
|
}
|
|
|
|
self.balance.unconfirm(coin.value);
|
|
coin.height = tx.height;
|
|
coin = coin.toRaw();
|
|
|
|
self.put(self.key.c(hash, i), coin);
|
|
|
|
self.coinCache.set(key, coin);
|
|
|
|
next();
|
|
});
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.emit('unconfirmed', tx, info);
|
|
|
|
return callback(null, true, info);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get hashes of all transactions in the database.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link Hash}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.iterate({
|
|
gte: account != null
|
|
? this.key.T(account, constants.NULL_HASH)
|
|
: this.key.t(constants.NULL_HASH),
|
|
lte: account != null
|
|
? this.key.T(account, constants.HIGH_HASH)
|
|
: this.key.t(constants.HIGH_HASH),
|
|
transform: function(key) {
|
|
if (account != null) {
|
|
key = self.key.Tt(key);
|
|
return key[1];
|
|
}
|
|
key = self.key.tt(key);
|
|
return key[0];
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get hashes of all unconfirmed transactions in the database.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link Hash}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.iterate({
|
|
gte: account != null
|
|
? this.key.P(account, constants.NULL_HASH)
|
|
: this.key.p(constants.NULL_HASH),
|
|
lte: account != null
|
|
? this.key.P(account, constants.HIGH_HASH)
|
|
: this.key.p(constants.HIGH_HASH),
|
|
transform: function(key) {
|
|
if (account != null) {
|
|
key = self.key.Pp(key);
|
|
return key[1];
|
|
}
|
|
key = self.key.pp(key);
|
|
return key[0];
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get all coin hashes in the database.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link Hash}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.iterate({
|
|
gte: account != null
|
|
? this.key.C(account, constants.NULL_HASH, 0)
|
|
: this.key.c(constants.NULL_HASH, 0),
|
|
lte: account != null
|
|
? this.key.C(account, constants.HIGH_HASH, 0xffffffff)
|
|
: this.key.c(constants.HIGH_HASH, 0xffffffff),
|
|
transform: function(key) {
|
|
if (account != null) {
|
|
key = self.key.Cc(key);
|
|
return [key[1], key[2]];
|
|
}
|
|
key = self.key.cc(key);
|
|
return key;
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @param {Function} callback - Returns [Error, {@link Hash}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account !== 'number') {
|
|
callback = options;
|
|
options = account;
|
|
account = null;
|
|
}
|
|
|
|
this.iterate({
|
|
gte: account != null
|
|
? this.key.H(account, options.start, constants.NULL_HASH)
|
|
: this.key.h(options.start, constants.NULL_HASH),
|
|
lte: account != null
|
|
? this.key.H(account, options.end, constants.HIGH_HASH)
|
|
: this.key.h(options.end, constants.HIGH_HASH),
|
|
limit: options.limit,
|
|
reverse: options.reverse,
|
|
transform: function(key) {
|
|
if (account != null) {
|
|
key = self.key.Hh(key);
|
|
return key[2];
|
|
}
|
|
key = self.key.hh(key);
|
|
return key[1];
|
|
}
|
|
}, 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 {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.
|
|
* @param {Function} callback - Returns [Error, {@link Hash}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.iterate({
|
|
gte: account != null
|
|
? this.key.M(account, options.start, constants.NULL_HASH)
|
|
: this.key.m(options.start, constants.NULL_HASH),
|
|
lte: account != null
|
|
? this.key.M(account, options.end, constants.HIGH_HASH)
|
|
: this.key.m(options.end, constants.HIGH_HASH),
|
|
limit: options.limit,
|
|
reverse: options.reverse,
|
|
transform: function(key) {
|
|
if (account != null) {
|
|
key = self.key.Mm(key);
|
|
return key[2];
|
|
}
|
|
key = self.key.mm(key);
|
|
return key[1];
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get transactions 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.
|
|
* @param {Function} callback - Returns [Error, {@link TX}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getRange = function getRange(account, options, callback) {
|
|
var self = this;
|
|
var txs = [];
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.getRangeHashes(account, 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 {Number?} account
|
|
* @param {Number} limit - Max number of transactions.
|
|
* @param {Function} callback - Returns [Error, {@link TX}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getLast = function getLast(account, limit, callback) {
|
|
if (typeof limit === 'function') {
|
|
callback = limit;
|
|
limit = account;
|
|
account = null;
|
|
}
|
|
|
|
this.getRange(account, {
|
|
start: 0,
|
|
end: 0xffffffff,
|
|
reverse: true,
|
|
limit: limit
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get all transactions.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link TX}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getHistory = function getHistory(account, callback) {
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
// Slow case
|
|
if (account != null)
|
|
return this.getAccountHistory(account, callback);
|
|
|
|
// Fast case
|
|
this.iterate({
|
|
gte: this.key.t(constants.NULL_HASH),
|
|
lte: this.key.t(constants.HIGH_HASH),
|
|
values: true,
|
|
parse: function(data) {
|
|
return bcoin.tx.fromExtended(data);
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get all account transactions.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link TX}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getAccountHistory = function getAccountHistory(account, callback) {
|
|
var self = this;
|
|
var txs = [];
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.getHistoryHashes(account, 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, sortTX(txs));
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get last active timestamp and height.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, Number(ts), Number(height)].
|
|
*/
|
|
|
|
TXDB.prototype.getLastTime = function getLastTime(account, callback) {
|
|
var i, tx, lastTs, lastHeight;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.getHistory(account, 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 {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link TX}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getUnconfirmed = function getUnconfirmed(account, callback) {
|
|
var self = this;
|
|
var txs = [];
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
this.getUnconfirmedHashes(account, 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, sortTX(txs));
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get coins.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link Coin}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getCoins = function getCoins(account, callback) {
|
|
var self = this;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
// Slow case
|
|
if (account != null)
|
|
return this.getAccountCoins(account, callback);
|
|
|
|
// Fast case
|
|
this.iterate({
|
|
gte: this.key.c(constants.NULL_HASH, 0),
|
|
lte: this.key.c(constants.HIGH_HASH, 0xffffffff),
|
|
keys: true,
|
|
values: true,
|
|
parse: function(data, key) {
|
|
var parts = self.key.cc(key);
|
|
var hash = parts[0];
|
|
var index = parts[1];
|
|
var coin = bcoin.coin.fromRaw(data);
|
|
coin.hash = hash;
|
|
coin.index = index;
|
|
key = hash + index;
|
|
self.coinCache.set(key, data);
|
|
return coin;
|
|
}
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get coins by account.
|
|
* @param {Number} account
|
|
* @param {Function} callback - Returns [Error, {@link Coin}[]].
|
|
*/
|
|
|
|
TXDB.prototype.getAccountCoins = function getCoins(account, callback) {
|
|
var self = this;
|
|
var coins = [];
|
|
|
|
this.getCoinHashes(account, 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;
|
|
var hash, index, coin, input;
|
|
|
|
if (tx.isCoinbase()) {
|
|
callback = utils.asyncify(callback);
|
|
return callback(null, tx);
|
|
}
|
|
|
|
hash = tx.hash('hex');
|
|
|
|
this.iterate({
|
|
gte: this.key.d(hash, 0),
|
|
lte: this.key.d(hash, 0xffffffff),
|
|
keys: true,
|
|
values: true,
|
|
parse: function(value, key) {
|
|
index = self.key.dd(key)[1];
|
|
coin = bcoin.coin.fromRaw(value);
|
|
input = tx.inputs[index];
|
|
coin.hash = input.prevout.hash;
|
|
coin.index = input.prevout.index;
|
|
input.coin = coin;
|
|
}
|
|
}, 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) {
|
|
var prevout = input.prevout;
|
|
|
|
if (input.coin)
|
|
return next();
|
|
|
|
self.getCoin(prevout.hash, 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.fetch(this.key.t(hash), function(tx) {
|
|
return bcoin.tx.fromExtended(tx);
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Get transaction details.
|
|
* @param {Hash} hash
|
|
* @param {Function} callback - Returns [Error, {@link TXDetails}].
|
|
*/
|
|
|
|
TXDB.prototype.getDetails = function getDetails(hash, callback) {
|
|
var self = this;
|
|
this.getTX(hash, function(err, tx) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!tx)
|
|
return callback();
|
|
|
|
self.toDetails(tx, callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Convert transaction to transaction details.
|
|
* @param {TX|TX[]} tx
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.toDetails = function toDetails(tx, callback) {
|
|
var self = this;
|
|
var out;
|
|
|
|
if (Array.isArray(tx)) {
|
|
out = [];
|
|
return utils.forEachSerial(tx, function(tx, next) {
|
|
self.toDetails(tx, function(err, details) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!details)
|
|
return next();
|
|
|
|
out.push(details);
|
|
next();
|
|
});
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, out);
|
|
});
|
|
}
|
|
|
|
this.fillHistory(tx, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.getInfo(tx, function(err, info) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!info)
|
|
return callback(new Error('Info not found.'));
|
|
|
|
return callback(null, info.toDetails());
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Test whether the database has a transaction.
|
|
* @param {Hash} hash
|
|
* @param {Function} callback - Returns [Error, Boolean].
|
|
*/
|
|
|
|
TXDB.prototype.hasTX = function hasTX(hash, callback) {
|
|
this.has(this.key.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.fetch(this.key.c(hash, index), function(data) {
|
|
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);
|
|
|
|
this.has(this.key.c(hash, index), callback);
|
|
};
|
|
|
|
/**
|
|
* Calculate balance.
|
|
* @param {Number?} account
|
|
* @param {Function} callback - Returns [Error, {@link Balance}].
|
|
*/
|
|
|
|
TXDB.prototype.getBalance = function getBalance(account, callback) {
|
|
var self = this;
|
|
var balance;
|
|
|
|
if (typeof account === 'function') {
|
|
callback = account;
|
|
account = null;
|
|
}
|
|
|
|
// Slow case
|
|
if (account != null)
|
|
return this.getAccountBalance(account, callback);
|
|
|
|
// Really fast case
|
|
if (this.balance)
|
|
return callback(null, this.balance);
|
|
|
|
// Fast case
|
|
balance = new Balance(this.wallet);
|
|
|
|
this.iterate({
|
|
gte: this.key.c(constants.NULL_HASH, 0),
|
|
lte: this.key.c(constants.HIGH_HASH, 0xffffffff),
|
|
keys: true,
|
|
values: true,
|
|
parse: function(data, key) {
|
|
var parts = self.key.cc(key);
|
|
var hash = parts[0];
|
|
var index = parts[1];
|
|
var height = data.readUInt32LE(4, true);
|
|
var value = utils.read64N(data, 8);
|
|
|
|
assert(data.length >= 16);
|
|
|
|
balance.total += value;
|
|
|
|
if (height === 0x7fffffff)
|
|
balance.unconfirmed += value;
|
|
else
|
|
balance.confirmed += value;
|
|
|
|
key = hash + index;
|
|
|
|
self.coinCache.set(key, data);
|
|
}
|
|
}, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, balance);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Calculate balance by account.
|
|
* @param {Number} account
|
|
* @param {Function} callback - Returns [Error, {@link Balance}].
|
|
*/
|
|
|
|
TXDB.prototype.getAccountBalance = function getBalance(account, callback) {
|
|
var self = this;
|
|
var balance = new Balance(this.wallet);
|
|
var key, coin;
|
|
|
|
function parse(data) {
|
|
var height = data.readUInt32LE(4, true);
|
|
var value = utils.read64N(data, 8);
|
|
|
|
assert(data.length >= 16);
|
|
|
|
balance.total += value;
|
|
|
|
if (height === 0x7fffffff)
|
|
balance.unconfirmed += value;
|
|
else
|
|
balance.confirmed += value;
|
|
}
|
|
|
|
this.getCoinHashes(account, 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.get(self.key.c(hash[0], hash[1]), 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, balance);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {Number?} account
|
|
* @param {Number} age - Age delta (delete transactions older than `now - age`).
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.zap = function zap(account, age, callback, force) {
|
|
var self = this;
|
|
var unlock;
|
|
|
|
if (typeof age === 'function') {
|
|
force = callback;
|
|
callback = age;
|
|
age = account;
|
|
account = null;
|
|
}
|
|
|
|
unlock = this._lock(zap, [account, age, callback], force);
|
|
|
|
if (!unlock)
|
|
return;
|
|
|
|
callback = utils.wrap(callback, unlock);
|
|
|
|
if (!utils.isUInt32(age))
|
|
return callback(new Error('Age must be a number.'));
|
|
|
|
this.getRange(account, {
|
|
start: 0,
|
|
end: bcoin.now() - age
|
|
}, function(err, txs) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
if (tx.ts !== 0)
|
|
return next();
|
|
self.remove(tx.hash('hex'), next, true);
|
|
}, callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Abandon transaction.
|
|
* @param {Hash} hash
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
TXDB.prototype.abandon = function abandon(hash, callback, force) {
|
|
var self = this;
|
|
this.has(this.key.p(hash), function(err, result) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!result)
|
|
return callback(new Error('TX not eligible.'));
|
|
|
|
self.remove(hash, callback, force);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Details
|
|
*/
|
|
|
|
function Details(info) {
|
|
if (!(this instanceof Details))
|
|
return new Details(info);
|
|
|
|
this.db = info.db;
|
|
this.network = info.db.network;
|
|
this.wid = info.wid;
|
|
this.id = info.id;
|
|
this.hash = info.tx.hash('hex');
|
|
this.height = info.tx.height;
|
|
this.block = info.tx.block;
|
|
this.index = info.tx.index;
|
|
this.confirmations = info.tx.getConfirmations(this.db.height);
|
|
this.fee = info.tx.hasCoins() ? info.tx.getFee() : 0;
|
|
this.ts = info.tx.ts;
|
|
this.ps = info.tx.ps;
|
|
this.tx = info.tx;
|
|
this.inputs = [];
|
|
this.outputs = [];
|
|
|
|
this.init(info.table);
|
|
}
|
|
|
|
Details.prototype.init = function init(table) {
|
|
this._insert(this.tx.inputs, this.inputs, table);
|
|
this._insert(this.tx.outputs, this.outputs, table);
|
|
};
|
|
|
|
Details.prototype._insert = function _insert(vector, target, table) {
|
|
var i, j, io, address, hash, paths, path, member;
|
|
|
|
for (i = 0; i < vector.length; i++) {
|
|
io = vector[i];
|
|
member = new DetailsMember();
|
|
|
|
if (io instanceof bcoin.input)
|
|
member.value = io.coin ? io.coin.value : 0;
|
|
else
|
|
member.value = io.value;
|
|
|
|
address = io.getAddress();
|
|
|
|
if (address) {
|
|
member.address = address;
|
|
|
|
hash = address.getHash('hex');
|
|
paths = table[hash];
|
|
|
|
for (j = 0; j < paths.length; j++) {
|
|
path = paths[j];
|
|
if (path.wid === this.wid) {
|
|
path.id = this.id;
|
|
member.path = path;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
target.push(member);
|
|
}
|
|
};
|
|
|
|
Details.prototype.toJSON = function toJSON() {
|
|
var self = this;
|
|
return {
|
|
wid: this.wid,
|
|
id: this.id,
|
|
hash: utils.revHex(this.hash),
|
|
height: this.height,
|
|
block: this.block ? utils.revHex(this.block) : null,
|
|
ts: this.ts,
|
|
ps: this.ps,
|
|
index: this.index,
|
|
fee: utils.btc(this.fee),
|
|
confirmations: this.confirmations,
|
|
inputs: this.inputs.map(function(input) {
|
|
return input.toJSON(self.network);
|
|
}),
|
|
outputs: this.outputs.map(function(output) {
|
|
return output.toJSON(self.network);
|
|
}),
|
|
tx: this.tx.toRaw().toString('hex')
|
|
};
|
|
};
|
|
|
|
/**
|
|
* DetailsMember
|
|
*/
|
|
|
|
function DetailsMember() {
|
|
if (!(this instanceof DetailsMember))
|
|
return new DetailsMember();
|
|
|
|
this.value = 0;
|
|
this.address = null;
|
|
this.path = null;
|
|
}
|
|
|
|
DetailsMember.prototype.toJSON = function toJSON(network) {
|
|
return {
|
|
value: utils.btc(this.value),
|
|
address: this.address
|
|
? this.address.toBase58(network)
|
|
: null,
|
|
path: this.path
|
|
? this.path.toJSON()
|
|
: null
|
|
};
|
|
};
|
|
|
|
/*
|
|
* Balance
|
|
*/
|
|
|
|
function Balance(wallet) {
|
|
if (!(this instanceof Balance))
|
|
return new Balance(wallet);
|
|
|
|
this.wid = wallet.wid;
|
|
this.id = wallet.id;
|
|
this.unconfirmed = 0;
|
|
this.confirmed = 0;
|
|
this.total = 0;
|
|
}
|
|
|
|
Balance.prototype.add = function add(coin) {
|
|
this.total += coin.value;
|
|
if (coin.height === -1)
|
|
this.unconfirmed += coin.value;
|
|
else
|
|
this.confirmed += coin.value;
|
|
};
|
|
|
|
Balance.prototype.sub = function sub(coin) {
|
|
this.total -= coin.value;
|
|
if (coin.height === -1)
|
|
this.unconfirmed -= coin.value;
|
|
else
|
|
this.confirmed -= coin.value;
|
|
};
|
|
|
|
Balance.prototype.confirm = function confirm(value) {
|
|
this.unconfirmed -= value;
|
|
this.confirmed += value;
|
|
};
|
|
|
|
Balance.prototype.unconfirm = function unconfirm(value) {
|
|
this.unconfirmed += value;
|
|
this.confirmed -= value;
|
|
};
|
|
|
|
Balance.prototype.toJSON = function toJSON() {
|
|
return {
|
|
wid: this.wid,
|
|
id: this.id,
|
|
unconfirmed: utils.btc(this.unconfirmed),
|
|
confirmed: utils.btc(this.confirmed),
|
|
total: utils.btc(this.total)
|
|
};
|
|
};
|
|
|
|
Balance.prototype.toString = function toString() {
|
|
return '<Balance'
|
|
+ ' unconfirmed=' + utils.btc(this.unconfirmed)
|
|
+ ' confirmed=' + utils.btc(this.confirmed)
|
|
+ ' total=' + utils.btc(this.total)
|
|
+ '>';
|
|
};
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function sortTX(txs) {
|
|
return txs.sort(function(a, b) {
|
|
return a.ps - b.ps;
|
|
});
|
|
}
|
|
|
|
function sortCoins(coins) {
|
|
return coins.sort(function(a, b) {
|
|
a = a.height === -1 ? 0x7fffffff : a.height;
|
|
b = b.height === -1 ? 0x7fffffff : b.height;
|
|
return a - b;
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
TXDB.Details = Details;
|
|
module.exports = TXDB;
|