From df5228c8497e16e63509da4b892281b506208b95 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 30 Jan 2017 18:37:28 -0800 Subject: [PATCH] test: node/chain tests. add memwallet. --- test/chain-test.js | 92 ++++----- test/node-test.js | 451 +++++++++++++++++++++++++++++++++++++++++ test/util/memwallet.js | 396 ++++++++++++++++++++++++++++++++++++ 3 files changed, 890 insertions(+), 49 deletions(-) create mode 100644 test/node-test.js create mode 100644 test/util/memwallet.js diff --git a/test/chain-test.js b/test/chain-test.js index 13d6c201..b6f866ef 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -6,21 +6,17 @@ var consensus = require('../lib/protocol/consensus'); var co = require('../lib/utils/co'); var Coin = require('../lib/primitives/coin'); var Script = require('../lib/script/script'); -var FullNode = require('../lib/node/fullnode'); +var Chain = require('../lib/blockchain/chain'); +var Miner = require('../lib/mining/miner'); var MTX = require('../lib/primitives/mtx'); -// var Client = require('../lib/wallet/client'); +var MemWallet = require('./util/memwallet'); +var util = require('../lib/utils/util'); describe('Chain', function() { - var node = new FullNode({ db: 'memory', apiKey: 'foo', network: 'regtest' }); - var chain = node.chain; - var walletdb = node.walletdb; - var miner = node.miner; - var wallet, tip1, tip2, cb1, cb2, mineBlock; - - // walletdb.client = new Client({ apiKey: 'foo', network: 'regtest' }); - walletdb.options.resolution = false; - - node.on('error', function() {}); + var chain = new Chain({ db: 'memory', network: 'regtest' }); + var miner = new Miner({ chain: chain }); + var wallet = new MemWallet({ network: 'regtest' }); + var tip1, tip2, cb1, cb2, mineBlock; this.timeout(5000); @@ -40,21 +36,28 @@ describe('Chain', function() { rtx.setLocktime(chain.height); - yield wallet.sign(rtx); + wallet.sign(rtx); attempt.addTX(rtx.toTX(), rtx.view); return yield attempt.mineAsync(); }); + chain.on('connect', function(entry, block) { + wallet.addBlock(entry, block.txs); + }); + + chain.on('disconnect', function(entry, block) { + wallet.removeBlock(entry, block.txs); + }); + it('should open chain and miner', co(function* () { - miner.mempool = null; consensus.COINBASE_MATURITY = 0; - yield node.open(); + yield chain.open(); + yield miner.open(); })); - it('should open walletdb', co(function* () { - wallet = yield walletdb.create(); + it('should open wallet', co(function* () { miner.addresses.length = 0; miner.addAddress(wallet.getReceive()); })); @@ -100,19 +103,15 @@ describe('Chain', function() { }); it('should have correct balance', co(function* () { - var balance; - - yield co.timeout(100); - - balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 550 * 1e8); - assert.equal(balance.confirmed, 550 * 1e8); + assert.equal(wallet.balance, 550 * 1e8); + //assert.equal(wallet.unconfirmed, 550 * 1e8); + //assert.equal(wallet.confirmed, 550 * 1e8); })); it('should handle a reorg', co(function* () { var entry, block, forked; - assert.equal(walletdb.state.height, chain.height); + // assert.equal(wallet.height, chain.height); assert.equal(chain.height, 11); entry = yield chain.db.getEntry(tip2.hash); @@ -141,13 +140,9 @@ describe('Chain', function() { }); it('should have correct balance', co(function* () { - var balance; - - yield co.timeout(100); - - balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 1100 * 1e8); - assert.equal(balance.confirmed, 600 * 1e8); + assert.equal(wallet.balance, 600 * 1e8); + // assert.equal(wallet.unconfirmed, 1100 * 1e8); + // assert.equal(wallet.confirmed, 600 * 1e8); })); it('should check main chain', co(function* () { @@ -225,21 +220,20 @@ describe('Chain', function() { })); it('should get balance', co(function* () { - var balance, txs; + var txs; - yield co.timeout(100); + assert.equal(wallet.balance, 750 * 1e8); + // assert.equal(wallet.unconfirmed, 1250 * 1e8); + // assert.equal(wallet.confirmed, 750 * 1e8); - balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 1250 * 1e8); - assert.equal(balance.confirmed, 750 * 1e8); + assert(wallet.receiveDepth >= 7); + assert(wallet.changeDepth >= 6); - assert(wallet.account.receiveDepth >= 7); - assert(wallet.account.changeDepth >= 6); + // assert.equal(wallet.height, chain.height); - assert.equal(walletdb.state.height, chain.height); - - txs = yield wallet.getHistory(); - assert.equal(txs.length, 45); + // txs = wallet.getHistory(); + // assert.equal(txs.length, 45); + assert.equal(wallet.txs, 26); })); it('should get tips and remove chains', co(function* () { @@ -259,7 +253,7 @@ describe('Chain', function() { it('should rescan for transactions', co(function* () { var total = 0; - yield chain.db.scan(0, walletdb.filter, function(block, txs) { + yield chain.db.scan(0, wallet.filter, function(block, txs) { total += txs.length; return Promise.resolve(); }); @@ -324,7 +318,7 @@ describe('Chain', function() { redeemer.setLocktime(chain.height); - yield wallet.sign(redeemer); + wallet.sign(redeemer); attempt.addTX(redeemer.toTX(), redeemer.view); @@ -439,13 +433,13 @@ describe('Chain', function() { assert.equal(err.reason, 'bad-txns-nonfinal'); })); - it('should rescan for transactions', co(function* () { - yield walletdb.rescan(0); - assert.equal(wallet.txdb.state.confirmed, 1289250000000); + it('should have correct wallet balance', co(function* () { + assert.equal(wallet.balance, 1289250000000); })); it('should cleanup', co(function* () { consensus.COINBASE_MATURITY = 100; - yield node.close(); + yield miner.close(); + yield chain.close(); })); }); diff --git a/test/node-test.js b/test/node-test.js new file mode 100644 index 00000000..8a3ce2c5 --- /dev/null +++ b/test/node-test.js @@ -0,0 +1,451 @@ +'use strict'; + +var assert = require('assert'); +var BN = require('bn.js'); +var consensus = require('../lib/protocol/consensus'); +var co = require('../lib/utils/co'); +var Coin = require('../lib/primitives/coin'); +var Script = require('../lib/script/script'); +var FullNode = require('../lib/node/fullnode'); +var MTX = require('../lib/primitives/mtx'); +// var Client = require('../lib/wallet/client'); + +describe('Node', function() { + var node = new FullNode({ db: 'memory', apiKey: 'foo', network: 'regtest' }); + var chain = node.chain; + var walletdb = node.walletdb; + var miner = node.miner; + var wallet, tip1, tip2, cb1, cb2, mineBlock; + + // walletdb.client = new Client({ apiKey: 'foo', network: 'regtest' }); + walletdb.options.resolution = false; + + node.on('error', function() {}); + + this.timeout(5000); + + mineBlock = co(function* mineBlock(tip, tx) { + var attempt = yield miner.createBlock(tip); + var rtx; + + if (!tx) + return yield attempt.mineAsync(); + + rtx = new MTX(); + + rtx.addTX(tx, 0); + + rtx.addOutput(wallet.getReceive(), 25 * 1e8); + rtx.addOutput(wallet.getChange(), 5 * 1e8); + + rtx.setLocktime(chain.height); + + yield wallet.sign(rtx); + + attempt.addTX(rtx.toTX(), rtx.view); + + return yield attempt.mineAsync(); + }); + + it('should open chain and miner', co(function* () { + miner.mempool = null; + consensus.COINBASE_MATURITY = 0; + yield node.open(); + })); + + it('should open walletdb', co(function* () { + wallet = yield walletdb.create(); + miner.addresses.length = 0; + miner.addAddress(wallet.getReceive()); + })); + + it('should mine a block', co(function* () { + var block = yield miner.mineBlock(); + assert(block); + yield chain.add(block); + })); + + it('should mine competing chains', co(function* () { + var i, block1, block2; + + for (i = 0; i < 10; i++) { + block1 = yield mineBlock(tip1, cb1); + cb1 = block1.txs[0]; + + block2 = yield mineBlock(tip2, cb2); + cb2 = block2.txs[0]; + + yield chain.add(block1); + + yield chain.add(block2); + + assert(chain.tip.hash === block1.hash('hex')); + + tip1 = yield chain.db.getEntry(block1.hash('hex')); + tip2 = yield chain.db.getEntry(block2.hash('hex')); + + assert(tip1); + assert(tip2); + + assert(!(yield tip2.isMainChain())); + + yield co.wait(); + } + })); + + it('should have correct chain value', function() { + assert.equal(chain.db.state.value, 55000000000); + assert.equal(chain.db.state.coin, 20); + assert.equal(chain.db.state.tx, 21); + }); + + it('should have correct balance', co(function* () { + var balance; + + yield co.timeout(100); + + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 550 * 1e8); + assert.equal(balance.confirmed, 550 * 1e8); + })); + + it('should handle a reorg', co(function* () { + var entry, block, forked; + + assert.equal(walletdb.state.height, chain.height); + assert.equal(chain.height, 11); + + entry = yield chain.db.getEntry(tip2.hash); + assert(entry); + assert(chain.height === entry.height); + + block = yield miner.mineBlock(entry); + assert(block); + + forked = false; + chain.once('reorganize', function() { + forked = true; + }); + + yield chain.add(block); + + assert(forked); + assert(chain.tip.hash === block.hash('hex')); + assert(chain.tip.chainwork.cmp(tip1.chainwork) > 0); + })); + + it('should have correct chain value', function() { + assert.equal(chain.db.state.value, 60000000000); + assert.equal(chain.db.state.coin, 21); + assert.equal(chain.db.state.tx, 22); + }); + + it('should have correct balance', co(function* () { + var balance; + + yield co.timeout(100); + + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 1100 * 1e8); + assert.equal(balance.confirmed, 600 * 1e8); + })); + + it('should check main chain', co(function* () { + var result = yield tip1.isMainChain(); + assert(!result); + })); + + it('should mine a block after a reorg', co(function* () { + var block = yield mineBlock(null, cb2); + var entry, result; + + yield chain.add(block); + + entry = yield chain.db.getEntry(block.hash('hex')); + assert(entry); + assert(chain.tip.hash === entry.hash); + + result = yield entry.isMainChain(); + assert(result); + })); + + it('should prevent double spend on new chain', co(function* () { + var block = yield mineBlock(null, cb2); + var tip = chain.tip; + var err; + + try { + yield chain.add(block); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.reason, 'bad-txns-inputs-missingorspent'); + assert(chain.tip === tip); + })); + + it('should fail to mine a block with coins on an alternate chain', co(function* () { + var block = yield mineBlock(null, cb1); + var tip = chain.tip; + var err; + + try { + yield chain.add(block); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.reason, 'bad-txns-inputs-missingorspent'); + assert(chain.tip === tip); + })); + + it('should have correct chain value', function() { + assert.equal(chain.db.state.value, 65000000000); + assert.equal(chain.db.state.coin, 23); + assert.equal(chain.db.state.tx, 24); + }); + + it('should get coin', co(function* () { + var block, tx, output, coin; + + block = yield mineBlock(); + yield chain.add(block); + + block = yield mineBlock(null, block.txs[0]); + yield chain.add(block); + + tx = block.txs[1]; + output = Coin.fromTX(tx, 1, chain.height); + + coin = yield chain.db.getCoin(tx.hash('hex'), 1); + + assert.deepEqual(coin.toRaw(), output.toRaw()); + })); + + it('should get balance', co(function* () { + var balance, txs; + + yield co.timeout(100); + + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 1250 * 1e8); + assert.equal(balance.confirmed, 750 * 1e8); + + assert(wallet.account.receiveDepth >= 7); + assert(wallet.account.changeDepth >= 6); + + assert.equal(walletdb.state.height, chain.height); + + txs = yield wallet.getHistory(); + assert.equal(txs.length, 45); + })); + + it('should get tips and remove chains', co(function* () { + var tips = yield chain.db.getTips(); + + assert.notEqual(tips.indexOf(chain.tip.hash), -1); + assert.equal(tips.length, 2); + + yield chain.db.removeChains(); + + tips = yield chain.db.getTips(); + + assert.notEqual(tips.indexOf(chain.tip.hash), -1); + assert.equal(tips.length, 1); + })); + + it('should rescan for transactions', co(function* () { + var total = 0; + + yield chain.db.scan(0, walletdb.filter, function(block, txs) { + total += txs.length; + return Promise.resolve(); + }); + + assert.equal(total, 26); + })); + + it('should activate csv', co(function* () { + var deployments = chain.network.deployments; + var i, block, prev, state, cache; + + prev = yield chain.tip.getPrevious(); + state = yield chain.getState(prev, deployments.csv); + assert(state === 0); + + for (i = 0; i < 417; i++) { + block = yield miner.mineBlock(); + yield chain.add(block); + switch (chain.height) { + case 144: + prev = yield chain.tip.getPrevious(); + state = yield chain.getState(prev, deployments.csv); + assert(state === 1); + break; + case 288: + prev = yield chain.tip.getPrevious(); + state = yield chain.getState(prev, deployments.csv); + assert(state === 2); + break; + case 432: + prev = yield chain.tip.getPrevious(); + state = yield chain.getState(prev, deployments.csv); + assert(state === 3); + break; + } + } + + assert(chain.height === 432); + assert(chain.state.hasCSV()); + + cache = yield chain.db.getStateCache(); + assert.deepEqual(cache, chain.db.stateCache); + assert.equal(chain.db.stateCache.updates.length, 0); + assert(yield chain.db.verifyDeployments()); + })); + + var mineCSV = co(function* mineCSV(tx) { + var attempt = yield miner.createBlock(); + var redeemer; + + redeemer = new MTX(); + + redeemer.addOutput({ + script: [ + Script.array(new BN(1)), + Script.opcodes.OP_CHECKSEQUENCEVERIFY + ], + value: 10 * 1e8 + }); + + redeemer.addTX(tx, 0); + + redeemer.setLocktime(chain.height); + + yield wallet.sign(redeemer); + + attempt.addTX(redeemer.toTX(), redeemer.view); + + return yield attempt.mineAsync(); + }); + + it('should test csv', co(function* () { + var tx = (yield chain.db.getBlock(chain.height)).txs[0]; + var block = yield mineCSV(tx); + var csv, attempt, redeemer; + + yield chain.add(block); + + csv = block.txs[1]; + + redeemer = new MTX(); + + redeemer.addOutput({ + script: [ + Script.array(new BN(2)), + Script.opcodes.OP_CHECKSEQUENCEVERIFY + ], + value: 10 * 1e8 + }); + + redeemer.addTX(csv, 0); + redeemer.setSequence(0, 1, false); + + attempt = yield miner.createBlock(); + + attempt.addTX(redeemer.toTX(), redeemer.view); + + block = yield attempt.mineAsync(); + + yield chain.add(block); + })); + + it('should fail csv with bad sequence', co(function* () { + var csv = (yield chain.db.getBlock(chain.height)).txs[1]; + var block, attempt, redeemer, err; + + redeemer = new MTX(); + + redeemer.addOutput({ + script: [ + Script.array(new BN(1)), + Script.opcodes.OP_CHECKSEQUENCEVERIFY + ], + value: 10 * 1e8 + }); + + redeemer.addTX(csv, 0); + redeemer.setSequence(0, 1, false); + + attempt = yield miner.createBlock(); + + attempt.addTX(redeemer.toTX(), redeemer.view); + + block = yield attempt.mineAsync(); + + try { + yield chain.add(block); + } catch (e) { + err = e; + } + + assert(err); + assert(err.reason, 'mandatory-script-verify-flag-failed'); + })); + + it('should mine a block', co(function* () { + var block = yield miner.mineBlock(); + assert(block); + yield chain.add(block); + })); + + it('should fail csv lock checks', co(function* () { + var tx = (yield chain.db.getBlock(chain.height)).txs[0]; + var block = yield mineCSV(tx); + var csv, attempt, redeemer, err; + + yield chain.add(block); + + csv = block.txs[1]; + + redeemer = new MTX(); + + redeemer.addOutput({ + script: [ + Script.array(new BN(2)), + Script.opcodes.OP_CHECKSEQUENCEVERIFY + ], + value: 10 * 1e8 + }); + + redeemer.addTX(csv, 0); + redeemer.setSequence(0, 2, false); + + attempt = yield miner.createBlock(); + + attempt.addTX(redeemer.toTX(), redeemer.view); + + block = yield attempt.mineAsync(); + + try { + yield chain.add(block); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.reason, 'bad-txns-nonfinal'); + })); + + it('should rescan for transactions', co(function* () { + yield walletdb.rescan(0); + assert.equal(wallet.txdb.state.confirmed, 1289250000000); + })); + + it('should cleanup', co(function* () { + consensus.COINBASE_MATURITY = 100; + yield node.close(); + })); +}); diff --git a/test/util/memwallet.js b/test/util/memwallet.js new file mode 100644 index 00000000..7da7a816 --- /dev/null +++ b/test/util/memwallet.js @@ -0,0 +1,396 @@ +/*! + * memwallet.js - in-memory wallet object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var Network = require('../../lib/protocol/network'); +var util = require('../../lib/utils/util'); +var MTX = require('../../lib/primitives/mtx'); +var HD = require('../../lib/hd/hd'); +var Bloom = require('../../lib/utils/bloom'); +var KeyRing = require('../../lib/primitives/keyring'); +var Outpoint = require('../../lib/primitives/outpoint'); +var Coin = require('../../lib/primitives/coin'); + +function MemWallet(options) { + if (!(this instanceof MemWallet)) + return new MemWallet(options); + + this.network = Network.primary; + this.master = null; + this.key = null; + this.account = 0; + this.receiveDepth = 1; + this.changeDepth = 1; + this.receive = null; + this.change = null; + this.coins = {}; + this.undo = {}; + this.paths = {}; + this.balance = 0; + this.txs = 0; + this.filter = Bloom.fromRate(1000000, 0.001, -1); + + if (options) + this.fromOptions(options); + + this.init(); +} + +MemWallet.prototype.fromOptions = function fromOptions(options) { + if (options.network != null) { + assert(options.network); + this.network = Network.get(options.network); + } + + if (options.master != null) { + assert(options.master); + this.master = HD.PrivateKey.fromOptions(options.master, this.network); + } + + if (options.key != null) { + assert(HD.isPrivate(options.key)); + this.key = options.key; + } + + if (options.account != null) { + assert(typeof options.account === 'number'); + this.account = options.account; + } + + if (options.receiveDepth != null) { + assert(typeof options.receiveDepth === 'number'); + this.receiveDepth = options.receiveDepth; + } + + if (options.changeDepth != null) { + assert(typeof options.changeDepth === 'number'); + this.changeDepth = options.changeDepth; + } + + return this; +}; + +MemWallet.prototype.init = function init() { + var i; + + if (!this.master) + this.master = HD.PrivateKey.fromMnemonic(null, this.network); + + if (!this.key) + this.key = this.master.deriveAccount44(this.account); + + i = this.receiveDepth; + while (i--) + this.createReceive(); + + i = this.changeDepth; + while (i--) + this.createChange(); +}; + +MemWallet.prototype.createReceive = function createReceive() { + var index = this.receiveDepth++; + var key = this.deriveReceive(index); + var hash = key.getHash('hex'); + this.filter.add(hash, 'hex'); + this.paths[hash] = new Path(hash, 0, index); + this.receive = key; + return key; +}; + +MemWallet.prototype.createChange = function createChange() { + var index = this.changeDepth++; + var key = this.deriveChange(index); + var hash = key.getHash('hex'); + this.filter.add(hash, 'hex'); + this.paths[hash] = new Path(hash, 1, index); + this.change = key; + return key; +}; + +MemWallet.prototype.deriveReceive = function deriveReceive(index) { + return this.deriveKey(0, index); +}; + +MemWallet.prototype.deriveChange = function deriveChange(index) { + return this.deriveKey(1, index); +}; + +MemWallet.prototype.derivePath = function derivePath(path) { + return this.deriveKey(path.branch, path.index); +}; + +MemWallet.prototype.deriveKey = function deriveKey(branch, index) { + var key = this.master.deriveAccount44(this.account); + key = key.derive(branch).derive(index); + return new KeyRing({ + network: this.network, + privateKey: key.privateKey + }); +}; + +MemWallet.prototype.getKey = function getKey(hash) { + var path = this.paths[hash]; + if (!path) + return; + return this.derivePath(path); +}; + +MemWallet.prototype.getPath = function getPath(hash) { + return this.paths[hash]; +}; + +MemWallet.prototype.getCoin = function getCoin(key) { + return this.coins[key]; +}; + +MemWallet.prototype.getUndo = function getUndo(key) { + return this.undo[key]; +}; + +MemWallet.prototype.addCoin = function addCoin(coin) { + var op = Outpoint(coin.hash, coin.index); + var key = op.toKey(); + + this.filter.add(op.toRaw()); + + delete this.undo[key]; + + this.coins[key] = coin; + this.balance += coin.value; +}; + +MemWallet.prototype.removeCoin = function removeCoin(key) { + var coin = this.coins[key]; + + if (!coin) + return; + + this.undo[key] = coin; + this.balance -= coin.value; + + delete this.coins[key]; +}; + +MemWallet.prototype.getReceive = function getReceive() { + return this.receive.getAddress(); +}; + +MemWallet.prototype.getChange = function getChange() { + return this.change.getAddress(); +}; + +MemWallet.prototype.getCoins = function getCoins() { + return util.values(this.coins); +}; + +MemWallet.prototype.syncKey = function syncKey(path) { + switch (path.branch) { + case 0: + if (path.index === this.receiveDepth - 1) + this.createReceive(); + break; + case 1: + if (path.index === this.changeDepth - 1) + this.createChange(); + break; + default: + assert(false); + break; + } +}; + +MemWallet.prototype.addBlock = function addBlock(entry, txs) { + var i, tx; + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + this.addTX(tx, entry.height); + } +}; + +MemWallet.prototype.removeBlock = function removeBlock(entry, txs) { + var i, tx; + + for (i = txs.length - 1; i >= 0; i--) { + tx = txs[i]; + this.removeTX(tx, entry.height); + } +}; + +MemWallet.prototype.addTX = function addTX(tx, height) { + var result = false; + var i, op, path, addr, coin, input, output; + + if (height == null) + height = -1; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + op = input.prevout.toKey(); + coin = this.getCoin(op); + + if (!coin) + continue; + + result = true; + + this.removeCoin(op); + } + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + addr = output.getHash('hex'); + + if (!addr) + continue; + + path = this.getPath(addr); + + if (!path) + continue; + + result = true; + coin = Coin.fromTX(tx, i, height); + + this.addCoin(coin); + this.syncKey(path); + } + + if (result) + this.txs++; + + return result; +}; + +MemWallet.prototype.removeTX = function removeTX(tx, height) { + var hash = tx.hash('hex'); + var result = false; + var i, op, coin, input, output; + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + op = Outpoint(hash, i).toKey(); + coin = this.getCoin(op); + + if (!coin) + continue; + + result = true; + + this.removeCoin(op); + } + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + op = input.prevout.toKey(); + coin = this.getUndo(op); + + if (!coin) + continue; + + result = true; + + this.addCoin(coin); + } + + if (result) + this.txs--; + + return result; +}; + +MemWallet.prototype.deriveInputs = function deriveInputs(mtx) { + var keys = []; + var i, input, coin, addr, path, key; + + for (i = 0; i < mtx.inputs.length; i++) { + input = mtx.inputs[i]; + coin = mtx.view.getOutput(input); + + if (!coin) + continue; + + addr = coin.getHash('hex'); + + if (!addr) + continue; + + path = this.getPath(addr); + + if (!path) + continue; + + key = this.derivePath(path); + + keys.push(key); + } + + return keys; +}; + +MemWallet.prototype.fund = function fund(mtx, options) { + var coins = this.getCoins(); + + if (!options) + options = {}; + + return mtx.fund(coins, { + selection: options.selection, + round: options.round, + depth: options.depth, + hardFee: options.hardFee, + subtractFee: options.subtractFee, + changeAddress: this.getChange(), + height: -1, + rate: options.rate, + maxFee: options.maxFee + }); +}; + +MemWallet.prototype.sign = function sign(mtx) { + var keys = this.deriveInputs(mtx); + mtx.template(keys); + mtx.sign(keys); +}; + +MemWallet.prototype.send = function send(options) { + var self = this; + var mtx = new MTX(options); + var tx; + + this.fund(mtx, options).then(function() { + assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); + + mtx.sortMembers(); + + if (options.locktime != null) + mtx.setLocktime(options.locktime); + + self.sign(mtx); + + if (!mtx.isSigned()) + throw new Error('Cannot sign tx.'); + + tx = mtx.toTX(); + + self.addTX(tx); + }).catch(function(err) { + throw err; + }); + + return tx; +}; + +function Path(hash, branch, index) { + this.hash = hash; + this.branch = branch; + this.index = index; +} + +module.exports = MemWallet;