wallet: path handling.

This commit is contained in:
Christopher Jeffrey 2016-10-03 23:45:03 -07:00
parent 9a3e3fba3a
commit aa8f9fdf90
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 377 additions and 396 deletions

296
lib/wallet/pathinfo.js Normal file
View File

@ -0,0 +1,296 @@
var utils = require('../utils/utils');
/**
* Path Info
* @constructor
* @param {WalletDB} db
* @param {WalletID} wid
* @param {TX} tx
* @param {Object} table
*/
function PathInfo(wallet, tx, paths) {
if (!(this instanceof PathInfo))
return new PathInfo(wallet, tx, paths);
// All relevant Accounts for
// inputs and outputs (for database indexing).
this.accounts = [];
// All output paths (for deriving during sync).
this.paths = [];
// Wallet
this.wallet = wallet;
// Wallet ID
this.wid = wallet.wid;
// Wallet Label
this.id = wallet.id;
// Map of address hashes->paths.
this.pathMap = {};
// Current transaction.
this.tx = null;
// Wallet-specific details cache.
this._details = null;
this._json = null;
if (tx)
this.fromTX(tx, paths);
}
/**
* Instantiate path info from a transaction.
* @private
* @param {TX} tx
* @param {Object} table
* @returns {PathInfo}
*/
PathInfo.prototype.fromTX = function fromTX(tx, paths) {
var uniq = {};
var i, j, hashes, hash, paths, path;
this.tx = tx;
for (i = 0; i < paths.length; i++) {
path = paths[i];
this.pathMap[path.hash] = path;
if (!uniq[path.account]) {
uniq[path.account] = true;
this.accounts.push(path.account);
}
}
hashes = tx.getOutputHashes('hex');
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
paths = this.pathMap[hash];
this.paths.push(path);
}
return this;
};
/**
* Instantiate path info from a transaction.
* @param {WalletDB} db
* @param {WalletID} wid
* @param {TX} tx
* @param {Object} table
* @returns {PathInfo}
*/
PathInfo.fromTX = function fromTX(wallet, tx, paths) {
return new PathInfo(wallet).fromTX(tx, paths);
};
/**
* Test whether the map has paths
* for a given address hash.
* @param {Hash} hash
* @returns {Boolean}
*/
PathInfo.prototype.hasPath = function hasPath(hash) {
if (!hash)
return false;
return this.pathMap[hash] != null;
};
/**
* Get path for a given address hash.
* @param {Hash} hash
* @returns {Path}
*/
PathInfo.prototype.getPath = function getPath(hash) {
if (!hash)
return;
return this.pathMap[hash];
};
/**
* Convert path info to transaction details.
* @returns {Details}
*/
PathInfo.prototype.toDetails = function toDetails() {
var details = this._details;
if (!details) {
details = new Details(this);
this._details = details;
}
return details;
};
/**
* Convert path info to JSON details (caches json).
* @returns {Object}
*/
PathInfo.prototype.toJSON = function toJSON() {
var json = this._json;
if (!json) {
json = this.toDetails().toJSON();
this._json = json;
}
return json;
};
module.exports = PathInfo;
/**
* Transaction Details
* @constructor
* @param {PathInfo} info
*/
function Details(info) {
if (!(this instanceof Details))
return new Details(info);
this.db = info.wallet.db;
this.network = this.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.getFee();
this.ts = info.tx.ts;
this.ps = info.tx.ps;
this.tx = info.tx;
this.inputs = [];
this.outputs = [];
this.init(info.pathMap);
}
/**
* Initialize transactions details
* by pushing on mapped members.
* @private
* @param {Object} table
*/
Details.prototype.init = function init(map) {
this._insert(this.tx.inputs, true, this.inputs, map);
this._insert(this.tx.outputs, false, this.outputs, map);
};
/**
* Insert members in the input or output vector.
* @private
* @param {Input[]|Output[]} vector
* @param {Array} target
* @param {Object} table
*/
Details.prototype._insert = function _insert(vector, input, target, map) {
var i, j, io, address, hash, paths, path, member;
for (i = 0; i < vector.length; i++) {
io = vector[i];
member = new DetailsMember();
if (input) {
if (io.coin)
member.value = io.coin.value;
} else {
member.value = io.value;
}
address = io.getAddress();
if (address) {
member.address = address;
hash = address.getHash('hex');
path = map[hash];
if (path)
member.path = path;
}
target.push(member);
}
};
/**
* Convert details to a more json-friendly object.
* @returns {Object}
*/
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')
};
};
/**
* Transaction Details Member
* @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.
* @param {Network} network
* @returns {Object}
*/
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
};
};

