txdb: add txdb state.

This commit is contained in:
Christopher Jeffrey 2016-10-10 07:13:42 -07:00
parent a864ec1552
commit aed03c2655
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
8 changed files with 396 additions and 132 deletions

View File

@ -481,7 +481,7 @@ HTTPClient.prototype.getCoins = function getCoins(id, account) {
* @returns {Promise} - Returns {@link TX}[].
*/
HTTPClient.prototype.getUnconfirmed = function getUnconfirmed(id, account) {
HTTPClient.prototype.getPending = function getPending(id, account) {
var options = { account: account };
return this._get('/wallet/' + id + '/tx/unconfirmed', options);
};

View File

@ -1013,7 +1013,7 @@ HTTPServer.prototype._init = function _init() {
this.get('/wallet/:id/tx/unconfirmed', con(function* (req, res, send, next) {
var options = req.options;
var acct = options.name || options.account;
var txs = yield req.wallet.getUnconfirmed(acct);
var txs = yield req.wallet.getPending(acct);
var details;
sortTX(txs);
@ -1146,6 +1146,11 @@ HTTPServer.prototype._initIO = function _initIO() {
if (!utils.isHex256(token))
return callback({ error: 'Invalid parameter.' });
if (socket.api && id === '!all') {
socket.join(id);
return callback();
}
self.walletdb.auth(id, token).then(function(wallet) {
if (!wallet)
return callback({ error: 'Wallet does not exist.' });
@ -1175,6 +1180,7 @@ HTTPServer.prototype._initIO = function _initIO() {
socket.on('watch chain', function(args, callback) {
if (!socket.api)
return callback({ error: 'Not authorized.' });
socket.watchChain();
callback();
});
@ -1182,6 +1188,7 @@ HTTPServer.prototype._initIO = function _initIO() {
socket.on('unwatch chain', function(args, callback) {
if (!socket.api)
return callback({ error: 'Not authorized.' });
socket.unwatchChain();
callback();
});

View File

@ -153,11 +153,11 @@ HTTPWallet.prototype.getCoins = function getCoins(account) {
};
/**
* @see Wallet#getUnconfirmed
* @see Wallet#getPending
*/
HTTPWallet.prototype.getUnconfirmed = function getUnconfirmed(account) {
return this.client.getUnconfirmed(this.id, account);
HTTPWallet.prototype.getPending = function getPending(account) {
return this.client.getPending(this.id, account);
};
/**

View File

@ -60,6 +60,7 @@ layout.txdb = {
pre: function prefix(key) {
return +key.slice(1, 11);
},
R: 'R',
hi: function hi(ch, hash, index) {
return ch + hash + pad32(index);
},

View File

@ -47,6 +47,7 @@ var layout = {
pre: function prefix(key) {
return key.readUInt32BE(1, true);
},
R: new Buffer([0x52]),
hi: function hi(ch, hash, index) {
var key = new Buffer(37);
key[0] = ch;
@ -211,10 +212,13 @@ function TXDB(wallet) {
this.logger = wallet.db.logger;
this.network = wallet.db.network;
this.options = wallet.db.options;
this.locked = {};
this.coinCache = new LRU(10000);
this.locked = {};
this.state = null;
this.balance = null;
this.pending = null;
this.events = [];
}
/**
@ -230,8 +234,21 @@ TXDB.layout = layout;
*/
TXDB.prototype.open = co(function* open() {
this.balance = yield this.getBalance();
this.logger.info('TXDB loaded for %s.', this.wallet.id);
var state = yield 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.balance = this.state.balance;
this.logger.info('TXDB State: tx=%d coin=%s.',
this.state.tx, this.state.coin);
this.logger.info(
'Balance: unconfirmed=%s confirmed=%s total=%s.',
utils.btc(this.balance.unconfirmed),
@ -239,17 +256,83 @@ TXDB.prototype.open = co(function* open() {
utils.btc(this.balance.total));
});
/**
* Start batch.
* @private
*/
TXDB.prototype.start = function start() {
this.pending = this.state.clone();
return this.wallet.start();
};
/**
* Drop batch.
* @private
*/
TXDB.prototype.drop = function drop() {
this.pending = null;
this.events.length = 0;
return this.wallet.drop();
};
/**
* Clear batch.
* @private
*/
TXDB.prototype.clear = function clear() {
this.pending = this.state.clone();
this.events.length = 0;
return this.wallet.clear();
};
/**
* Save batch.
* @returns {Promise}
*/
TXDB.prototype.commit = co(function* commit() {
var i, item;
try {
yield this.wallet.commit();
} catch (e) {
this.pending = null;
this.events.length = 0;
throw e;
}
// Overwrite the entire state
// with our new committed state.
if (this.pending.committed) {
this.state = this.pending;
this.balance = this.state.balance;
// Emit buffered events now that
// we know everything is written.
for (i = 0; i < this.events.length; i++) {
item = this.events[i];
this.walletdb.emit(item[0], this.wallet.id, item[1], item[2]);
this.wallet.emit(item[0], item[1], item[2]);
}
}
this.pending = null;
this.events.length = 0;
});
/**
* Emit transaction event.
* @private
* @param {String} event
* @param {TX} tx
* @param {Object} data
* @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);
TXDB.prototype.emit = function emit(event, data, info) {
this.events.push([event, data, info]);
};
/**
@ -502,7 +585,7 @@ TXDB.prototype.verify = co(function* verify(tx, info) {
// remove other transactions on behalf of
// a non-eligible tx.
if (!conflict) {
this.wallet.clear();
this.clear();
return;
}
@ -569,7 +652,7 @@ TXDB.prototype.resolveOrphans = co(function* resolveOrphans(tx, index) {
// We had orphans, but they were invalid. The
// balance will be (incorrectly) added outside.
// Subtract to compensate.
this.balance.sub(coin);
this.pending.sub(coin);
return false;
});
@ -585,16 +668,16 @@ TXDB.prototype.add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
var result;
this.wallet.start();
this.start();
try {
result = yield this._add(tx, info);
} catch (e) {
this.wallet.drop();
this.drop();
throw e;
}
yield this.wallet.commit();
yield this.commit();
return result;
});
@ -681,7 +764,7 @@ TXDB.prototype._add = co(function* add(tx, info) {
this.del(layout.c(prevout.hash, prevout.index));
this.del(layout.C(path.account, prevout.hash, prevout.index));
this.put(layout.d(hash, i), input.coin.toRaw());
this.balance.sub(input.coin);
this.pending.sub(input.coin);
this.coinCache.remove(key);
}
@ -709,7 +792,7 @@ TXDB.prototype._add = co(function* add(tx, info) {
coin = Coin.fromTX(tx, i);
this.balance.add(coin);
this.pending.add(coin);
coin = coin.toRaw();
@ -727,6 +810,11 @@ TXDB.prototype._add = co(function* add(tx, info) {
if (tx.ts !== 0)
this.emit('confirmed', tx, info);
this.emit('balance', this.pending.balance, info);
this.pending.tx++;
this.put(layout.R, this.pending.commit());
return true;
});
@ -926,7 +1014,7 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
continue;
}
this.balance.confirm(coin.value);
this.pending.confirm(coin.value);
coin.height = tx.height;
coin = coin.toRaw();
@ -938,6 +1026,9 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
this.emit('tx', tx, info);
this.emit('confirmed', tx, info);
this.emit('balance', this.pending.balance, info);
this.put(layout.R, this.pending.commit());
return true;
});
@ -951,16 +1042,16 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
TXDB.prototype.remove = co(function* remove(hash) {
var result;
this.wallet.start();
this.start();
try {
result = yield this._remove(hash);
} catch (e) {
this.wallet.drop();
this.drop();
throw e;
}
yield this.wallet.commit();
yield this.commit();
return result;
});
@ -1055,7 +1146,7 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
if (!path)
continue;
this.balance.add(input.coin);
this.pending.add(input.coin);
coin = input.coin.toRaw();
@ -1081,7 +1172,7 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
coin = Coin.fromTX(tx, i);
this.balance.sub(coin);
this.pending.sub(coin);
this.del(layout.c(hash, i));
this.del(layout.C(path.account, hash, i));
@ -1090,6 +1181,10 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
}
this.emit('remove tx', tx, info);
this.emit('balance', this.pending.balance, info);
this.pending.tx--;
this.put(layout.R, this.pending.commit());
return info;
});
@ -1103,16 +1198,16 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
TXDB.prototype.unconfirm = co(function* unconfirm(hash) {
var result;
this.wallet.start();
this.start();
try {
result = yield this._unconfirm(hash);
} catch (e) {
this.wallet.drop();
this.drop();
throw e;
}
yield this.wallet.commit();
yield this.commit();
return result;
});
@ -1183,7 +1278,7 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx, info) {
continue;
}
this.balance.unconfirm(coin.value);
this.pending.unconfirm(coin.value);
coin.height = tx.height;
coin = coin.toRaw();
@ -1194,6 +1289,9 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx, info) {
}
this.emit('unconfirmed', tx, info);
this.emit('balance', this.pending.balance, info);
this.put(layout.R, this.pending.commit());
return info;
});
@ -1303,6 +1401,23 @@ TXDB.prototype.getLocked = function getLocked() {
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, constants.NULL_HASH),
lte: layout.T(account, constants.HIGH_HASH),
parse: function(key) {
key = layout.Tt(key);
return key[1];
}
});
};
/**
* Get hashes of all transactions in the database.
* @param {Number?} account
@ -1310,16 +1425,8 @@ TXDB.prototype.getLocked = function getLocked() {
*/
TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) {
if (account != null) {
return this.keys({
gte: layout.T(account, constants.NULL_HASH),
lte: layout.T(account, constants.HIGH_HASH),
parse: function(key) {
key = layout.Tt(key);
return key[1];
}
});
}
if (account != null)
return this.getAccountHistoryHashes(account);
return this.keys({
gte: layout.t(constants.NULL_HASH),
@ -1334,17 +1441,26 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) {
* @returns {Promise} - Returns {@link Hash}[].
*/
TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) {
if (account != null) {
return this.keys({
gte: layout.P(account, constants.NULL_HASH),
lte: layout.P(account, constants.HIGH_HASH),
parse: function(key) {
key = layout.Pp(key);
return key[1];
}
});
}
TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(account) {
return this.keys({
gte: layout.P(account, constants.NULL_HASH),
lte: layout.P(account, constants.HIGH_HASH),
parse: function(key) {
key = layout.Pp(key);
return key[1];
}
});
};
/**
* 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(constants.NULL_HASH),
@ -1353,6 +1469,23 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) {
});
};
/**
* 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, constants.NULL_HASH, 0),
lte: layout.C(account, constants.HIGH_HASH, 0xffffffff),
parse: function(key) {
key = layout.Cc(key);
return new Outpoint(key[1], key[2]);
}
});
};
/**
* Get all coin hashes in the database.
* @param {Number?} account
@ -1360,16 +1493,8 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) {
*/
TXDB.prototype.getOutpoints = function getOutpoints(account) {
if (account != null) {
return this.keys({
gte: layout.C(account, constants.NULL_HASH, 0),
lte: layout.C(account, constants.HIGH_HASH, 0xffffffff),
parse: function(key) {
key = layout.Cc(key);
return new Outpoint(key[1], key[2]);
}
});
}
if (account != null)
return this.getAccountOutpoints(account);
return this.keys({
gte: layout.c(constants.NULL_HASH, 0),
@ -1438,6 +1563,33 @@ 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) {
var start = options.start || 0;
var end = options.end || 0xffffffff;
return this.keys({
gte: layout.M(account, start, constants.NULL_HASH),
lte: layout.M(account, end, constants.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: function(key) {
key = layout.Mm(key);
return key[2];
}
});
};
/**
* Get TX hashes by timestamp range.
* @param {Number?} account
@ -1457,22 +1609,12 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) {
account = null;
}
if (account != null)
return this.getAccountRangeHashes(account, options);
start = options.start || 0;
end = options.end || 0xffffffff;
if (account != null) {
return this.keys({
gte: layout.M(account, start, constants.NULL_HASH),
lte: layout.M(account, end, constants.HIGH_HASH),
limit: options.limit,
reverse: options.reverse,
parse: function(key) {
key = layout.Mm(key);
return key[2];
}
});
}
return this.keys({
gte: layout.m(start, constants.NULL_HASH),
lte: layout.m(end, constants.HIGH_HASH),
@ -1586,11 +1728,11 @@ TXDB.prototype.getAccountHistory = co(function* getAccountHistory(account) {
* @returns {Promise} - Returns {@link TX}[].
*/
TXDB.prototype.getUnconfirmed = co(function* getUnconfirmed(account) {
TXDB.prototype.getPending = co(function* getPending(account) {
var txs = [];
var i, hashes, hash, tx;
hashes = yield this.getUnconfirmedHashes(account);
hashes = yield this.getPendingHashes(account);
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
@ -1717,6 +1859,20 @@ TXDB.prototype.fillCoins = co(function* fillCoins(tx) {
return tx;
});
/**
* Get TXDB state.
* @returns {Promise}
*/
TXDB.prototype.getState = co(function* getState() {
var data = yield this.get(layout.R);
if (!data)
return;
return TXDBState.fromRaw(this.wallet.wid, this.wallet.id, data);
});
/**
* Get transaction.
* @param {Hash} hash
@ -1894,19 +2050,23 @@ TXDB.prototype.hasCoin = function hasCoin(hash, index) {
*/
TXDB.prototype.getBalance = co(function* getBalance(account) {
var self = this;
var balance;
// Slow case
if (account != null)
return yield this.getAccountBalance(account);
// Really fast case
if (this.balance)
return this.balance;
// Fast case
balance = new Balance(this.wallet);
return this.balance;
});
/**
* Calculate balance.
* @param {Number?} account
* @returns {Promise} - Returns {@link Balance}.
*/
TXDB.prototype.getWalletBalance = co(function* getWalletBalance(account) {
var self = this;
var balance = new Balance(this.wallet.wid, this.wallet.id, -1);
yield this.range({
gte: layout.c(constants.NULL_HASH, 0x00000000),
@ -1932,7 +2092,7 @@ TXDB.prototype.getBalance = co(function* getBalance(account) {
TXDB.prototype.getAccountBalance = co(function* getBalance(account) {
var prevout = yield this.getOutpoints(account);
var balance = new Balance(this.wallet);
var balance = new Balance(this.wallet.wid, this.wallet.id, account);
var i, ckey, key, coin, op, data;
for (i = 0; i < prevout.length; i++) {
@ -1988,7 +2148,7 @@ TXDB.prototype.zap = co(function* zap(account, age) {
assert(now - tx.ps >= age);
this.wallet.start();
this.start();
this.logger.debug('Zapping TX: %s (%s)',
hash, this.wallet.id);
@ -1996,13 +2156,13 @@ TXDB.prototype.zap = co(function* zap(account, age) {
try {
yield this._remove(hash);
} catch (e) {
this.wallet.drop();
this.drop();
throw e;
}
hashes.push(hash);
yield this.wallet.commit();
yield this.commit();
}
return hashes;
@ -2025,17 +2185,33 @@ TXDB.prototype.abandon = co(function* abandon(hash) {
* Balance
*/
function Balance(wallet) {
function Balance(wid, id, account) {
if (!(this instanceof Balance))
return new Balance(wallet);
return new Balance(wid, id, account);
this.wid = wallet.wid;
this.id = wallet.id;
this.wid = wid;
this.id = id;
this.account = account;
this.unconfirmed = 0;
this.confirmed = 0;
this.total = 0;
}
Balance.prototype.clone = function clone() {
var balance = new Balance(this.wid, this.id, this.account);
balance.unconfirmed = this.unconfirmed;
balance.confirmed = this.confirmed;
balance.total = this.total;
return balance;
};
Balance.prototype.equal = function equal(balance) {
return this.wid === balance.wid
&& this.total === balance.total
&& this.confirmed === balance.confirmed
&& this.unconfirmed === balance.unconfirmed;
};
Balance.prototype.add = function add(coin) {
this.total += coin.value;
if (coin.height === -1)
@ -2076,10 +2252,36 @@ Balance.prototype.addRaw = function addRaw(data) {
this.confirmed += value;
};
Balance.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
p.writeU64(this.unconfirmed);
p.writeU64(this.confirmed);
if (!writer)
p = p.render();
return p;
};
Balance.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.unconfirmed = p.readU53();
this.confirmed = p.readU53();
this.total += this.unconfirmed
this.total += this.confirmed;
return this;
};
Balance.fromRaw = function fromRaw(wid, id, data) {
return new Balance(wid, id, -1).fromRaw(data);
};
Balance.prototype.toJSON = function toJSON() {
return {
wid: this.wid,
id: this.id,
account: this.account,
unconfirmed: utils.btc(this.unconfirmed),
confirmed: utils.btc(this.confirmed),
total: utils.btc(this.total)
@ -2094,6 +2296,85 @@ Balance.prototype.toString = function toString() {
+ '>';
};
/**
* Chain State
* @constructor
*/
function TXDBState(wid, id) {
this.wid = wid;
this.id = id;
this.tx = 0;
this.coin = 0;
this.balance = new Balance(wid, id, -1);
this.committed = false;
}
TXDBState.prototype.clone = function clone() {
var state = new TXDBState();
state.wid = this.wid;
state.id = this.id;
state.tx = this.tx;
state.coin = this.coin;
state.balance = this.balance.clone();
return state;
};
TXDBState.prototype.commit = function commit() {
this.committed = true;
return this.toRaw();
};
TXDBState.prototype.toRaw = function toRaw() {
var p = new BufferWriter();
p.writeU64(this.tx);
p.writeU64(this.coin);
this.balance.toRaw(p);
return p.render();
};
TXDBState.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.tx = p.readU53();
this.coin = p.readU53();
this.balance.fromRaw(p);
return this;
};
TXDBState.fromRaw = function fromRaw(wid, id, data) {
return new TXDBState(wid, id).fromRaw(data);
};
TXDBState.prototype.add = function add(coin) {
this.coin++;
return this.balance.add(coin);
};
TXDBState.prototype.sub = function sub(coin) {
this.coin--;
return this.balance.sub(coin);
};
TXDBState.prototype.confirm = function confirm(value) {
return this.balance.confirm(value);
};
TXDBState.prototype.unconfirm = function unconfirm(value) {
return this.balance.unconfirm(value);
};
TXDBState.prototype.toJSON = function toJSON() {
return {
wid: this.wid,
id: this.id,
tx: this.tx,
coin: this.coin,
unconfirmed: utils.btc(this.balance.unconfirmed),
confirmed: utils.btc(this.balance.confirmed),
total: utils.btc(this.balance.total)
};
};
/*
* Helpers
*/

View File

@ -1482,7 +1482,7 @@ Wallet.prototype._send = co(function* send(options, passphrase) {
*/
Wallet.prototype.resend = co(function* resend() {
var txs = yield this.getUnconfirmed();
var txs = yield this.getPending();
var i;
if (txs.length > 0)
@ -1686,36 +1686,9 @@ Wallet.prototype.syncOutputDepth = co(function* syncOutputDepth(info) {
derived.push(ring);
}
if (derived.length > 0) {
this.db.emit('address', this.id, derived);
this.emit('address', derived);
}
return derived;
});
/**
* Emit balance events after a tx is saved.
* @private
* @param {TX} tx
* @param {PathInfo} info
* @returns {Promise}
*/
Wallet.prototype.updateBalances = co(function* updateBalances() {
var balance;
if (this.db.listeners('balance').length === 0
&& this.listeners('balance').length === 0) {
return;
}
balance = yield this.getBalance();
this.db.emit('balance', this.id, balance);
this.emit('balance', balance);
});
/**
* Get a redeem script or witness script by hash.
* @param {Hash} hash - Can be a ripemd160 or a sha256.
@ -1856,20 +1829,24 @@ Wallet.prototype.add = co(function* add(tx) {
Wallet.prototype._add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
var result;
var result, derived;
this.start();
this.txdb.start();
try {
result = yield this.txdb._add(tx, info);
yield this.syncOutputDepth(info);
yield this.updateBalances();
derived = yield this.syncOutputDepth(info);
} catch (e) {
this.drop();
this.txdb.drop();
throw e;
}
yield this.commit();
yield this.txdb.commit();
if (derived.length > 0) {
this.db.emit('address', this.id, derived);
this.emit('address', derived);
}
return result;
});
@ -2044,9 +2021,9 @@ Wallet.prototype.getCoins = co(function* getCoins(acct) {
* @returns {Promise} - Returns {@link TX}[].
*/
Wallet.prototype.getUnconfirmed = co(function* getUnconfirmed(acct) {
Wallet.prototype.getPending = co(function* getPending(acct) {
var account = yield this.ensureIndex(acct);
return yield this.txdb.getUnconfirmed(account);
return yield this.txdb.getPending(account);
});
/**

View File

@ -207,7 +207,7 @@ WalletDB.prototype._init = function _init() {
WalletDB.prototype._open = co(function* open() {
yield this.db.open();
yield this.db.checkVersion('V', 3);
yield this.db.checkVersion('V', 4);
yield this.writeGenesis();
this.depth = yield this.getDepth();

View File

@ -34,9 +34,7 @@ var updateVersion = co(function* updateVersion() {
console.log('Checking version.');
data = yield db.get('V');
if (!data)
return;
assert(data, 'No version.');
ver = data.readUInt32LE(0, true);