From e31e4855533e9d12feb08154f1dc1f55b8509bfe Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 2 Mar 2016 02:06:57 -0800 Subject: [PATCH] walletdb. tests. refactor. --- lib/bcoin/node.js | 4 +- lib/bcoin/pool.js | 149 ++++------ lib/bcoin/tx.js | 2 +- lib/bcoin/txdb.js | 390 ++++++++++-------------- lib/bcoin/wallet.js | 223 +++++++------- lib/bcoin/walletdb.js | 292 +++++++----------- package.json | 2 +- test/wallet-test.js | 669 +++++++++++++++++++++--------------------- 8 files changed, 786 insertions(+), 945 deletions(-) diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 94beb55f..2f0077f4 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -161,9 +161,7 @@ Fullnode.prototype._init = function _init() { if (!self.miner.address) self.miner.address = wallet.getAddress(); - wallet.on('load', function() { - load(); - }); + load(); }); this.chain.once('load', function() { diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 2968613b..f4d08fec 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -1224,44 +1224,29 @@ Pool.prototype.addWallet = function addWallet(wallet, callback) { if (this.loading) return this.once('load', this.addWallet.bind(this, wallet, callback)); - if (!this.options.spv) { - wallet.getPending().forEach(function(tx) { - self.sendTX(tx); - }); - return utils.nextTick(callback); - } + callback = utils.asyncify(callback); - if (this.wallets.indexOf(wallet) !== -1) - return false; - - this.watchWallet(wallet); - this.wallets.push(wallet); - - function search() { - // Relay pending TXs - // NOTE: It is important to do it after - // search, because search could add TS - // to pending TXs, thus making them - // confirmed. - wallet.getPending().forEach(function(tx) { - self.sendTX(tx); - }); - - if (self._pendingSearch) + if (this.options.spv) { + if (this.wallets.indexOf(wallet) !== -1) return callback(); - self._pendingSearch = true; - - utils.nextTick(function() { - self._pendingSearch = false; - self.searchWallet(callback); - }); + this.watchWallet(wallet); + this.wallets.push(wallet); } - if (wallet.loading) - wallet.once('load', search); - else - search(); + wallet.getPending(function(err, txs) { + if (err) + return callback(err); + + txs.forEach(function(tx) { + self.sendTX(tx); + }); + + if (!self.options.spv) + return callback(); + + self.searchWallet(wallet, callback); + }); }; Pool.prototype.removeWallet = function removeWallet(wallet) { @@ -1303,57 +1288,56 @@ Pool.prototype.unwatchWallet = function unwatchWallet(wallet) { }, this); }; -Pool.prototype.searchWallet = function(ts, height, callback) { +Pool.prototype.searchWallet = function(wallet, callback) { var self = this; var wallet; assert(!this.loading); - if (!this.options.spv) - return; - - if (ts == null || typeof ts === 'function') { - callback = ts; - height = this.wallets.reduce(function(height, wallet) { - if (wallet.lastHeight < height) - return wallet.lastHeight; - return height; - }, Infinity); - assert(height !== Infinity); - ts = this.wallets.reduce(function(ts, wallet) { - if (wallet.lastTs < ts) - return wallet.lastTs; - return ts; - }, Infinity); - assert(ts !== Infinity); - } else if (typeof ts !== 'number') { - callback = height; - wallet = ts; - if (wallet.loading) { - wallet.once('load', function() { - self.searchWallet(wallet); - }); - return; - } - ts = wallet.lastTs; - height = wallet.lastHeight; - } - callback = utils.asyncify(callback); - // Always prefer height - if (height > 0) { - // Back one week - if (!height || height === -1) - height = this.chain.height - (7 * 24 * 6); + if (!this.options.spv) + return callback(); - this.chain.resetHeightAsync(height, function(err) { + wallet.getLast(function(err, ts, height) { + if (err) + return callback(err); + + // Always prefer height + if (height > 0) { + // Back one week + if (!height || height === -1) + height = self.chain.height - (7 * 24 * 6); + + self.chain.resetHeightAsync(height, function(err) { + if (err) { + utils.debug('Failed to reset height: %s', err.stack + ''); + return callback(err); + } + + utils.debug('Wallet height: %s', height); + utils.debug( + 'Reverted chain to height=%d (%s)', + self.chain.height, + new Date(self.chain.tip.ts * 1000) + ); + + callback(); + }); + + return; + } + + if (!ts) + ts = utils.now() - 7 * 24 * 3600; + + self.chain.resetTimeAsync(ts, function(err) { if (err) { - utils.debug('Failed to reset height: %s', err.stack + ''); + utils.debug('Failed to reset time: %s', err.stack + ''); return callback(err); } - utils.debug('Wallet height: %s', height); + utils.debug('Wallet time: %s', new Date(ts * 1000)); utils.debug( 'Reverted chain to height=%d (%s)', self.chain.height, @@ -1362,27 +1346,6 @@ Pool.prototype.searchWallet = function(ts, height, callback) { callback(); }); - - return; - } - - if (!ts) - ts = utils.now() - 7 * 24 * 3600; - - this.chain.resetTimeAsync(ts, function(err) { - if (err) { - utils.debug('Failed to reset time: %s', err.stack + ''); - return callback(err); - } - - utils.debug('Wallet time: %s', new Date(ts * 1000)); - utils.debug( - 'Reverted chain to height=%d (%s)', - self.chain.height, - new Date(self.chain.tip.ts * 1000) - ); - - callback(); }); }; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index d6f6a6b2..c6ecb6fc 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -601,7 +601,7 @@ TX.prototype.fillPrevout = function fillPrevout(txs, unspent) { } else if (txs instanceof bcoin.txpool) { unspent = txs._unspent; txs = txs._all; - } else if (txs instanceof bcoin.wallet) { + } else if (txs instanceof bcoin.wallet && txs.tx) { unspent = txs.tx._unspent; txs = txs.tx._all; } diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 0031b500..525d0a61 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -80,31 +80,36 @@ TXPool.prototype.add = function add(tx, callback) { }; TXPool.prototype._hasAddress = function _hasAddress(address, callback) { - callback = utils.ensure(callback); - if (!address) return callback(null, false); - return callback(null, true); }; // This big scary function is what a persistent tx pool // looks like. It's a semi mempool in that it can handle // receiving txs out of order. -TXPool.prototype._add = function add(tx, callback) { +TXPool.prototype._add = function add(tx, callback, force) { var self = this; var p = this.prefix + '/'; var hash = tx.hash('hex'); var updated = false; var batch; - callback = utils.ensure(callback); + var unlock = this._lock(add, [tx, callback], force); + if (!unlock) + return; - batch = this.db.batch(); + function done(err, result) { + unlock(); + if (callback) + callback(err, result); + }; this.getTX(hash, function(err, existing) { if (err) - return callback(err); + return done(err); + + batch = self.db.batch(); if (existing) { // Tricky - update the tx and coin in storage, @@ -137,10 +142,10 @@ TXPool.prototype._add = function add(tx, callback) { batch.del(p + 'p/a/' + uaddr + '/' + hash); }); - tx.outputs.forEach(function(output) { + utils.forEachSerial(tx.outputs, function(output, next, i) { var type = output.getType(); var address = output.getAddress(); - var uaddr, coinRaw; + var uaddr; if (type === 'pubkey' || type === 'multisig') address = null; @@ -157,28 +162,37 @@ TXPool.prototype._add = function add(tx, callback) { if (uaddr) batch.del(p + 'p/a/' + uaddr + '/' + hash); - coinRaw = bcoin.protocol.framer.coin({ - version: tx.version, - height: tx.height, - value: output.value, - script: output.script, - hash: hash, - index: i, - spent: false - }, true); + self.getCoin(hash, i, function(err, coin) { + if (err) + return next(err); - batch.put(p + 'u/t/' + hash + '/' + i, coinRaw); - }); + if (!coin) + return next(); - batch.write(function(err) { + coin.height = tx.height; + + batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); + + next(); + }); + }, function(err) { if (err) - return callback(err); - self.emit('confirmed', tx); - self.emit('tx', tx); - return callback(null, true); + return done(err); + + batch.write(function(err) { + if (err) + return done(err); + + self.emit('confirmed', tx); + self.emit('tx', tx); + + return done(null, true); + }); }); + + return; } - return callback(null, false); + return done(null, false); } // Consume unspent money or add orphans @@ -199,7 +213,7 @@ TXPool.prototype._add = function add(tx, callback) { // Skip invalid transactions if (!tx.verify(i)) - return callback(null, false); + return done(null, false); updated = true; @@ -226,7 +240,7 @@ TXPool.prototype._add = function add(tx, callback) { // Only add orphans if this input is ours. self._hasAddress(input.getAddress(), function(err, result) { if (err) - return callback(err); + return done(err); if (!result) return next(); @@ -234,15 +248,13 @@ TXPool.prototype._add = function add(tx, callback) { // Add orphan, if no parent transaction is yet known self.db.get(p + 'o/' + key, function(err, orphans) { if (err && err.type !== 'NotFoundError') - return callback(err); - - // orphans = self._addOrphan(orphans, { tx: tx, index: i }); + return done(err); if (orphans) { try { orphans = JSON.parse(orphans.toString('utf8')); } catch (e) { - return callback(e); + return done(e); } } else { orphans = []; @@ -263,32 +275,35 @@ TXPool.prototype._add = function add(tx, callback) { }); }, function(err) { if (err) - return callback(err); + return done(err); // Add unspent outputs or resolve orphans utils.forEachSerial(tx.outputs, function(output, next, i) { // Do not add unspents for outputs that aren't ours. self._hasAddress(output.getAddress(), function(err, result) { + var key, coin; + if (err) - return callback(err); + return done(err); if (!result) return next(); - var coin = bcoin.coin(tx, i); - - var key = hash + '/' + i; + key = hash + '/' + i; + coin = bcoin.coin(tx, i); self.db.get(p + 'o/' + key, function(err, orphans) { var some; if (err && err.type !== 'NotFoundError') - return callback(err); + return done(err); if (orphans) { try { - orphans = JSON.parse(orphans.toString('utf8')).map(function(orphan) { - orphan.tx = bcoin.tx.fromExtended(new Buffer(orphan.tx, 'hex'), true); + orphans = JSON.parse(orphans.toString('utf8')); + orphans = orphans.map(function(orphan) { + var tx = new Buffer(orphan.tx, 'hex'); + orphan.tx = bcoin.tx.fromExtended(tx, true); return orphan; }); } catch (e) { @@ -362,7 +377,7 @@ TXPool.prototype._add = function add(tx, callback) { }); }, function(err) { if (err) - return callback(err); + return done(err); batch.put(p + 't/t/' + hash, tx.toExtended()); if (tx.ts === 0) @@ -376,204 +391,23 @@ TXPool.prototype._add = function add(tx, callback) { batch.write(function(err) { if (err) - return callback(err); + return done(err); self.emit('tx', tx); if (tx.ts !== 0) self.emit('confirmed', tx); - return callback(null, true); + if (updated) + self.emit('updated', tx); + + return done(null, true); }); }); }); }); }; -TXPool.prototype._add_ = function _add(tx, callback) { - var self = this; - var p = this.prefix + '/'; - var hash = tx.hash('hex'); - var uniq = {}; - - callback = utils.ensure(callback); - - this.getTX(hash, function(err, existing) { - var batch; - - if (err) - return callback(err); - - batch = self.db.batch(); - - if (existing) { - // Tricky - update the tx and coin in storage, - // and remove pending flag to mark as confirmed. - if (existing.ts === 0 && tx.ts !== 0) { - batch.put(p + 't/t/' + hash, tx.toExtended()); - batch.del(p + 'p/' + hash); - - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); - var uaddr; - - if (input.isCoinbase()) - return; - - if (type === 'pubkey' || type === 'multisig') - address = null; - - uaddr = address; - - if (uaddr) { - if (!uniq[uaddr]) - uniq[uaddr] = true; - else - uaddr = null; - } - - if (uaddr) - batch.del(p + 'p/a/' + uaddr + '/' + hash); - }); - - tx.outputs.forEach(function(output) { - var type = output.getType(); - var address = output.getAddress(); - var uaddr, coinRaw; - - if (type === 'pubkey' || type === 'multisig') - address = null; - - uaddr = address; - - if (uaddr) { - if (!uniq[uaddr]) - uniq[uaddr] = true; - else - uaddr = null; - } - - if (uaddr) - batch.del(p + 'p/a/' + uaddr + '/' + hash); - - coinRaw = bcoin.protocol.framer.coin({ - version: tx.version, - height: tx.height, - value: output.value, - script: output.script, - hash: hash, - index: i, - spent: false - }, true); - - batch.put(p + 'u/t/' + hash + '/' + i, coinRaw); - }); - - batch.write(function(err) { - if (err) - return callback(err); - self.emit('confirmed', tx); - self.emit('tx', tx); - return callback(null, true); - }); - } - return callback(null, false); - } - - batch.put(p + 't/t/' + hash, tx.toExtended()); - if (tx.ts === 0) - batch.put(p + 'p/' + hash, new Buffer([])); - - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); - var uaddr; - - if (input.isCoinbase()) - return; - - if (type === 'pubkey' || type === 'multisig') - address = null; - - uaddr = address; - - if (uaddr) { - if (!uniq[uaddr]) - uniq[uaddr] = true; - else - uaddr = null; - } - - if (uaddr) { - batch.put(p + 't/a/' + uaddr + '/' + hash, new Buffer([])); - if (tx.ts === 0) - batch.put(p + 'p/a/' + uaddr + '/' + hash, new Buffer([])); - } - - if (address) { - batch.del( - p + 'u/a/' + address - + '/' + input.prevout.hash - + '/' + input.prevout.index); - } - - batch.del(p + 'u/t/' + input.prevout.hash + '/' + input.prevout.index); - }); - - tx.outputs.forEach(function(output, i) { - var type = output.getType(); - var address = output.getAddress(); - var uaddr, coinRaw; - - if (type === 'pubkey' || type === 'multisig') - address = null; - - uaddr = address; - - if (uaddr) { - if (!uniq[uaddr]) - uniq[uaddr] = true; - else - uaddr = null; - } - - coinRaw = bcoin.protocol.framer.coin({ - version: tx.version, - height: tx.height, - value: output.value, - script: output.script, - hash: hash, - index: i, - spent: false - }, true); - - if (uaddr) { - batch.put(p + 't/a/' + uaddr + '/' + hash, new Buffer([])); - if (tx.ts === 0) - batch.put(p + 'p/a/' + uaddr + '/' + hash, new Buffer([])); - } - - if (address) - batch.put(p + 'u/a/' + address + '/' + hash + '/' + i, new Buffer([])); - - batch.put(p + 'u/t/' + hash + '/' + i, coinRaw); - }); - - batch.write(function(err) { - if (err) - return callback(err); - - self.emit('tx', tx); - - if (tx.ts !== 0) - self.emit('confirmed', tx); - - return callback(null, true); - }); - }); -}; - TXPool.prototype.remove = function remove(hash, callback) { var self = this; var p = this.prefix + '/'; @@ -606,7 +440,7 @@ TXPool.prototype.remove = function remove(hash, callback) { tx.inputs.forEach(function(input) { var type = input.getType(); var address = input.getAddress(); - var uaddr, coinRaw; + var uaddr; if (input.isCoinbase()) return; @@ -640,11 +474,10 @@ TXPool.prototype.remove = function remove(hash, callback) { } if (input.output) { - coinRaw = bcoin.protocol.framer.coin(input.output, true); batch.put(p + 'u/t/' + input.prevout.hash + '/' + input.prevout.index, - coinRaw); + input.output.toRaw()); } batch.del(p + 'o/' + input.prevout.hash + '/' + input.prevout.index); @@ -690,6 +523,103 @@ TXPool.prototype.remove = function remove(hash, callback) { }); }; +TXPool.prototype.unconfirm = function unconfirm(hash, callback) { + var self = this; + var p = this.prefix + '/'; + var uniq = {}; + + if (hash.hash) + hash = hash.hash('hex'); + + this.getTX(hash, function(err, tx) { + var batch; + + if (err) + return callback(err); + + if (!tx) + return callback(null, true); + + tx.height = -1; + tx.ps = utils.now(); + tx.ts = 0; + tx.index = -1; + tx.block = null; + + batch.put(p + 't/t/' + hash, tx.toExtended()); + batch.put(p + 'p/' + hash, new Buffer([])); + + tx.inputs.forEach(function(input) { + var type = input.getType(); + var address = input.getAddress(); + var uaddr; + + if (input.isCoinbase()) + return; + + if (type === 'pubkey' || type === 'multisig') + address = null; + + uaddr = address; + + if (uaddr) { + if (!uniq[uaddr]) + uniq[uaddr] = true; + else + uaddr = null; + } + + if (uaddr) + batch.put(p + 'p/a/' + uaddr + '/' + hash, new Buffer([])); + }); + + utils.forEachSerial(tx.outputs, function(output, next, i) { + var type = output.getType(); + var address = output.getAddress(); + var uaddr; + + if (type === 'pubkey' || type === 'multisig') + address = null; + + uaddr = address; + + if (uaddr) { + if (!uniq[uaddr]) + uniq[uaddr] = true; + else + uaddr = null; + } + + if (uaddr) + batch.put(p + 'p/a/' + uaddr + '/' + hash, new Buffer([])); + + self.getCoin(hash, i, function(err, coin) { + if (err) + return next(err); + + if (!coin) + return next(); + + coin.height = tx.height; + + batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); + + next(); + }); + }, function(err) { + if (err) + return callback(err); + + batch.write(function(err) { + if (err) + return callback(err); + self.emit('unconfirmed', tx); + return callback(null, true); + }); + }); + }); +}; + TXPool.prototype.getTXHashes = function getTXHashes(address, callback) { var p = this.prefix + '/'; var self = this; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 4b98d4af..a0f4cf5a 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -44,7 +44,7 @@ function Wallet(options) { options.master = bcoin.hd.fromSeed(); this.options = options; - this.db = options.db || new bcoin.walletdb(); + this.db = options.db; this.addresses = []; this.master = options.master || null; this.addressMap = options.addressMap || {}; @@ -85,10 +85,6 @@ function Wallet(options) { : this.master.deriveAccount44(this.accountIndex); } - this.loading = true; - this.lastTs = 0; - this.lastHeight = 0; - this.addKey(this.accountKey); (options.keys || []).forEach(function(key) { @@ -100,12 +96,13 @@ utils.inherits(Wallet, EventEmitter); Wallet.prototype._init = function _init() { var self = this; - var prevBalance = null; var options = this.options; var addr, i; assert(!this._initialized); + this._initialized = true; + this.id = this.getID(); if (Object.keys(this.addressMap).length === 0) { @@ -128,31 +125,6 @@ Wallet.prototype._init = function _init() { assert(this.receiveAddress); assert(!this.receiveAddress.change); assert(this.changeAddress.change); - - this.tx = this.db.tx; - - this.tx.on('tx', function(tx) { - self.emit('tx', tx); - }); - - this.tx.on('confirmed', function(tx) { - // TX using this address was confirmed. - // Allocate a new address. - self.syncOutputDepth(tx); - self.emit('confirmed', tx); - }); - - this.tx.on('error', function(err) { - self.emit('error', err); - }); - - this.save(function(err) { - if (err) - throw err; - - self.loading = false; - self.emit('load', self.lastTs); - }); }; Wallet.prototype.addKey = function addKey(key) { @@ -355,17 +327,12 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) { address = new bcoin.address(options); this.addressMap[address.getKeyAddress()] = data.path; - this._saveAddress(address.getKeyAddress()); - if (this.type === 'multisig') { + if (this.type === 'multisig') this.addressMap[address.getScriptAddress()] = data.path; - this._saveAddress(address.getScriptAddress()); - } - if (this.witness) { + if (this.witness) this.addressMap[address.getProgramAddress()] = data.path; - this._saveAddress(address.getProgramAddress()); - } this.emit('add address', address); @@ -503,8 +470,6 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { if (tx instanceof bcoin.input) return tx.test(this.addressMap); - this.fillPrevout(tx); - return tx.testInputs(this.addressMap, index); }; @@ -551,14 +516,39 @@ Wallet.prototype.fill = function fill(tx, options, callback) { }; Wallet.prototype.fillPrevout = function fillPrevout(tx, callback) { - return tx.fillPrevout(this); - return this.db.fillTX(tx, callback); + if (!this.db) + return callback(new Error('No wallet db available.')); + + return this.db.fillCoin(tx, callback); }; -Wallet.prototype.createTX = function createTX(options, outputs) { +Wallet.prototype.sync = function sync(callback) { + var self = this; + + callback = utils.ensure(callback); + + this.getUnspent(function(err, unspent) { + if (err) + return callback(err); + + unspent.forEach(function(coin) { + self.syncOutputDepth(coin); + }); + + return callback(); + }); +}; + +Wallet.prototype.tx = function _tx(options, outputs, callback) { + var self = this; var tx = bcoin.mtx(); var target; + if (typeof outputs === 'function') { + callback = outputs; + outputs = null; + } + if (!outputs) { outputs = options; options = {}; @@ -573,28 +563,44 @@ Wallet.prototype.createTX = function createTX(options, outputs) { }); // Fill the inputs with unspents - this.fill(tx, options); + this.fill(tx, options, function(err) { + if (err) + return callback(err); - // Sort members a la BIP69 - tx.sortMembers(); + // Sort members a la BIP69 + tx.sortMembers(); - // Find the necessary locktime if there is - // a checklocktimeverify script in the unspents. - target = tx.getTargetLocktime(); + // Find the necessary locktime if there is + // a checklocktimeverify script in the unspents. + target = tx.getTargetLocktime(); - // No target value. The unspents have an - // incompatible locktime type. - if (!target) - return; + // No target value. The unspents have an + // incompatible locktime type. + if (!target) + return callback(new Error('Incompatible locktime.')); - // Set the locktime to target value or - // `height - whatever` to avoid fee snipping. - if (target.value > 0) - tx.setLocktime(target.value); - else - tx.avoidFeeSniping(); + // Set the locktime to target value or + // `height - whatever` to avoid fee snipping. + if (target.value > 0) + tx.setLocktime(target.value); + else + tx.avoidFeeSniping(); - return tx; + // Sign the transaction + if (!self.sign(tx)) + return callback(new Error('Could not sign transaction.')); + + if (self.m > 1) + return callback(null, tx); + + // Add to pool + self.addTX(tx, function(err) { + if (err) + return callback(err); + + callback(null, tx); + }); + }); }; Wallet.prototype.deriveInputs = function deriveInputs(tx) { @@ -618,6 +624,13 @@ Wallet.prototype.getInputPaths = function getInputPaths(tx) { var paths = []; var i, input, output, address, path; + if (tx instanceof bcoin.input) { + path = this.getPath(tx.output.getAddress()); + if (path) + paths.push(path); + return paths; + } + for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; output = input.output; @@ -639,6 +652,13 @@ Wallet.prototype.getOutputPaths = function getOutputPaths(tx) { var paths = []; var i, output, address, path; + if (tx instanceof bcoin.output) { + path = this.getPath(tx.getAddress()); + if (path) + paths.push(path); + return paths; + } + for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; @@ -678,10 +698,19 @@ Wallet.prototype.getOutputDepth = function getOutputDepth(tx) { Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx) { var depth = this.getOutputDepth(tx); - if (depth.changeDepth >= this.changeDepth) + var res = false; + + if (depth.changeDepth >= this.changeDepth) { this.setChangeDepth(depth.changeDepth + 1); - if (depth.receiveDepth >= this.receiveDepth) + res = true; + } + + if (depth.receiveDepth >= this.receiveDepth) { this.setReceiveDepth(depth.receiveDepth + 1); + res = true; + } + + return res; }; Wallet.prototype.scan = function scan(txByAddress, callback) { @@ -761,11 +790,7 @@ Wallet.prototype._scan = function _scan(options, txByAddress, callback) { }; Wallet.prototype.scriptInputs = function scriptInputs(tx, index) { - var addresses; - - this.fillPrevout(tx); - - addresses = this.deriveInputs(tx); + var addresses = this.deriveInputs(tx); return addresses.reduce(function(total, address) { return total + address.scriptInputs(tx, index); @@ -773,11 +798,7 @@ Wallet.prototype.scriptInputs = function scriptInputs(tx, index) { }; Wallet.prototype.signInputs = function signInputs(tx, type, index) { - var addresses; - - this.fillPrevout(tx); - - addresses = this.deriveInputs(tx); + var addresses = this.deriveInputs(tx); return addresses.reduce(function(total, address) { return total + address.signInputs(tx, type, index); @@ -785,11 +806,7 @@ Wallet.prototype.signInputs = function signInputs(tx, type, index) { }; Wallet.prototype.sign = function sign(tx, type, index) { - var addresses; - - this.fillPrevout(tx); - - addresses = this.deriveInputs(tx); + var addresses = this.deriveInputs(tx); return addresses.reduce(function(total, address) { return total + address.sign(tx, type, index); @@ -797,35 +814,49 @@ Wallet.prototype.sign = function sign(tx, type, index) { }; Wallet.prototype.addTX = function addTX(tx, callback) { + this.syncOutputDepth(tx); + if (!this.db) - return; + return callback(new Error('No wallet db available.')); + return this.db.addTX(tx, callback); }; Wallet.prototype.getAll = function getAll(callback) { if (!this.db) - return; + return callback(new Error('No wallet db available.')); + return this.db.getAll(this, callback); }; Wallet.prototype.getUnspent = function getUnspent(callback) { if (!this.db) - return; + return callback(new Error('No wallet db available.')); + return this.db.getUnspent(this, callback); }; Wallet.prototype.getPending = function getPending(callback) { if (!this.db) - return; + return callback(new Error('No wallet db available.')); + return this.db.getPending(this, callback); }; Wallet.prototype.getBalance = function getBalance(callback) { if (!this.db) - return; + return callback(new Error('No wallet db available.')); + return this.db.getBalance(this, callback); }; +Wallet.prototype.getLast = function getLast(callback) { + if (!this.db) + return callback(new Error('No wallet db available.')); + + return this.db.getLast(this, callback); +}; + Wallet.prototype.__defineGetter__('script', function() { return this.getScript(); }); @@ -901,14 +932,11 @@ Wallet.prototype.toJSON = function toJSON() { addressMap: this.addressMap, keys: this.keys.map(function(key) { return key.xpubkey; - }), - txs: [] + }) }; }; Wallet._fromJSON = function _fromJSON(json, passphrase) { - var wallet; - assert.equal(json.v, 3); assert.equal(json.name, 'wallet'); @@ -929,10 +957,7 @@ Wallet._fromJSON = function _fromJSON(json, passphrase) { master: bcoin.hd.fromJSON(json.master, passphrase), addressMap: json.addressMap, keys: json.keys, - passphrase: passphrase, - txs: json.txs.map(function(json) { - return bcoin.tx.fromCompact(json); - }) + passphrase: passphrase }; }; @@ -940,24 +965,6 @@ Wallet.fromJSON = function fromJSON(json, passphrase) { return new Wallet(Wallet._fromJSON(json, passphrase)); }; -Wallet.prototype._saveAddress = function _saveAddress(address, callback) { - callback = utils.ensure(callback); - - if (!this.db) - return utils.nextTick(callback); - - return this.db.saveAddress(this.id, address, callback); -}; - -Wallet.prototype.save = function save(callback) { - callback = utils.ensure(callback); - - if (!this.db) - return utils.nextTick(callback); - - return this.db.save(this, callback); -}; - /** * Expose */ diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index e0791d95..21b98cc3 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -53,7 +53,7 @@ WalletDB._db = {}; WalletDB.prototype.dump = function dump(callback) { var self = this; - var records = []; + var records = {}; var iter = this.db.db.iterator({ gte: 'w', @@ -83,7 +83,7 @@ WalletDB.prototype.dump = function dump(callback) { }); } - records.push([key, value.slice(0, 200).toString('hex')]); + records[key] = value.slice(0, 50).toString('hex'); next(); }); @@ -134,19 +134,8 @@ WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) { callback = utils.ensure(callback); - if (json instanceof bcoin.wallet) { + if (json instanceof bcoin.wallet) json = json.toJSON(); - } else { - if (typeof json.v !== 'number') { - json = utils.merge({}, json); - delete json.store; - delete json.db; - var save = bcoin.wallet.prototype.save; - bcoin.wallet.prototype.save = function() {}; - json = new bcoin.wallet(json).toJSON(); - bcoin.wallet.prototype.save = save; - } - } function cb(err, json) { var batch; @@ -204,20 +193,6 @@ WalletDB.prototype.removeJSON = function removeJSON(id, callback) { return this._removeDB(id, cb); }; -WalletDB.prototype.createJSON = function createJSON(id, options, callback) { - var self = this; - callback = utils.ensure(callback); - return this.getJSON(id, function(err, json) { - if (err) - return callback(err); - - if (!json) - return self.saveJSON(options.id, options, callback); - - return callback(null, json); - }); -}; - WalletDB.prototype._getDB = function _getDB(id, callback) { var self = this; var key; @@ -295,9 +270,15 @@ WalletDB.prototype.get = function get(id, passphrase, callback) { if (!options) return callback(); - wallet = bcoin.wallet.fromJSON(options, passphrase); - wallet.store = true; - wallet.db = self; + try { + wallet = bcoin.wallet.fromJSON(options, passphrase); + wallet.store = true; + wallet.db = self; + } catch (e) { + return callback(e); + } + + wallet.on('add address', self._onAddress(wallet, wallet.id)); return callback(null, wallet); }); @@ -332,148 +313,82 @@ WalletDB.prototype.create = function create(options, callback) { callback = utils.ensure(callback); - if (options instanceof bcoin.wallet) { - options.store = true; - options.db = this; + function getJSON(id, callback) { + if (!id) + return callback(); + + return self.getJSON(id, function(err, json) { + if (err) + return callback(err); + + return callback(null, json); + }); } - return this.createJSON(options.id, options, function(err, json) { + return getJSON(options.id, function(err, json) { var wallet; if (err) return callback(err); - wallet = bcoin.wallet.fromJSON(json, options.passphrase); - wallet.store = true; - wallet.db = self; - - return callback(null, wallet); - }); -}; - -WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) { - callback = utils.ensure(callback); - this.db.put('w/a/' + address + '/' + id, new Buffer([]), callback); -}; - -WalletDB.prototype.removeAddress = function removeAddress(id, address, callback) { - callback = utils.ensure(callback); - this.db.del('w/a/' + address + '/' + id, callback); -}; - -/* -WalletDB.prototype._getIDs = function _getIDs(address, callback) { - var self = this; - var ids = []; - - var iter = this.db.db.iterator({ - gte: 'w/a/' + address, - lte: 'w/a/' + address + '~', - keys: true, - values: false, - fillCache: false, - keyAsBuffer: false - }); - - callback = utils.ensure(callback); - - (function next() { - iter.next(function(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); + if (json) { + try { + wallet = bcoin.wallet.fromJSON(json, options.passphrase); + wallet.store = true; + wallet.db = self; + } catch (e) { + return callback(e); } + done(); + } else { + options.store = true; + options.db = self; + wallet = new bcoin.wallet(options); + self._saveDB(wallet.id, wallet.toJSON(), done); + } - if (key === undefined) { - return iter.end(function(err) { - if (err) - return callback(err); - return callback(null, ids); - }); - } - - ids.push(key.split('/')[2]); - - next(); - }); - })(); -}; - -WalletDB.prototype.test = function test(addresses, callback) { - var self = this; - - utils.forEachSerial(addresses, function(address, next) { - self._getIDs(address, function(err, ids) { - if (err) - return next(err); - - if (ids.length > 0) - return callback(null, ids); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - return callback(); - }); -}; - -WalletDB.prototype.ownInput = function ownInput(tx, callback) { - var self = this; - var addresses; - - if (tx.getAddress) { - assert(tx instanceof bcoin.input); - addresses = tx.getAddress(); - if (addresses) - addresses = [addresses]; - else - addresses = []; - } else { - addresses = tx.getInputAddresses(); - } - - return this.test(addresses, callback); -}; - -WalletDB.prototype.ownOutput = function ownOutput(tx, callback) { - var self = this; - var addresses; - - if (tx.getAddress) { - assert(tx instanceof bcoin.output); - addresses = tx.getAddress(); - if (addresses) - addresses = [addresses]; - else - addresses = []; - } else { - addresses = tx.getOutputAddresses(); - } - - return this.test(addresses, callback); -}; - -WalletDB.prototype.ownTX = function ownTX(tx, callback) { - var self = this; - return this.ownInput(tx, function(err, input) { - if (err) - return callback(err); - - return self.ownOutput(tx, function(err, output) { + function done(err) { if (err) return callback(err); - if (input || output) - return callback(null, input, output); + wallet.on('add address', self._onAddress(wallet, wallet.id)); - return callback(); - }); + return callback(null, wallet); + } }); }; -*/ + +WalletDB.prototype._onAddress = function _onAddress(wallet, id) { + var self = this; + return function(address) { + var batch = self.db.batch(); + + batch.put( + 'w/a/' + address.getKeyAddress() + '/' + id, + new Buffer([])); + + if (address.type === 'multisig') { + batch.put( + 'w/a/' + address.getScriptAddress() + '/' + id, + new Buffer([])); + } + + if (address.witness) { + batch.put( + 'w/a/' + address.getProgramAddress() + '/' + id, + new Buffer([])); + } + + batch.write(function(err) { + if (err) + self.emit('error', err); + self.saveJSON(wallet.id, wallet.toJSON(), function(err) { + if (err) + self.emit('error', err); + }); + }); + }; +}; WalletDB.prototype.addTX = function addTX(tx, callback) { return this.tx.add(tx, callback); @@ -493,7 +408,36 @@ WalletDB.prototype.getUnspent = function getUnspent(id, callback) { return this.getAddresses(id, function(err, addresses) { if (err) return callback(err); - return self.tx.getUnspentByAddress(addresses, callback); + + return self.tx.getUnspentByAddress(addresses, function(err, unspent) { + if (err) + return callback(err); + + if (unspent.length === 0) + return callback(null, unspent); + + self.get(id, function(err, wallet) { + var res = false; + + if (!wallet) + return callback(null, unspent); + + unspent.forEach(function(coin) { + if (wallet.syncOutputDepth(coin)) + res = true; + }); + + if (!res) + return callback(null, wallet); + + self.save(wallet, function(err) { + if (err) + return callback(err); + + return callback(null, unspent); + }); + }); + }); }); }; @@ -515,6 +459,15 @@ WalletDB.prototype.getBalance = function getBalance(id, callback) { }); }; +WalletDB.prototype.getLast = function getLast(id, callback) { + var self = this; + return this.getAddresses(id, function(err, addresses) { + if (err) + return callback(err); + return self.tx.getLast(addresses, callback); + }); +}; + WalletDB.prototype.getAddresses = function getAddresses(id, callback) { if (typeof id === 'string') return callback(null, [id]); @@ -582,27 +535,6 @@ WalletDB.prototype.getIDs = function _getIDs(address, callback) { })(); }; -WalletDB.prototype.testTX = function test(tx, callback) { - var self = this; - - return callback(null, true); - utils.forEachSerial(tx.getAddresses(), function(address, next) { - self.getIDs(address, function(err, ids) { - if (err) - return next(err); - - if (ids.length > 0) - return callback(null, true); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - return callback(null, false); - }); -}; - WalletDB.prototype.hasAddress = function hasAddress(address, callback) { var self = this; diff --git a/package.json b/package.json index 722516cd..19b248d5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "bin": "./bin/node", "preferGlobal": false, "scripts": { - "test": "mocha --reporter spec test/*-test.js" + "test": "rm -rf ~/.bcoin-test && BCOIN_PREFIX=~/.bcoin-test mocha --reporter spec test/*-test.js" }, "repository": "git://github.com/indutny/bcoin.git", "keywords": [ diff --git a/test/wallet-test.js b/test/wallet-test.js index e9cda03b..b1b9652a 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -23,6 +23,8 @@ var dummyInput = { }; describe('Wallet', function() { + var wdb = new bcoin.walletdb(); + it('should generate new key and address', function() { var w = bcoin.wallet(); var addr = w.getAddress(); @@ -38,54 +40,59 @@ describe('Wallet', function() { assert(!bcoin.address.validate('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc')); }); - function p2pkh(witness, bullshitNesting) { + function p2pkh(witness, bullshitNesting, cb) { var flags = bcoin.protocol.constants.flags.STANDARD_VERIFY_FLAGS; if (witness) flags |= bcoin.protocol.constants.flags.VERIFY_WITNESS; - var w = bcoin.wallet({ witness: witness }); + wdb.create({ witness: witness }, function(err, w) { + assert(!err); - if (witness) - assert(bcoin.address.parse(w.getAddress()).type === 'witnesspubkeyhash'); - else - assert(bcoin.address.parse(w.getAddress()).type === 'pubkeyhash'); + if (witness) + assert(bcoin.address.parse(w.getAddress()).type === 'witnesspubkeyhash'); + else + assert(bcoin.address.parse(w.getAddress()).type === 'pubkeyhash'); - // Input transcation - var src = bcoin.mtx({ - outputs: [{ - value: 5460 * 2, - address: bullshitNesting - ? w.getProgramAddress() - : w.getAddress() - }, { - value: 5460 * 2, - address: bcoin.address.compileData(new Buffer([])) - }] + // Input transcation + var src = bcoin.mtx({ + outputs: [{ + value: 5460 * 2, + address: bullshitNesting + ? w.getProgramAddress() + : w.getAddress() + }, { + value: 5460 * 2, + address: bcoin.address.compileData(new Buffer([])) + }] + }); + + src.addInput(dummyInput); + assert(w.ownOutput(src)); + assert(w.ownOutput(src.outputs[0])); + assert(!w.ownOutput(src.outputs[1])); + + var tx = bcoin.mtx() + .addInput(src, 0) + .addOutput(w.getAddress(), 5460); + + w.sign(tx); + assert(tx.verify(null, true, flags)); + + cb(); }); - src.addInput(dummyInput); - assert(w.ownOutput(src)); - assert(w.ownOutput(src.outputs[0])); - assert(!w.ownOutput(src.outputs[1])); - - var tx = bcoin.mtx() - .addInput(src, 0) - .addOutput(w.getAddress(), 5460); - - w.sign(tx); - assert(tx.verify(null, true, flags)); } - it('should sign/verify pubkeyhash tx', function() { - p2pkh(false, false); + it('should sign/verify pubkeyhash tx', function(cb) { + p2pkh(false, false, cb); }); - it('should sign/verify witnesspubkeyhash tx', function() { - p2pkh(true, false); + it('should sign/verify witnesspubkeyhash tx', function(cb) { + p2pkh(true, false, cb); }); - it('should sign/verify witnesspubkeyhash tx with bullshit nesting', function() { - p2pkh(true, true); + it('should sign/verify witnesspubkeyhash tx with bullshit nesting', function(cb) { + p2pkh(true, true, cb); }); it('should multisign/verify TX', function() { @@ -125,92 +132,94 @@ describe('Wallet', function() { }); it('should have TX pool and be serializable', function(cb) { - bcoin.walletdb().create({ id: 'w' }, function(err, w) { - assert(w); - bcoin.walletdb().create({ id: 'f' }, function(err, f) { - assert(f); - - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); - t1.addInput(dummyInput); - // balance: 51000 - w.sign(t1); - var t2 = bcoin.mtx().addInput(t1, 0) // 50000 - .addOutput(w, 24000) - .addOutput(w, 24000); - // balance: 49000 - w.sign(t2); - var t3 = bcoin.mtx().addInput(t1, 1) // 1000 - .addInput(t2, 0) // 24000 - .addOutput(w, 23000); - // balance: 47000 - w.sign(t3); - var t4 = bcoin.mtx().addInput(t2, 1) // 24000 - .addInput(t3, 0) // 23000 - .addOutput(w, 11000) - .addOutput(w, 11000); - // balance: 22000 - w.sign(t4); - var f1 = bcoin.mtx().addInput(t4, 1) // 11000 - .addOutput(f, 10000); - // balance: 11000 - w.sign(f1); - var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) - .addOutput(w, 500); - // Script inputs but do not sign - w.scriptInputs(fake); - // Fake signature - fake.inputs[0].script[0] = new Buffer([0,0,0,0,0,0,0,0,0]); - // balance: 11000 - - // Just for debugging - t1.hint = 't1'; - t2.hint = 't2'; - t3.hint = 't3'; - t4.hint = 't4'; - f1.hint = 'f1'; - fake.hint = 'fake'; - - // Fake TX should temporarly change output - // w.addTX(fake); - - w.addTX(fake, function(err) { + wdb.create({}, function(err, w) { assert(!err); - w.addTX(t4, function(err) { + wdb.create({}, function(err, f) { assert(!err); - w.getBalance(function(err, balance) { + + // Coinbase + var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); + t1.addInput(dummyInput); + // balance: 51000 + w.sign(t1); + var t2 = bcoin.mtx().addInput(t1, 0) // 50000 + .addOutput(w, 24000) + .addOutput(w, 24000); + // balance: 49000 + w.sign(t2); + var t3 = bcoin.mtx().addInput(t1, 1) // 1000 + .addInput(t2, 0) // 24000 + .addOutput(w, 23000); + // balance: 47000 + w.sign(t3); + var t4 = bcoin.mtx().addInput(t2, 1) // 24000 + .addInput(t3, 0) // 23000 + .addOutput(w, 11000) + .addOutput(w, 11000); + // balance: 22000 + w.sign(t4); + var f1 = bcoin.mtx().addInput(t4, 1) // 11000 + .addOutput(f, 10000); + // balance: 11000 + w.sign(f1); + var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) + .addOutput(w, 500); + // Script inputs but do not sign + w.scriptInputs(fake); + // Fake signature + fake.inputs[0].script[0] = new Buffer([0,0,0,0,0,0,0,0,0]); + // balance: 11000 + + // Just for debugging + t1.hint = 't1'; + t2.hint = 't2'; + t3.hint = 't3'; + t4.hint = 't4'; + f1.hint = 'f1'; + fake.hint = 'fake'; + + // Fake TX should temporarly change output + // w.addTX(fake); + + w.addTX(fake, function(err) { assert(!err); - assert.equal(balance.toString(10), '22500'); - w.addTX(t1, function(err) { + w.addTX(t4, function(err) { + assert(!err); w.getBalance(function(err, balance) { assert(!err); - assert.equal(balance.toString(10), '73000'); - w.addTX(t2, function(err) { - assert(!err); + assert.equal(balance.toString(10), '22500'); + w.addTX(t1, function(err) { w.getBalance(function(err, balance) { assert(!err); - assert.equal(balance.toString(10), '47000'); - w.addTX(t3, function(err) { + assert.equal(balance.toString(10), '73000'); + w.addTX(t2, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); - assert.equal(balance.toString(10), '22000'); - w.addTX(f1, function(err) { + assert.equal(balance.toString(10), '47000'); + w.addTX(t3, function(err) { assert(!err); w.getBalance(function(err, balance) { assert(!err); - assert.equal(balance.toString(10), '11000'); - w.getAll(function(err, txs) { - assert(txs.some(function(tx) { - return tx.hash('hex') === f1.hash('hex'); - })); + assert.equal(balance.toString(10), '22000'); + w.addTX(f1, function(err) { + assert(!err); + w.getBalance(function(err, balance) { + assert(!err); + assert.equal(balance.toString(10), '11000'); + w.getAll(function(err, txs) { + assert(txs.some(function(tx) { + return tx.hash('hex') === f1.hash('hex'); + })); - var w2 = bcoin.wallet.fromJSON(w.toJSON()); - // assert.equal(w2.getBalance().toString(10), '11000'); - // assert(w2.getAll().some(function(tx) { - // return tx.hash('hex') === f1.hash('hex'); - // })); - cb(); + var w2 = bcoin.wallet.fromJSON(w.toJSON()); + // assert.equal(w2.getBalance().toString(10), '11000'); + // assert(w2.getAll().some(function(tx) { + // return tx.hash('hex') === f1.hash('hex'); + // })); + cb(); + }); + }); }); }); }); @@ -223,114 +232,138 @@ describe('Wallet', function() { }); }); }); - }); - }); }); it('should fill tx with inputs', function(cb) { - var w1 = bcoin.wallet(); - var w2 = bcoin.wallet(); - - // Coinbase - var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); - t1.addInput(dummyInput); - - // Fake TX should temporarly change output - w1.addTX(t1, function(err) { + wdb.create({}, function(err, w1) { assert(!err); - - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 5460); - w1.fill(t2, function(err) { + wdb.create({}, function(err, w2) { assert(!err); - w1.sign(t2); - assert(t2.verify()); - assert.equal(t2.getInputValue().toString(10), 16380); - // If change < dust and is added to outputs: - // assert.equal(t2.getOutputValue().toString(10), 6380); - // If change < dust and is added to fee: - assert.equal(t2.getOutputValue().toString(10), 5460); + // Coinbase + var t1 = bcoin.mtx() + .addOutput(w1, 5460) + .addOutput(w1, 5460) + .addOutput(w1, 5460) + .addOutput(w1, 5460); - // Create new transaction - var t3 = bcoin.mtx().addOutput(w2, 15000); - w1.fill(t3, function(err) { - assert(err); - assert.equal(err.requiredFunds.toString(10), 25000); - cb(); + t1.addInput(dummyInput); + + // Fake TX should temporarly change output + w1.addTX(t1, function(err) { + assert(!err); + + // Create new transaction + var t2 = bcoin.mtx().addOutput(w2, 5460); + w1.fill(t2, function(err) { + assert(!err); + w1.sign(t2); + assert(t2.verify()); + + assert.equal(t2.getInputValue().toString(10), 16380); + // If change < dust and is added to outputs: + // assert.equal(t2.getOutputValue().toString(10), 6380); + // If change < dust and is added to fee: + assert.equal(t2.getOutputValue().toString(10), 5460); + + // Create new transaction + var t3 = bcoin.mtx().addOutput(w2, 15000); + w1.fill(t3, function(err) { + assert(err); + assert.equal(err.requiredFunds.toString(10), 25000); + cb(); + }); + }); }); }); }); }); it('should sign multiple inputs using different keys', function(cb) { - var w1 = bcoin.wallet(); - var w2 = bcoin.wallet(); - var to = bcoin.wallet(); - - // Coinbase - var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); - t1.addInput(dummyInput); - // Fake TX should temporarly change output - // Coinbase - var t2 = bcoin.mtx().addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460); - t2.addInput(dummyInput); - // Fake TX should temporarly change output - - w1.addTX(t1, function(err) { + wdb.create({}, function(err, w1) { assert(!err); - w2.addTX(t2, function(err) { + wdb.create({}, function(err, w2) { assert(!err); - - // Create our tx with an output - var tx = bcoin.mtx(); - tx.addOutput(to, 5460); - - var cost = tx.getOutputValue(); - var total = cost.add(new bn(constants.tx.minFee)); - - w1.getUnspent(function(err, unspent1) { + wdb.create({}, function(err, to) { assert(!err); - w2.getUnspent(function(err, unspent2) { + + // Coinbase + var t1 = bcoin.mtx() + .addOutput(w1, 5460) + .addOutput(w1, 5460) + .addOutput(w1, 5460) + .addOutput(w1, 5460); + + t1.addInput(dummyInput); + + // Fake TX should temporarly change output + // Coinbase + var t2 = bcoin.mtx() + .addOutput(w2, 5460) + .addOutput(w2, 5460) + .addOutput(w2, 5460) + .addOutput(w2, 5460); + + t2.addInput(dummyInput); + // Fake TX should temporarly change output + + w1.addTX(t1, function(err) { assert(!err); + w2.addTX(t2, function(err) { + assert(!err); - // Add dummy output (for `left`) to calculate maximum TX size - tx.addOutput(w1, new bn(0)); + // Create our tx with an output + var tx = bcoin.mtx(); + tx.addOutput(to, 5460); - // Add our unspent inputs to sign - tx.addInput(unspent1[0]); - tx.addInput(unspent1[1]); - tx.addInput(unspent2[0]); + var cost = tx.getOutputValue(); + var total = cost.add(new bn(constants.tx.minFee)); - var left = tx.getInputValue().sub(total); - if (left.cmpn(constants.tx.dustThreshold) < 0) { - tx.outputs[tx.outputs.length - 2].value.iadd(left); - left = new bn(0); - } - if (left.cmpn(0) === 0) - tx.outputs.pop(); - else - tx.outputs[tx.outputs.length - 1].value = left; + w1.getUnspent(function(err, unspent1) { + assert(!err); + w2.getUnspent(function(err, unspent2) { + assert(!err); - // Sign transaction - assert.equal(w1.sign(tx), 2); - assert.equal(w2.sign(tx), 1); + // Add dummy output (for `left`) to calculate maximum TX size + tx.addOutput(w1, new bn(0)); - // Verify - assert.equal(tx.verify(), true); + // Add our unspent inputs to sign + tx.addInput(unspent1[0]); + tx.addInput(unspent1[1]); + tx.addInput(unspent2[0]); - // Sign transaction using `inputs` and `off` params. - tx.inputs.length = 0; - tx.addInput(unspent1[1]); - tx.addInput(unspent1[2]); - tx.addInput(unspent2[1]); - assert.equal(w1.sign(tx, 'all'), 2); - assert.equal(w2.sign(tx, 'all'), 1); + var left = tx.getInputValue().sub(total); + if (left.cmpn(constants.tx.dustThreshold) < 0) { + tx.outputs[tx.outputs.length - 2].value.iadd(left); + left = new bn(0); + } + if (left.cmpn(0) === 0) + tx.outputs.pop(); + else + tx.outputs[tx.outputs.length - 1].value = left; - // Verify - assert.equal(tx.verify(), true); + // Sign transaction + assert.equal(w1.sign(tx), 2); + assert.equal(w2.sign(tx), 1); - cb(); + // Verify + assert.equal(tx.verify(), true); + + // Sign transaction using `inputs` and `off` params. + tx.inputs.length = 0; + tx.addInput(unspent1[1]); + tx.addInput(unspent1[2]); + tx.addInput(unspent2[1]); + assert.equal(w1.sign(tx, 'all'), 2); + assert.equal(w2.sign(tx, 'all'), 1); + + // Verify + assert.equal(tx.verify(), true); + + cb(); + }); + }); + }); }); }); }); @@ -344,149 +377,144 @@ describe('Wallet', function() { flags |= bcoin.protocol.constants.flags.VERIFY_WITNESS; // Create 3 2-of-3 wallets with our pubkeys as "shared keys" - var w1 = bcoin.wallet({ + var options = { witness: witness, derivation: 'bip44', type: 'multisig', m: 2, n: 3 - }); + }; - var w2 = bcoin.wallet({ - witness: witness, - derivation: 'bip44', - type: 'multisig', - m: 2, - n: 3 - }); - - var w3 = bcoin.wallet({ - witness: witness, - derivation: 'bip44', - type: 'multisig', - m: 2, - n: 3 - }); - - var receive = bcoin.wallet(); - - w1.addKey(w2); - w1.addKey(w3); - w2.addKey(w1); - w2.addKey(w3); - w3.addKey(w1); - w3.addKey(w2); - - w3 = bcoin.wallet.fromJSON(w3.toJSON()); - - // Our p2sh address - var addr = w1.getAddress(); - - if (witness) - assert(bcoin.address.parse(addr).type === 'witnessscripthash'); - else - assert(bcoin.address.parse(addr).type === 'scripthash'); - - assert.equal(w1.getAddress(), addr); - assert.equal(w2.getAddress(), addr); - assert.equal(w3.getAddress(), addr); - - var paddr = w1.getProgramAddress(); - assert.equal(w1.getProgramAddress(), paddr); - assert.equal(w2.getProgramAddress(), paddr); - assert.equal(w3.getProgramAddress(), paddr); - - // Add a shared unspent transaction to our wallets - var utx = bcoin.mtx(); - if (bullshitNesting) - utx.addOutput({ address: paddr, value: 5460 * 10 }); - else - utx.addOutput({ address: addr, value: 5460 * 10 }); - - utx.addInput(dummyInput); - - assert(w1.ownOutput(utx.outputs[0])); - - // Simulate a confirmation - utx.ps = 0; - utx.ts = 1; - utx.height = 1; - - assert.equal(w1.receiveDepth, 1); - - w1.addTX(utx, function(err) { + wdb.create(utils.merge({}, options), function(err, w1) { assert(!err); - w2.addTX(utx, function(err) { + wdb.create(utils.merge({}, options), function(err, w2) { assert(!err); - w3.addTX(utx, function(err) { + wdb.create(utils.merge({}, options), function(err, w3) { assert(!err); - - assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 1); - - assert(w1.getAddress() !== addr); - addr = w1.getAddress(); - assert.equal(w1.getAddress(), addr); - assert.equal(w2.getAddress(), addr); - assert.equal(w3.getAddress(), addr); - - // Create a tx requiring 2 signatures - var send = bcoin.mtx(); - send.addOutput({ address: receive.getAddress(), value: 5460 }); - assert(!send.verify(null, true, flags)); - w1.fill(send, { m: w1.m, n: w1.n }, function(err) { + wdb.create({}, function(err, receive) { assert(!err); - w1.sign(send); + w1.addKey(w2); + w1.addKey(w3); + w2.addKey(w1); + w2.addKey(w3); + w3.addKey(w1); + w3.addKey(w2); - assert(!send.verify(null, true, flags)); - w2.sign(send); + // w3 = bcoin.wallet.fromJSON(w3.toJSON()); - assert(send.verify(null, true, flags)); + // Our p2sh address + var addr = w1.getAddress(); - assert.equal(w1.changeDepth, 1); - var change = w1.changeAddress.getAddress(); - assert.equal(w1.changeAddress.getAddress(), change); - assert.equal(w2.changeAddress.getAddress(), change); - assert.equal(w3.changeAddress.getAddress(), change); + if (witness) + assert(bcoin.address.parse(addr).type === 'witnessscripthash'); + else + assert(bcoin.address.parse(addr).type === 'scripthash'); + + assert.equal(w1.getAddress(), addr); + assert.equal(w2.getAddress(), addr); + assert.equal(w3.getAddress(), addr); + + var paddr = w1.getProgramAddress(); + assert.equal(w1.getProgramAddress(), paddr); + assert.equal(w2.getProgramAddress(), paddr); + assert.equal(w3.getProgramAddress(), paddr); + + // Add a shared unspent transaction to our wallets + var utx = bcoin.mtx(); + if (bullshitNesting) + utx.addOutput({ address: paddr, value: 5460 * 10 }); + else + utx.addOutput({ address: addr, value: 5460 * 10 }); + + utx.addInput(dummyInput); + + assert(w1.ownOutput(utx.outputs[0])); // Simulate a confirmation - send.ps = 0; - send.ts = 1; - send.height = 1; + utx.ps = 0; + utx.ts = 1; + utx.height = 1; - w1.addTX(send, function(err) { + assert.equal(w1.receiveDepth, 1); + + w1.addTX(utx, function(err) { assert(!err); - w2.addTX(send, function(err) { + w2.addTX(utx, function(err) { assert(!err); - w3.addTX(send, function(err) { + w3.addTX(utx, function(err) { assert(!err); assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 2); + assert.equal(w1.changeDepth, 1); - assert(w1.getAddress() === addr); - assert(w1.changeAddress.getAddress() !== change); - change = w1.changeAddress.getAddress(); - assert.equal(w1.changeAddress.getAddress(), change); - assert.equal(w2.changeAddress.getAddress(), change); - assert.equal(w3.changeAddress.getAddress(), change); - - if (witness) - send.inputs[0].witness[2] = new Buffer([]); - else - send.inputs[0].script[2] = 0; - - assert(!send.verify(null, true, flags)); - assert.equal(send.getFee().toNumber(), 10000); - - w3 = bcoin.wallet.fromJSON(w3.toJSON()); - assert.equal(w3.receiveDepth, 2); - assert.equal(w3.changeDepth, 2); + assert(w1.getAddress() !== addr); + addr = w1.getAddress(); + assert.equal(w1.getAddress(), addr); + assert.equal(w2.getAddress(), addr); assert.equal(w3.getAddress(), addr); - assert.equal(w3.changeAddress.getAddress(), change); - cb(); + // Create a tx requiring 2 signatures + var send = bcoin.mtx(); + send.addOutput({ address: receive.getAddress(), value: 5460 }); + assert(!send.verify(null, true, flags)); + w1.fill(send, { m: w1.m, n: w1.n }, function(err) { + assert(!err); + + w1.sign(send); + + assert(!send.verify(null, true, flags)); + w2.sign(send); + + assert(send.verify(null, true, flags)); + + assert.equal(w1.changeDepth, 1); + var change = w1.changeAddress.getAddress(); + assert.equal(w1.changeAddress.getAddress(), change); + assert.equal(w2.changeAddress.getAddress(), change); + assert.equal(w3.changeAddress.getAddress(), change); + + // Simulate a confirmation + send.ps = 0; + send.ts = 1; + send.height = 1; + + w1.addTX(send, function(err) { + assert(!err); + w2.addTX(send, function(err) { + assert(!err); + w3.addTX(send, function(err) { + assert(!err); + + assert.equal(w1.receiveDepth, 2); + assert.equal(w1.changeDepth, 2); + + assert(w1.getAddress() === addr); + assert(w1.changeAddress.getAddress() !== change); + change = w1.changeAddress.getAddress(); + assert.equal(w1.changeAddress.getAddress(), change); + assert.equal(w2.changeAddress.getAddress(), change); + assert.equal(w3.changeAddress.getAddress(), change); + + if (witness) + send.inputs[0].witness[2] = new Buffer([]); + else + send.inputs[0].script[2] = 0; + + assert(!send.verify(null, true, flags)); + assert.equal(send.getFee().toNumber(), 10000); + + w3 = bcoin.wallet.fromJSON(w3.toJSON()); + assert.equal(w3.receiveDepth, 2); + assert.equal(w3.changeDepth, 2); + assert.equal(w3.getAddress(), addr); + assert.equal(w3.changeAddress.getAddress(), change); + + cb(); + }); + }); + }); + }); }); }); }); @@ -508,28 +536,11 @@ describe('Wallet', function() { multisig(true, true, cb); }); - var coinbase = '010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff2c027156266a24aa21a9edb1e139795984903d6629ddedf3763fb9bc582fd68a46b1f8c7c57f9fbcc7fc900101ffffffff02887d102a0100000023210290dd626747729e1cc445cb9a11cfb7e78ea896db9f5c335e6730491d9ee7474dac0000000000000000266a24aa21a9edb1e139795984903d6629ddedf3763fb9bc582fd68a46b1f8c7c57f9fbcc7fc900120000000000000000000000000000000000000000000000000000000000000000000000000'; - var chash = 'ba0cb2bf1aa19e4643208f7b38798a3deaa3320968d2cb1e42c5802a7baaba99'; - var wpkh = '0100000001fc8f4ccd25b285bcae9f305d2ec3feb79a71384bab0303f810b58089b9c6e084000000006a473044022036548e256acfbc6a77f322d32ae0f11cb20a05a240d72550bda9d8cf169b35e90220303ad1a60d8297a12501dbebc46ec39c7652ac3d75ff394b8d4c3cbdaf3279c7012103f85883e08a3581b636bbafee55f337b6bf4467826a280fda5bf0533368e99b73ffffffff0200ba1dd2050000001976a91443cec67a63867420c0c934ffbbf89f14729304f988acf0cbf01907000000160014e7b8143685eb4eb03810c8ffb7c4a74d5f23161c00000000'; - var whash = 'a72943c0131d655ff3d272f202d4f6ad2cf378eba9416c9b8028920d71d8f90a'; - var w2hash = 'c532af06b9a81d9171618fb0b30075ddb3a6fca68c9b89536e6e34b0beddcc23'; - - // https://segnet.smartbit.com.au/tx/c532af06b9a81d9171618fb0b30075ddb3a6fca68c9b89536e6e34b0beddcc23 - var w2pkh = new Buffer(bcoin.fs.readFileSync(__dirname + '/wtx.hex', 'ascii').trim(), 'hex'); - - it('should have a wtxid', function(cb) { - var src = bcoin.mtx({ - outputs: [{ - value: 5460 * 2, - address: bcoin.address.compileData(new Buffer([])) - }] + it('should have gratuitous dump', function(cb) { + bcoin.walletdb().dump(function(err, records) { + assert(!err); + console.log(records); + setTimeout(cb, 200); }); - src.addInput(dummyInput); - var t = bcoin.protocol.parser.parseWitnessTX(new Buffer(coinbase, 'hex')); - var t = new bcoin.tx(bcoin.protocol.parser.parseWitnessTX(new Buffer(w2pkh, 'hex'))); - delete t._raw; - delete t._hash; - delete t._whash; - cb(); }); });