From 00b8f8950b051d76edd013aaf6e66db6d6bd5fb8 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 29 Feb 2016 21:33:11 -0800 Subject: [PATCH] walletdb improvements. start refactoring node object. --- bin/node | 8 +- lib/bcoin.js | 3 +- lib/bcoin/blockdb.js | 14 +- lib/bcoin/chain.js | 9 +- lib/bcoin/http.js | 64 +++++- lib/bcoin/mempool.js | 2 +- lib/bcoin/miner.js | 18 +- lib/bcoin/mtx.js | 6 +- lib/bcoin/node.js | 172 ++++++++-------- lib/bcoin/node2.js | 54 +++++ lib/bcoin/pool.js | 23 ++- lib/bcoin/tx.js | 51 +++++ lib/bcoin/utils.js | 8 + lib/bcoin/wallet.js | 31 ++- lib/bcoin/walletdb.js | 454 +++++++++++++++++++++++++++++++++--------- 15 files changed, 699 insertions(+), 218 deletions(-) create mode 100644 lib/bcoin/node2.js diff --git a/bin/node b/bin/node index c887d6d8..8d0c3e8f 100755 --- a/bin/node +++ b/bin/node @@ -4,10 +4,16 @@ var bcoin = require('bcoin'); var utils = bcoin.utils; var assert = utils.assert; -var node = bcoin.node({ +var node = bcoin.fullnode({ debug: true }); node.on('error', function(err) { utils.print(err.message); }); + +node.pool.on('full', function() { + setInterval(function() { + console.log(node.wallet.getAll()); + }, 1000); +}); diff --git a/lib/bcoin.js b/lib/bcoin.js index 35e52f98..e75e1694 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -76,8 +76,9 @@ bcoin.merkleblock = require('./bcoin/merkleblock'); bcoin.headers = require('./bcoin/headers'); bcoin.ramdisk = require('./bcoin/ramdisk'); bcoin.blockdb = require('./bcoin/blockdb'); +bcoin.node = require('./bcoin/node2'); bcoin.spvnode = require('./bcoin/spvnode'); -bcoin.node = require('./bcoin/node'); +bcoin.fullnode = require('./bcoin/node'); bcoin.chainblock = require('./bcoin/chainblock'); bcoin.chaindb = require('./bcoin/chaindb'); bcoin.chain = require('./bcoin/chain'); diff --git a/lib/bcoin/blockdb.js b/lib/bcoin/blockdb.js index 161597ad..e68d4d24 100644 --- a/lib/bcoin/blockdb.js +++ b/lib/bcoin/blockdb.js @@ -15,12 +15,17 @@ var fs = bcoin.fs; * BlockDB */ -function BlockDB(options) { +function BlockDB(node, options) { var self = this; var levelup; if (!(this instanceof BlockDB)) - return new BlockDB(options); + return new BlockDB(node, options); + + if (!(node instanceof bcoin.node)) { + options = node; + node = null; + } // Some lazy loading levelup = require('levelup'); @@ -39,6 +44,7 @@ function BlockDB(options) { this.options = options; + this.node = node; this.data = new BlockData(options); this.cache = { @@ -1023,10 +1029,10 @@ BlockDB.prototype.resetHeight = function resetHeight(height, callback, emit) { }; BlockDB.prototype._getEntry = function _getEntry(height, callback) { - if (!bcoin.chain.global) + if (!this.node) return callback(); - return bcoin.chain.global.db.getAsync(height, callback); + return this.node.chain.db.getAsync(height, callback); }; /** diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index c1780336..e9665ad6 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -18,9 +18,9 @@ var fs = bcoin.fs; * Chain */ -function Chain(options) { +function Chain(node, options) { if (!(this instanceof Chain)) - return new Chain(options); + return new Chain(node, options); EventEmitter.call(this); @@ -35,8 +35,9 @@ function Chain(options) { this.db = new bcoin.chaindb(this); this.request = new utils.RequestCache(); this.loading = false; - this.mempool = options.mempool; - this.blockdb = options.blockdb; + this.node = node; + this.mempool = node.mempool; + this.blockdb = node.blockdb; this.busy = false; this.jobs = []; this.pending = []; diff --git a/lib/bcoin/http.js b/lib/bcoin/http.js index 1c6b1590..e457bc69 100644 --- a/lib/bcoin/http.js +++ b/lib/bcoin/http.js @@ -69,8 +69,10 @@ HTTPServer.prototype._init = function _init() { self.node.getCoinByAddress(addresses, function(err, coins) { if (err) return next(err); + if (!coins.length) return send(404); + send(200, coins.map(function(coin) { return coin.toJSON(); })); }); }); @@ -81,8 +83,10 @@ HTTPServer.prototype._init = function _init() { self.node.getCoin(req.params.hash, +req.params.index, function(err, coin) { if (err) return next(err); + if (!coin) return send(404); + send(200, coin.toJSON()); }); }); @@ -92,8 +96,10 @@ HTTPServer.prototype._init = function _init() { self.node.getCoinByAddress(req.body.addresses, function(err, coins) { if (err) return next(err); + if (!coins.length) return send(404); + send(200, coins.map(function(coin) { return coin.toJSON(); })); }); }); @@ -104,8 +110,10 @@ HTTPServer.prototype._init = function _init() { self.node.getTX(req.params.hash, function(err, tx) { if (err) return next(err); + if (!tx) return send(404); + send(200, tx.toJSON()); }); }); @@ -116,8 +124,10 @@ HTTPServer.prototype._init = function _init() { self.node.getTXByAddress(addresses, function(err, txs) { if (err) return next(err); + if (!txs.length) return send(404); + send(200, txs.map(function(tx) { return tx.toJSON(); })); }); }); @@ -145,8 +155,10 @@ HTTPServer.prototype._init = function _init() { self.node.getBlock(hash, function(err, block) { if (err) return next(err); + if (!block) return send(404); + send(200, block.toJSON()); }); }); @@ -156,8 +168,10 @@ HTTPServer.prototype._init = function _init() { self.node.walletdb.getJSON(req.params.id, function(err, json) { if (err) return next(err); + if (!json) return send(404); + send(200, json); }); }); @@ -165,33 +179,71 @@ HTTPServer.prototype._init = function _init() { // Create/get wallet this.post('/wallet/:id', function(req, res, next, send) { req.body.id = req.params.id; - self.node.walletdb.create(req.body, function(err, json) { + self.node.walletdb.createJSON(req.body.id, req.body, function(err, json) { if (err) return next(err); + if (!json) return send(404); - send(json); + + send(200, json); }); }); // Update wallet / sync address depth this.put('/wallet/:id', function(req, res, next, send) { req.body.id = req.params.id; - self.node.walletdb.save(req.body.id, req.body, function(err, json) { + self.node.walletdb.saveJSON(req.body.id, req.body, function(err, json) { if (err) return next(err); + if (!json) return send(404); - send(json); + + send(200, json); }); }); // Wallet UTXOs - this.get('/wallet/:id/utxo', function(req, res, next) { + this.get('/wallet/:id/utxo', function(req, res, next, send) { + self.node.walletdb.getJSON(req.params.id, function(err, json) { + if (err) + return next(err); + + if (!json) + return send(404); + + self.node.getCoinByAddress(Object.keys(json.addressMap), function(err, coins) { + if (err) + return next(err); + + if (!coins.length) + return send(404); + + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); }); // Wallet TXs - this.get('/wallet/:id/tx', function(req, res, next) { + this.get('/wallet/:id/tx', function(req, res, next, send) { + self.node.walletdb.getJSON(req.params.id, function(err, json) { + if (err) + return next(err); + + if (!json) + return send(404); + + self.node.getTXByAddress(Object.keys(json.addressMap), function(err, txs) { + if (err) + return next(err); + + if (!txs.length) + return send(404); + + send(200, coins.map(function(tx) { return tx.toJSON(); })); + }); + }); }); }; diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index 486523c7..dc9b1e53 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -20,7 +20,7 @@ var fs = bcoin.fs; function Mempool(node, options) { if (!(this instanceof Mempool)) - return new Mempool(pool, options); + return new Mempool(node, options); if (!options) options = {}; diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index 676efb36..3d2d46f1 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -16,9 +16,9 @@ var EventEmitter = require('events').EventEmitter; * Miner */ -function Miner(pool, options) { +function Miner(node, options) { if (!(this instanceof Miner)) - return new Miner(options); + return new Miner(node, options); EventEmitter.call(this); @@ -27,19 +27,19 @@ function Miner(pool, options) { this.options = options; this.address = this.options.address; - this.msg = this.options.msg || 'mined by bcoin'; + this.coinbaseFlags = this.options.coinbaseFlags || 'mined by bcoin'; - this.pool = pool || bcoin.pool.global; - this.chain = this.pool.chain; - this.mempool = this.pool.mempool; - this.blockdb = this.pool.blockdb; + this.node = node; + this.pool = node.pool; + this.chain = node.chain; + this.mempool = node.mempool; this.running = false; this.timeout = null; this.interval = null; this.fee = new bn(0); - this.last = this.chain.tip; + this.last = this.node.chain.tip; this.block = null; this.iterations = 0; this._begin = utils.now(); @@ -229,7 +229,7 @@ Miner.prototype.createBlock = function createBlock(tx) { new Buffer(utils.nonce().toArray()), // Let the world know this little // miner succeeded. - new Buffer(this.msg || 'mined by bcoin', 'ascii') + new Buffer(this.coinbaseFlags || 'mined by bcoin', 'ascii') ], sequence: 0xffffffff }); diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index 101cf448..024606e0 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -699,6 +699,9 @@ MTX.prototype.maxSize = function maxSize(maxM, maxN) { if (bcoin.script.isWitnessProgram(prev)) { witness = true; + // Now calculating vsize. The regular + // redeem script (if there was one) + // is now worth 4 points. size *= 4; if (this.inputs[i].witness.length && bcoin.script.isWitnessScripthash(prev)) { prev = this.inputs[i].witness[this.inputs[i].witness.length - 1]; @@ -763,9 +766,10 @@ MTX.prototype.maxSize = function maxSize(maxM, maxN) { } } - // Byte for varint size of input script + // Byte for varint size of input script. size += utils.sizeIntv(size); + // Calculate vsize if we're a witness program. if (witness) size = (size + 3) / 4 | 0; diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 805c104a..74656d5f 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -1,5 +1,5 @@ /** - * node.js - full node for bcoin + * fullnode.js - full node for bcoin * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * https://github.com/indutny/bcoin */ @@ -14,68 +14,66 @@ var assert = utils.assert; var fs = bcoin.fs; /** - * Node + * Fullnode */ -function Node(options) { - if (!(this instanceof Node)) - return new Node(options); +function Fullnode(options) { + if (!(this instanceof Fullnode)) + return new Fullnode(options); EventEmitter.call(this); if (!options) options = {}; - this.options = options; + bcoin.node.call(this, options); - if (this.options.debug) - bcoin.debug = this.options.debug; + this.options.http = {}; - if (this.options.network) - network.set(this.options.network); + if (!this.options.wallet) + this.options.wallet = {}; - this.blockdb = null; - this.mempool = null; - this.pool = null; - this.chain = null; - this.miner = null; - this.wallet = null; + if (!this.options.wallet.id) + this.options.wallet.id = 'primary'; + + if (!this.options.wallet.passphrase) + this.options.wallet.passphrase = 'node'; this.loading = false; - Node.global = this; + Fullnode.global = this; this._init(); } -utils.inherits(Node, EventEmitter); +utils.inherits(Fullnode, bcoin.node); -Node.prototype._init = function _init() { +Fullnode.prototype._init = function _init() { var self = this; this.loading = true; - if (!this.options.pool) - this.options.pool = {}; + // BlockDB and Mempool need to be instantiated + // first because the chain needs access to them. + this.blockdb = new bcoin.blockdb(this, { + cache: false + }); - if (!this.options.miner) - this.options.miner = {}; + this.mempool = new bcoin.mempool(this); - this.blockdb = new bcoin.blockdb(this.options.blockdb); - this.mempool = new bcoin.mempool(this, this.options.mempool); + // Chain is instantiated next. The pool needs it. + this.chain = new bcoin.chain(this, { + preload: false + }); - this.options.pool.spv = false; - this.options.pool.blockdb = this.blockdb; - this.options.pool.mempool = this.mempool; + this.pool = new bcoin.pool(this, { + witness: this.network.type === 'segnet', + spv: false + }); - this.pool = new bcoin.pool(this.options.pool); - this.chain = this.pool.chain; + this.miner = new bcoin.miner(this, this.options.miner); + this.walletdb = new bcoin.walletdb(this, this.options.walletdb); - this.miner = new bcoin.miner(this.pool, this.options.miner); - - this.walletdb = new bcoin.walletdb(this.options.walletdb); - - this.options.http = {}; if (this.options.http && bcoin.http) { this.http = new bcoin.http(this, this.options.http); this.http.listen(this.options.http.port || 8080); @@ -89,61 +87,79 @@ Node.prototype._init = function _init() { self.emit('error', err); }); - if (!this.options.wallet) - this.options.wallet = {}; + this.on('tx', function(tx) { + self.walletdb.ownTX(tx, function(err, input, output) { + if (err) + return self.emit('error', err); - if (!this.options.wallet.id) - this.options.wallet.id = 'primary'; + self.emit('own tx', tx, input, output); + }); + }); - if (!this.options.wallet.passphrase) - this.options.wallet.passphrase = 'node'; + this.chain.on('block', function(block) { + self.emit('block', block); + block.txs.forEach(function(tx) { + self.emit('tx', tx, block); + }); + }); - this.walletdb.create(this.options.wallet, function(err, wallet) { + this.mempool.on('tx', function(tx) { + self.emit('tx', tx); + }); + + // Handle forks + this.chain.on('remove entry', function(entry) { + self.wallets.forEach(function(wallet) { + wallet.tx.getAll().forEach(function(tx) { + if (tx.block === entry.hash || tx.height >= entry.height) + wallet.tx.unconfirm(tx); + }); + }); + }); + + this.createWallet(this.options.wallet, function(err, wallet) { if (err) throw err; - self.wallet = wallet; + self.miner.address = wallet.getAddress(); + + self.pool.startSync(); + + self.loading = false; + self.emit('load'); + }); +}; + +Fullnode.prototype.createWallet = function createWallet(options, callback) { + var self = this; + callback = utils.ensure(callback); + this.walletdb.create(options, function(err, wallet) { + if (err) + return callback(err); + + assert(wallet); utils.debug('Loaded wallet with id=%s address=%s', wallet.getID(), wallet.getAddress()); - self.chain.on('block', function(block) { - block.txs.forEach(function(tx) { - self.wallet.addTX(tx); - }); - }); + self.wallets.push(wallet); - self.mempool.on('tx', function(tx) { - self.wallet.addTX(tx); - }); + return callback(null, wallet); - self.miner.address = self.wallet.getAddress(); - - // Handle forks - self.chain.on('remove entry', function(entry) { - self.wallet.tx.getAll().forEach(function(tx) { - if (tx.block === entry.hash || tx.height >= entry.height) - self.wallet.tx.unconfirm(tx); - }); - }); - - self.pool.addWallet(self.wallet, function(err) { + self.pool.addWallet(wallet, function(err) { if (err) - throw err; + return callback(err); - self.pool.startSync(); - - self.loading = false; - self.emit('load'); + return callback(null, wallet); }); }); }; -Node.prototype.scanWallet = function scanWallet(callback) { - this.wallet.scan(this.getTXByAddress.bind(this), callback); +Fullnode.prototype.scanWallet = function scanWallet(wallet, callback) { + wallet.scan(this.getTXByAddress.bind(this), callback); }; -Node.prototype.getBlock = function getBlock(hash, callback) { +Fullnode.prototype.getBlock = function getBlock(hash, callback) { var self = this; var coin; @@ -158,7 +174,7 @@ Node.prototype.getBlock = function getBlock(hash, callback) { }); }; -Node.prototype.getCoin = function getCoin(hash, index, callback) { +Fullnode.prototype.getCoin = function getCoin(hash, index, callback) { var self = this; var coin; @@ -182,7 +198,7 @@ Node.prototype.getCoin = function getCoin(hash, index, callback) { }); }; -Node.prototype.getCoinByAddress = function getCoinByAddress(addresses, callback) { +Fullnode.prototype.getCoinByAddress = function getCoinByAddress(addresses, callback) { var self = this; var mempool; @@ -202,7 +218,7 @@ Node.prototype.getCoinByAddress = function getCoinByAddress(addresses, callback) }); }; -Node.prototype.getTX = function getTX(hash, callback) { +Fullnode.prototype.getTX = function getTX(hash, callback) { var self = this; var tx; @@ -223,7 +239,7 @@ Node.prototype.getTX = function getTX(hash, callback) { }); }; -Node.prototype.isSpent = function isSpent(hash, index, callback) { +Fullnode.prototype.isSpent = function isSpent(hash, index, callback) { var self = this; callback = utils.asyncify(callback); @@ -234,7 +250,7 @@ Node.prototype.isSpent = function isSpent(hash, index, callback) { this.blockdb.isSpent(hash, index, callback); }; -Node.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { +Fullnode.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { var self = this; var mempool; @@ -250,7 +266,7 @@ Node.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { }); }; -Node.prototype.fillCoin = function fillCoin(tx, callback) { +Fullnode.prototype.fillCoin = function fillCoin(tx, callback) { callback = utils.asyncify(callback); if (this.mempool.fillCoin(tx)) @@ -259,7 +275,7 @@ Node.prototype.fillCoin = function fillCoin(tx, callback) { this.blockdb.fillCoin(tx, callback); }; -Node.prototype.fillTX = function fillTX(tx, callback) { +Fullnode.prototype.fillTX = function fillTX(tx, callback) { callback = utils.asyncify(callback); if (this.mempool.fillTX(tx)) @@ -272,4 +288,4 @@ Node.prototype.fillTX = function fillTX(tx, callback) { * Expose */ -module.exports = Node; +module.exports = Fullnode; diff --git a/lib/bcoin/node2.js b/lib/bcoin/node2.js new file mode 100644 index 00000000..e484197e --- /dev/null +++ b/lib/bcoin/node2.js @@ -0,0 +1,54 @@ +/** + * node.js - node object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var EventEmitter = require('events').EventEmitter; +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; +var fs = bcoin.fs; + +/** + * Node + */ + +function Node(options) { + if (!(this instanceof Node)) + return new Node(options); + + EventEmitter.call(this); + + if (!options) + options = {}; + + this.options = options; + + if (this.options.debug) + bcoin.debug = this.options.debug; + + if (this.options.network) + network.set(this.options.network); + + this.network = network; + this.blockdb = null; + this.mempool = null; + this.pool = null; + this.chain = null; + this.miner = null; + this.wallets = []; + + Node.global = this; +} + +utils.inherits(Node, EventEmitter); + +/** + * Expose + */ + +module.exports = Node; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index a87f4a56..3cac6e33 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -16,12 +16,12 @@ var constants = bcoin.protocol.constants; * Pool */ -function Pool(options) { +function Pool(node, options) { var self = this; var Chain; if (!(this instanceof Pool)) - return new Pool(options); + return new Pool(node, options); EventEmitter.call(this); @@ -55,8 +55,9 @@ function Pool(options) { this.destroyed = false; this.size = options.size || 8; - this.blockdb = options.blockdb; - this.mempool = options.mempool; + this.chain = node.chain; + this.blockdb = node.blockdb; + this.mempool = node.mempool; if (options.spv) { if (options.headers == null) @@ -76,12 +77,12 @@ function Pool(options) { this.requestTimeout = options.requestTimeout || 2 * 60000; - this.chain = new bcoin.chain({ - spv: options.spv, - preload: options.preload, - blockdb: options.blockdb, - mempool: options.mempool - }); + // this.chain = new bcoin.chain({ + // spv: options.spv, + // preload: options.preload, + // blockdb: options.blockdb, + // mempool: options.mempool + // }); this.watchMap = {}; @@ -118,7 +119,7 @@ function Pool(options) { type: 'tx' }; - if (network.type === 'segnet') { + if (this.options.witness) { this.block.type = 'witness' + this.block.type; this.tx.type = 'witness' + this.tx.type; } diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index b04bd35e..cce7faae 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -465,6 +465,57 @@ TX.prototype.getFunds = function getFunds(side) { return this.getOutputValue(); }; +TX.prototype.getInputAddresses = function getInputAddresses() { + var table = {}; + var addresses = []; + var i, address; + + for (i = 0; i < this.inputs.length; i++) { + address = this.inputs[i].getAddress(); + if (address && !table[address]) { + table[address] = true; + addresses.push(address); + } + } + + addresses.table = table; + + return addresses; +}; + +TX.prototype.getOutputAddresses = function getOutputAddresses() { + var table = {}; + var addresses = []; + var i, address; + + for (i = 0; i < this.outputs.length; i++) { + address = this.outputs[i].getAddress(); + if (address && !table[address]) { + table[address] = true; + addresses.push(address); + } + } + + addresses.table = table; + + return addresses; +}; + +TX.prototype.getAddresses = function getAddresses() { + var input = this.getInputAddresses(); + var output = this.getOutputAddresses(); + var i; + + for (i = 0; i < output.length; i++) { + if (!input.table[output[i]]) { + input.table[output[i]] = true; + input.push(output[i]); + } + } + + return input; +}; + TX.prototype.testInputs = function testInputs(addressTable, index) { var i, input; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 2f596bf2..81ebc8e8 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -415,6 +415,14 @@ utils.asyncify = function asyncify(callback) { return asyncifyFn; }; +utils.nop = function() {}; + +utils.ensure = function ensure(callback) { + if (!callback) + return utils.nop; + return callback; +}; + utils.revHex = function revHex(s) { var r = ''; var i = 0; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 07854511..a7afbbce 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -373,12 +373,17 @@ 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); @@ -876,7 +881,7 @@ Wallet.prototype.__defineGetter__('address', function() { return this.getAddress(); }); -Wallet.prototype.toJSON = function toJSON(noPool) { +Wallet.prototype.toJSON = function toJSON() { return { v: 3, name: 'wallet', @@ -897,7 +902,7 @@ Wallet.prototype.toJSON = function toJSON(noPool) { return key.xpubkey; }), balance: utils.btc(this.getBalance()), - txs: noPool ? [] : this.tx.getAll().map(function(tx) { + txs: this.options.noPool ? [] : this.tx.getAll().map(function(tx) { return tx.toCompact(); }) }; @@ -926,6 +931,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); }) @@ -936,11 +942,20 @@ Wallet.fromJSON = function fromJSON(json, passphrase) { return new Wallet(Wallet._fromJSON(json, passphrase)); }; -Wallet.prototype.save = function save(callback) { - callback = utils.asyncify(callback); +Wallet.prototype._saveAddress = function _saveAddress(address, callback) { + callback = utils.ensure(callback); - if (!this.options.store && !this.options.db) - return callback(); + if (!this.options.store || !this.options.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.options.store || !this.options.db) + return utils.nextTick(callback); return this.db.save(this.id, this, callback); }; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index c522ecff..2108a774 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -19,20 +19,25 @@ var fs = bcoin.fs; * WalletDB */ -function WalletDB(options) { +function WalletDB(node, options) { if (!(this instanceof WalletDB)) - return new WalletDB(options); + return new WalletDB(node, options); if (WalletDB.global) return WalletDB.global; + if (!(node instanceof bcoin.node)) { + options = node; + node = null; + } + if (!options) options = {}; EventEmitter.call(this); + this.node = node; this.options = options; - this.file = options.file; this.dir = options.dir; this.type = options.type; @@ -112,107 +117,128 @@ WalletDB.prototype._init = function _init() { throw new Error('Unknown storage type: ' + this.type); }; -WalletDB.prototype.save = function save(id, json, callback) { - callback = utils.asyncify(callback); +WalletDB.prototype.getJSON = function getJSON(id, callback) { + if (typeof id === 'object') + id = id.id; + + callback = utils.ensure(callback); if (this.type === 'leveldb') - return this.saveDB(id, json, callback); + return this._getDB(id, callback); if (this.type === 'file') - return this.saveFile(id, json, callback); + return this._getFile(id, callback); throw new Error('Unknown storage type: ' + this.type); }; -WalletDB.prototype.saveDB = function saveFile(id, json, callback) { - var key; +WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) { + var self = this; - key = 'w/' + id; + callback = utils.ensure(callback); if (json instanceof bcoin.wallet) { - json.store = true; - json.db = this; - json = json.toJSON(this.options.noPool); + json = json.toJSON(); + } else { + if (typeof json.v !== 'number') { + json = utils.merge({}, json); + delete json.store; + delete json.db; + json = new bcoin.wallet(json).toJSON(); + } } - callback = utils.asyncify(callback); + function cb(err, json) { + var batch; - json = JSON.stringify(json); - - this.db.put(key, json, callback); -}; - -WalletDB.prototype.saveFile = function saveFile(id, json, callback) { - var file, options; - - file = this.dir + '/' + id + '.json'; - - if (json instanceof bcoin.wallet) { - json.store = true; - json.db = this; - json = json.toJSON(this.options.noPool); - } - - callback = utils.asyncify(callback); - - if (!bcoin.fs) - return callback(); - - json = JSON.stringify(json, null, 2); - - options = { - encoding: 'utf8', - mode: 0600 - }; - - fs.writeFile(file, json, options, function(err) { if (err) return callback(err); - return callback(null, file); - }); -}; + if (json && self.type === 'leveldb') { + batch = self.db.batch(); + Object.keys(json.addressMap).forEach(function(address) { + batch.put('a/' + address + '/' + json.id, ''); + }); + return batch.write(function(err) { + if (err) + return callback(err); + return callback(null, json); + }); + } -WalletDB.prototype.getJSON = function getJSON(id, passphrase, callback) { - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; - } - - callback = utils.asyncify(callback); - - if (id instanceof bcoin.wallet) { - id = wallet.id; - json.store = true; - json.db = this; + return callback(null, json); } if (this.type === 'leveldb') - return this.getDB(id, passphrase, callback); + return this._saveDB(id, json, cb); if (this.type === 'file') - return this.getFile(id, passphrase, callback); + return this._saveFile(id, json, cb); throw new Error('Unknown storage type: ' + this.type); }; -WalletDB.prototype.getFile = function getFile(id, passphrase, callback) { +WalletDB.prototype.removeJSON = function removeJSON(id, callback) { + var self = this; + + callback = utils.ensure(callback); + + if (typeof id === 'object') + id = id.id; + + function cb(err, json) { + var batch; + + if (err) + return callback(err); + + if (json && self.type === 'leveldb') { + batch = self.db.batch(); + Object.keys(json.addressMap).forEach(function(address) { + batch.del('a/' + address + '/' + json.id); + }); + return batch.write(function(err) { + if (err) + return callback(err); + return callback(null, json); + }); + } + + return callback(null, json); + } + + if (this.type === 'leveldb') + return this._removeDB(id, cb); + + if (this.type === 'file') + return this._removeFile(id, cb); + + throw new Error('Unknown storage type: ' + this.type); +}; + +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._getFile = function _getFile(id, callback) { var self = this; var file; - callback = utils.asyncify(callback); - - if (!bcoin.fs) - return callback(); - - if (!id) - return callback(); + callback = utils.ensure(callback); file = this.dir + '/' + id + '.json'; fs.readFile(file, 'utf8', function(err, json) { - var options; - if (err && err.code === 'ENOENT') return callback(); @@ -220,29 +246,24 @@ WalletDB.prototype.getFile = function getFile(id, passphrase, callback) { return callback(err); try { - options = bcoin.wallet._fromJSON(JSON.parse(json), passphrase); + json = JSON.parse(json); } catch (e) { return callback(e); } - options.store = true; - options.db = self; - - return callback(null, options); + return callback(null, json); }); }; -WalletDB.prototype.getDB = function getDB(id, passphrase, callback) { +WalletDB.prototype._getDB = function _getDB(id, callback) { var self = this; var key; - callback = utils.asyncify(callback); + callback = utils.ensure(callback); key = 'w/' + id; this.db.get(key, function(err, json) { - var options; - if (err && err.type === 'NotFoundError') return callback(); @@ -250,45 +271,288 @@ WalletDB.prototype.getDB = function getDB(id, passphrase, callback) { return callback(err); try { - options = bcoin.wallet._fromJSON(JSON.parse(json), passphrase); + json = JSON.parse(json); } catch (e) { return callback(e); } - options.store = true; - options.db = self; + return callback(null, json); + }); +}; - return callback(null, options); +WalletDB.prototype._saveDB = function _saveDB(id, json, callback) { + var key = 'w/' + id; + var data; + + callback = utils.ensure(callback); + + data = JSON.stringify(json); + + this.db.put(key, data, function(err) { + if (err) + return callback(err); + + return callback(null, json); + }); +}; + +WalletDB.prototype._saveFile = function _saveFile(id, json, callback) { + var file = this.dir + '/' + id + '.json'; + var options, data; + + callback = utils.ensure(callback); + + data = JSON.stringify(json, null, 2); + + options = { + encoding: 'utf8', + mode: 0600 + }; + + fs.writeFile(file, data, options, function(err) { + if (err) + return callback(err); + + return callback(null, json); + }); +}; + +WalletDB.prototype._removeDB = function _removeDB(id, callback) { + var self = this; + var key = 'w/' + id; + + callback = utils.ensure(callback); + + this._getDB(id, function(err, json) { + if (err) + return callback(err); + + self.db.del(key, function(err) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + return callback(null, json); + }); + }); +}; + +WalletDB.prototype._removeFile = function _removeFile(id, callback) { + var file = this.dir + '/' + id + '.json'; + + callback = utils.ensure(callback); + + this._getFile(id, function(err, json) { + if (err) + return callback(err); + + fs.unlink(file, function(err) { + if (err && err.code !== 'ENOENT') + return callback(err); + + return callback(null, json); + }); }); }; WalletDB.prototype.get = function get(id, passphrase, callback) { - callback = utils.asyncify(callback); - return this.getJSON(id, passphrase, function(err, options) { + if (typeof passphrase === 'function') { + callback = passphrase; + passphrase = null; + } + + callback = utils.ensure(callback); + + return this.getJSON(id, function(err, options) { + var wallet; + if (err) return callback(err); if (!options) return callback(); - return callback(null, new bcoin.wallet(options)); + wallet = bcoin.wallet.fromJSON(options, passphrase); + wallet.store = true; + wallet.db = self; + + return callback(null, wallet); }); }; +WalletDB.prototype.save = function save(options, callback) { + var self = this; + var passphrase = options.passphrase; + + callback = utils.ensure(callback); + + return this.saveJSON(options.id, options, callback); +}; + +WalletDB.prototype.remove = function save(id, callback) { + var self = this; + + callback = utils.ensure(callback); + + if (id instanceof bcoin.wallet) { + id.store = false; + id.db = null; + id = id.id; + } + + return this.removeJSON(id, callback); +}; + WalletDB.prototype.create = function create(options, callback) { var self = this; - callback = utils.asyncify(callback); - return this.getJSON(options.id, options.passphrase, function(err, opt) { + var passphrase = options.passphrase; + + callback = utils.ensure(callback); + + if (options instanceof bcoin.wallet) { + options.store = true; + options.db = this; + } + + return this.createJSON(options.id, options, function(err, json) { + var wallet; + if (err) return callback(err); - if (!opt) { - options.store = true; - options.db = self; - return callback(null, new bcoin.wallet(options)); - } + wallet = bcoin.wallet.fromJSON(json, options.passphrase); + wallet.store = true; + wallet.db = self; - return callback(null, new bcoin.wallet(opt)); + return callback(null, wallet); + }); +}; + +WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) { + callback = utils.ensure(callback); + if (this.type !== 'leveldb') + return utils.nextTick(callback); + this.db.put('a/' + address + '/' + id, '', callback); +}; + +WalletDB.prototype.removeAddress = function removeAddress(id, address, callback) { + callback = utils.ensure(callback); + if (this.type !== 'leveldb') + return utils.nextTick(callback); + this.db.del('a/' + address + '/' + id, callback); +}; + +WalletDB.prototype._getIDs = function _getIDs(address, callback) { + var self = this; + var ids = []; + + var iter = this.db.db.iterator({ + gte: 'a/' + address, + lte: '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 (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; + + if (this.type !== 'leveldb') + return utils.nextTick(callback); + + 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) { + if (err) + return callback(err); + + if (input || output) + return callback(null, input || [], output || []); + + return callback(); + }); }); }; @@ -296,4 +560,6 @@ WalletDB.prototype.create = function create(options, callback) { * Expose */ + var self = this; + var self = this; module.exports = WalletDB;