diff --git a/lib/wallet/pathinfo.js b/lib/wallet/pathinfo.js new file mode 100644 index 00000000..10374765 --- /dev/null +++ b/lib/wallet/pathinfo.js @@ -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 + }; +}; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 332ef08c..23d38e31 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -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); }; /** diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 42c691c3..8206d6b5 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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 diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 8c2298b6..30a1e5f0 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -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; } /*