View File

@ -403,7 +403,7 @@ TXDB.prototype.commit = co(function* commit() {
*/
TXDB.prototype.getInfo = function getInfo(tx) {
return this.walletdb.getPathInfo(this.wallet, tx);
return this.wallet.getPathInfo(tx);
};
/**

View File

@ -28,6 +28,7 @@ var MasterKey = require('./masterkey');
var Input = require('../primitives/input');
var Output = require('../primitives/output');
var LRU = require('../utils/lru');
var PathInfo = require('./pathinfo');
/**
* BIP44 Wallet
@ -1823,6 +1824,37 @@ Wallet.prototype.addTX = function addTX(tx) {
return this.db.addTX(tx);
};
Wallet.prototype.add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
yield this.txdb.add(tx, info);
yield this.handleTX(info);
});
Wallet.prototype.unconfirm = co(function* unconfirm(hash) {
return yield this.txdb.unconfirm(hash);
});
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
* @returns {Promise} - Returns {@link PathInfo}.
*/
Wallet.prototype.getPathInfo = co(function* getPathInfo(tx) {
var hashes = tx.getHashes('hex');
var paths = [];
var i, hash, path;
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
path = yield this.getPath(hash);
if (path)
paths.push(path);
}
return new PathInfo(this, tx, paths);
});
/**
* Get all transactions in transaction history (accesses db).
* @param {(String|Number)?} acct

View File

@ -25,6 +25,7 @@ var ldb = require('../db/ldb');
var Bloom = require('../utils/bloom');
var Logger = require('../node/logger');
var TX = require('../primitives/tx');
var PathInfo = require('./pathinfo');
/*
* Database Layout:
@ -1163,38 +1164,14 @@ WalletDB.prototype.resend = co(function* resend() {
});
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
* @returns {Promise} - Returns {@link PathInfo[}].
*/
WalletDB.prototype.mapWallets = co(function* mapWallets(tx) {
var hashes = tx.getHashes('hex');
var wallets = yield this.getWalletsByHashes(hashes);
var info = [];
var i, wallets, item;
if (wallets.length === 0)
return;
for (i = 0; i < wallets.length; i++) {
item = wallets[i];
info.push(new PathInfo(item.wallet, tx, item.matches));
}
return info;
});
/**
* Get all wallets by multiple address hashes.
* Get all wallet ids by multiple address hashes.
* @param {Hash[]} hashes
* @returns {Promise}
*/
WalletDB.prototype.getWalletsByHashes = co(function* getWalletsByHashes(hashes) {
var map = {};
WalletDB.prototype.getWidsByHashes = co(function* getWidsByHashes(hashes) {
var result = [];
var i, j, hash, wids, wid, wallet, item, path;
var i, j, hash, wids, wid;
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
@ -1204,61 +1181,13 @@ WalletDB.prototype.getWalletsByHashes = co(function* getWalletsByHashes(hashes)
wids = yield this.getWalletsByHash(hash);
for (j = 0; j < wids.length; j++) {
wid = wids[j];
item = map[wid];
if (item) {
wallet = item.wallet;
path = yield wallet.getPath(hash);
assert(path);
item.matches.push(path);
continue;
}
wallet = yield this.get(wid);
assert(wallet);
path = yield wallet.getPath(hash);
assert(path);
item = new WalletMatch(wallet);
item.matches.push(path);
map[wid] = item;
result.push(item);
}
for (j = 0; j < wids.length; j++)
utils.binaryInsert(result, wids[j], compare, true);
}
return result;
});
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
* @returns {Promise} - Returns {@link PathInfo}.
*/
WalletDB.prototype.getPathInfo = co(function* getPathInfo(wallet, tx) {
var hashes = tx.getHashes('hex');
var paths = [];
var i, hash, path;
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
if (!this.testFilter(hash))
continue;
path = yield wallet.getPath(hash);
if (path)
paths.push(path);
}
return new PathInfo(wallet, tx, paths);
});
/**
* Write the genesis block as the best hash.
* @returns {Promise}
@ -1324,7 +1253,7 @@ WalletDB.prototype.writeBlock = function writeBlock(block, matches) {
for (i = 0; i < block.hashes.length; i++) {
hash = block.hashes[i];
wallets = matches[i];
batch.put(layout.e(hash), serializeInfo(wallets));
batch.put(layout.e(hash), serializeWallets(wallets));
}
return batch.write();
@ -1422,7 +1351,7 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) {
for (i = 0; i < txs.length; i++) {
tx = txs[i];
wallets = yield this._addTX(tx);
wallets = yield this._add(tx);
if (!wallets)
continue;
@ -1488,20 +1417,7 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
for (i = 0; i < block.hashes.length; i++) {
hash = block.hashes[i];
wallets = yield this.getWalletsByTX(hash);
if (!wallets)
continue;
for (j = 0; j < wallets.length; j++) {
wid = wallets[j];
wallet = yield this.get(wid);
if (!wallet)
continue;
yield wallet.txdb.unconfirm(hash);
}
yield this._unconfirm(hash);
}
this.tip = block.hash;
@ -1516,10 +1432,11 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
* @returns {Promise}
*/
WalletDB.prototype.addTX = co(function* addTX(tx, force) {
WalletDB.prototype.addTX =
WalletDB.prototype.add = co(function* add(tx) {
var unlock = yield this.txLock.lock();
try {
return yield this._addTX(tx);
return yield this._add(tx);
} finally {
unlock();
}
@ -1532,17 +1449,18 @@ WalletDB.prototype.addTX = co(function* addTX(tx, force) {
* @returns {Promise}
*/
WalletDB.prototype._addTX = co(function* addTX(tx, force) {
var i, wallets, info, wallet;
WalletDB.prototype._add = co(function* add(tx) {
var i, hashes, wallets, wid, wallet;
assert(!tx.mutable, 'Cannot add mutable TX to wallet.');
// Atomicity doesn't matter here. If we crash,
// the automatic rescan will get the database
// back in the correct state.
wallets = yield this.mapWallets(tx);
hashes = tx.getHashes('hex');
wallets = yield this.getWidsByHashes(hashes);
if (!wallets)
if (wallets.length === 0)
return;
this.logger.info(
@ -1550,313 +1468,61 @@ WalletDB.prototype._addTX = co(function* addTX(tx, force) {
wallets.length, tx.rhash);
for (i = 0; i < wallets.length; i++) {
info = wallets[i];
wallet = info.wallet;
wid = wallets[i];
wallet = yield this.get(wid);
if (!wallet)
continue;
this.logger.debug('Adding tx to wallet: %s', info.id);
this.logger.debug('Adding tx to wallet: %s', wallet.id);
yield wallet.txdb.add(tx, info);
yield wallet.handleTX(info);
yield wallet.add(tx);
}
return wallets;
});
/**
* Path Info
* @constructor
* @param {WalletDB} db
* @param {WalletID} wid
* Add a transaction to the database, map addresses
* to wallet IDs, potentially store orphans, resolve
* orphans, or confirm a transaction.
* @param {TX} tx
* @param {Object} table
* @returns {Promise}
*/
function PathInfo(wallet, tx, paths) {
if (!(this instanceof PathInfo))
return new PathInfo(wallet, tx, paths);
// All relevant Accounts for
// inputs and outputs (for database indexing).
this.accounts = [];
// All output paths (for deriving during sync).
this.paths = [];
// Wallet
this.wallet = wallet;
// Wallet ID
this.wid = wallet.wid;
// Wallet Label
this.id = wallet.id;
// Map of address hashes->paths.
this.pathMap = {};
// Current transaction.
this.tx = null;
// Wallet-specific details cache.
this._details = null;
this._json = null;
if (tx)
this.fromTX(tx, paths);
}
WalletDB.prototype.unconfirm = co(function* unconfirm(hash) {
var unlock = yield this.txLock.lock();
try {
return yield this._unconfirm(tx);
} finally {
unlock();
}
});
/**
* Instantiate path info from a transaction.
* Add a transaction to the database without a lock.
* @private
* @param {TX} tx
* @param {Object} table
* @returns {PathInfo}
* @returns {Promise}
*/
PathInfo.prototype.fromTX = function fromTX(tx, paths) {
var uniq = {};
var i, j, hashes, hash, paths, path;
WalletDB.prototype._unconfirm = co(function* unconfirm(hash) {
var wallets = yield this.getWalletsByTX(hash);
var i, wid, wallet;
this.tx = tx;
for (i = 0; i < paths.length; i++) {
path = paths[i];
this.pathMap[path.hash] = path;
if (!uniq[path.account]) {
uniq[path.account] = true;
this.accounts.push(path.account);
}
}
hashes = tx.getOutputHashes('hex');
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
paths = this.pathMap[hash];
this.paths.push(path);
}
return this;
};
/**
* Instantiate path info from a transaction.
* @param {WalletDB} db
* @param {WalletID} wid
* @param {TX} tx
* @param {Object} table
* @returns {PathInfo}
*/
PathInfo.fromTX = function fromTX(wallet, tx, paths) {
return new PathInfo(wallet).fromTX(tx, paths);
};
/**
* Test whether the map has paths
* for a given address hash.
* @param {Hash} hash
* @returns {Boolean}
*/
PathInfo.prototype.hasPath = function hasPath(hash) {
if (!hash)
return false;
return this.pathMap[hash] != null;
};
/**
* Get path for a given address hash.
* @param {Hash} hash
* @returns {Path}
*/
PathInfo.prototype.getPath = function getPath(hash) {
if (!hash)
if (!wallets)
return;
return this.pathMap[hash];
};
for (i = 0; i < wallets.length; i++) {
wid = wallets[i];
wallet = yield this.get(wid);
/**
* Convert path info to transaction details.
* @returns {Details}
*/
if (!wallet)
continue;
PathInfo.prototype.toDetails = function toDetails() {
var details = this._details;
if (!details) {
details = new Details(this);
this._details = details;
yield wallet.unconfirm(hash);
}
return details;
};
/**
* Convert path info to JSON details (caches json).
* @returns {Object}
*/
PathInfo.prototype.toJSON = function toJSON() {
var json = this._json;
if (!json) {
json = this.toDetails().toJSON();
this._json = json;
}
return json;
};
/**
* Transaction Details
* @constructor
* @param {PathInfo} info
*/
function Details(info) {
if (!(this instanceof Details))
return new Details(info);
this.db = info.wallet.db;
this.network = this.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.getFee();
this.ts = info.tx.ts;
this.ps = info.tx.ps;
this.tx = info.tx;
this.inputs = [];
this.outputs = [];
this.init(info.pathMap);
}
/**
* Initialize transactions details
* by pushing on mapped members.
* @private
* @param {Object} table
*/
Details.prototype.init = function init(map) {
this._insert(this.tx.inputs, true, this.inputs, map);
this._insert(this.tx.outputs, false, this.outputs, map);
};
/**
* Insert members in the input or output vector.
* @private
* @param {Input[]|Output[]} vector
* @param {Array} target
* @param {Object} table
*/
Details.prototype._insert = function _insert(vector, input, target, map) {
var i, j, io, address, hash, paths, path, member;
for (i = 0; i < vector.length; i++) {
io = vector[i];
member = new DetailsMember();
if (input) {
if (io.coin)
member.value = io.coin.value;
} else {
member.value = io.value;
}
address = io.getAddress();
if (address) {
member.address = address;
hash = address.getHash('hex');
path = map[hash];
if (path)
member.path = path;
}
target.push(member);
}
};
/**
* Convert details to a more json-friendly object.
* @returns {Object}
*/
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')
};
};
/**
* Transaction Details Member
* @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.
* @param {Network} network
* @returns {Object}
*/
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
};
};
});
/**
* Wallet Block
@ -2038,21 +1704,8 @@ function serializeWallets(wallets) {
return p.render();
}
function serializeInfo(wallets) {
var p = new BufferWriter();
var i, info;
for (i = 0; i < wallets.length; i++) {
info = wallets[i];
p.writeU32(info.wid);
}
return p.render();
}
function WalletMatch(wallet) {
this.wallet = wallet;
this.matches = [];
function compare(a, b) {
return a - b;
}
/*