diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index a5d6e567..e30788e5 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -18,508 +18,498 @@ const HTTP = require('./http'); const RPC = require('./rpc'); /** + * Full Node * Respresents a fullnode complete with a * chain, mempool, miner, etc. * @alias module:node.FullNode * @extends Node - * @constructor - * @param {Object?} options - * @property {Chain} chain - * @property {PolicyEstimator} fees - * @property {Mempool} mempool - * @property {Pool} pool - * @property {Miner} miner - * @property {HTTP} http - * @emits FullNode#block - * @emits FullNode#tx - * @emits FullNode#connect - * @emits FullNode#disconnect - * @emits FullNode#reset - * @emits FullNode#error */ -function FullNode(options) { - if (!(this instanceof FullNode)) - return new FullNode(options); +class FullNode extends Node { + /** + * Create a full node. + * @constructor + * @param {Object?} options + */ - Node.call(this, 'bcoin', 'bcoin.conf', 'debug.log', options); + constructor(options) { + super('bcoin', 'bcoin.conf', 'debug.log', options); - this.opened = false; + this.opened = false; - // SPV flag. - this.spv = false; + // SPV flag. + this.spv = false; - // Instantiate blockchain. - this.chain = new Chain({ - network: this.network, - logger: this.logger, - workers: this.workers, - db: this.config.str('db'), - prefix: this.config.prefix, - maxFiles: this.config.uint('max-files'), - cacheSize: this.config.mb('cache-size'), - forceFlags: this.config.bool('force-flags'), - bip91: this.config.bool('bip91'), - bip148: this.config.bool('bip148'), - prune: this.config.bool('prune'), - checkpoints: this.config.bool('checkpoints'), - coinCache: this.config.mb('coin-cache'), - entryCache: this.config.uint('entry-cache'), - indexTX: this.config.bool('index-tx'), - indexAddress: this.config.bool('index-address') - }); + // Instantiate blockchain. + this.chain = new Chain({ + network: this.network, + logger: this.logger, + workers: this.workers, + db: this.config.str('db'), + prefix: this.config.prefix, + maxFiles: this.config.uint('max-files'), + cacheSize: this.config.mb('cache-size'), + forceFlags: this.config.bool('force-flags'), + bip91: this.config.bool('bip91'), + bip148: this.config.bool('bip148'), + prune: this.config.bool('prune'), + checkpoints: this.config.bool('checkpoints'), + coinCache: this.config.mb('coin-cache'), + entryCache: this.config.uint('entry-cache'), + indexTX: this.config.bool('index-tx'), + indexAddress: this.config.bool('index-address') + }); - // Fee estimation. - this.fees = new Fees(this.logger); - this.fees.init(); + // Fee estimation. + this.fees = new Fees(this.logger); + this.fees.init(); - // Mempool needs access to the chain. - this.mempool = new Mempool({ - network: this.network, - logger: this.logger, - workers: this.workers, - chain: this.chain, - fees: this.fees, - db: this.config.str('db'), - prefix: this.config.prefix, - persistent: this.config.bool('persistent-mempool'), - maxSize: this.config.mb('mempool-size'), - limitFree: this.config.bool('limit-free'), - limitFreeRelay: this.config.uint('limit-free-relay'), - requireStandard: this.config.bool('require-standard'), - rejectAbsurdFees: this.config.bool('reject-absurd-fees'), - replaceByFee: this.config.bool('replace-by-fee'), - indexAddress: this.config.bool('index-address') - }); + // Mempool needs access to the chain. + this.mempool = new Mempool({ + network: this.network, + logger: this.logger, + workers: this.workers, + chain: this.chain, + fees: this.fees, + db: this.config.str('db'), + prefix: this.config.prefix, + persistent: this.config.bool('persistent-mempool'), + maxSize: this.config.mb('mempool-size'), + limitFree: this.config.bool('limit-free'), + limitFreeRelay: this.config.uint('limit-free-relay'), + requireStandard: this.config.bool('require-standard'), + rejectAbsurdFees: this.config.bool('reject-absurd-fees'), + replaceByFee: this.config.bool('replace-by-fee'), + indexAddress: this.config.bool('index-address') + }); - // Pool needs access to the chain and mempool. - this.pool = new Pool({ - network: this.network, - logger: this.logger, - chain: this.chain, - mempool: this.mempool, - prefix: this.config.prefix, - selfish: this.config.bool('selfish'), - compact: this.config.bool('compact'), - bip37: this.config.bool('bip37'), - bip151: this.config.bool('bip151'), - bip150: this.config.bool('bip150'), - identityKey: this.config.buf('identity-key'), - maxOutbound: this.config.uint('max-outbound'), - maxInbound: this.config.uint('max-inbound'), - createSocket: this.config.func('create-socket'), - proxy: this.config.str('proxy'), - onion: this.config.bool('onion'), - upnp: this.config.bool('upnp'), - seeds: this.config.array('seeds'), - nodes: this.config.array('nodes'), - only: this.config.array('only'), - publicHost: this.config.str('public-host'), - publicPort: this.config.uint('public-port'), - host: this.config.str('host'), - port: this.config.uint('port'), - listen: this.config.bool('listen'), - persistent: this.config.bool('persistent') - }); + // Pool needs access to the chain and mempool. + this.pool = new Pool({ + network: this.network, + logger: this.logger, + chain: this.chain, + mempool: this.mempool, + prefix: this.config.prefix, + selfish: this.config.bool('selfish'), + compact: this.config.bool('compact'), + bip37: this.config.bool('bip37'), + bip151: this.config.bool('bip151'), + bip150: this.config.bool('bip150'), + identityKey: this.config.buf('identity-key'), + maxOutbound: this.config.uint('max-outbound'), + maxInbound: this.config.uint('max-inbound'), + createSocket: this.config.func('create-socket'), + proxy: this.config.str('proxy'), + onion: this.config.bool('onion'), + upnp: this.config.bool('upnp'), + seeds: this.config.array('seeds'), + nodes: this.config.array('nodes'), + only: this.config.array('only'), + publicHost: this.config.str('public-host'), + publicPort: this.config.uint('public-port'), + host: this.config.str('host'), + port: this.config.uint('port'), + listen: this.config.bool('listen'), + persistent: this.config.bool('persistent') + }); - // Miner needs access to the chain and mempool. - this.miner = new Miner({ - network: this.network, - logger: this.logger, - workers: this.workers, - chain: this.chain, - mempool: this.mempool, - address: this.config.array('coinbase-address'), - coinbaseFlags: this.config.str('coinbase-flags'), - preverify: this.config.bool('preverify'), - maxWeight: this.config.uint('max-weight'), - reservedWeight: this.config.uint('reserved-weight'), - reservedSigops: this.config.uint('reserved-sigops') - }); + // Miner needs access to the chain and mempool. + this.miner = new Miner({ + network: this.network, + logger: this.logger, + workers: this.workers, + chain: this.chain, + mempool: this.mempool, + address: this.config.array('coinbase-address'), + coinbaseFlags: this.config.str('coinbase-flags'), + preverify: this.config.bool('preverify'), + maxWeight: this.config.uint('max-weight'), + reservedWeight: this.config.uint('reserved-weight'), + reservedSigops: this.config.uint('reserved-sigops') + }); - // RPC needs access to the node. - this.rpc = new RPC(this); + // RPC needs access to the node. + this.rpc = new RPC(this); - // HTTP needs access to the node. - this.http = new HTTP({ - network: this.network, - logger: this.logger, - node: this, - prefix: this.config.prefix, - ssl: this.config.bool('ssl'), - keyFile: this.config.path('ssl-key'), - certFile: this.config.path('ssl-cert'), - host: this.config.str('http-host'), - port: this.config.uint('http-port'), - apiKey: this.config.str('api-key'), - noAuth: this.config.bool('no-auth') - }); + // HTTP needs access to the node. + this.http = new HTTP({ + network: this.network, + logger: this.logger, + node: this, + prefix: this.config.prefix, + ssl: this.config.bool('ssl'), + keyFile: this.config.path('ssl-key'), + certFile: this.config.path('ssl-cert'), + host: this.config.str('http-host'), + port: this.config.uint('http-port'), + apiKey: this.config.str('api-key'), + noAuth: this.config.bool('no-auth') + }); - this.init(); -} - -Object.setPrototypeOf(FullNode.prototype, Node.prototype); - -/** - * Initialize the node. - * @private - */ - -FullNode.prototype.init = function init() { - // Bind to errors - this.chain.on('error', err => this.error(err)); - this.mempool.on('error', err => this.error(err)); - this.pool.on('error', err => this.error(err)); - this.miner.on('error', err => this.error(err)); - - if (this.http) - this.http.on('error', err => this.error(err)); - - this.mempool.on('tx', (tx) => { - this.miner.cpu.notifyEntry(); - this.emit('tx', tx); - }); - - this.chain.on('connect', async (entry, block) => { - try { - await this.mempool._addBlock(entry, block.txs); - } catch (e) { - this.error(e); - } - this.emit('block', block); - this.emit('connect', entry, block); - }); - - this.chain.on('disconnect', async (entry, block) => { - try { - await this.mempool._removeBlock(entry, block.txs); - } catch (e) { - this.error(e); - } - this.emit('disconnect', entry, block); - }); - - this.chain.on('reorganize', async (tip, competitor) => { - try { - await this.mempool._handleReorg(); - } catch (e) { - this.error(e); - } - this.emit('reorganize', tip, competitor); - }); - - this.chain.on('reset', async (tip) => { - try { - await this.mempool._reset(); - } catch (e) { - this.error(e); - } - this.emit('reset', tip); - }); - - this.loadPlugins(); -}; - -/** - * Open the node and all its child objects, - * wait for the database to load. - * @alias FullNode#open - * @returns {Promise} - */ - -FullNode.prototype.open = async function open() { - assert(!this.opened, 'FullNode is already open.'); - this.opened = true; - - await this.handlePreopen(); - await this.chain.open(); - await this.mempool.open(); - await this.miner.open(); - await this.pool.open(); - - await this.openPlugins(); - - await this.http.open(); - await this.handleOpen(); - - this.logger.info('Node is loaded.'); -}; - -/** - * Close the node, wait for the database to close. - * @alias FullNode#close - * @returns {Promise} - */ - -FullNode.prototype.close = async function close() { - assert(this.opened, 'FullNode is not open.'); - this.opened = false; - - await this.handlePreclose(); - await this.http.close(); - - await this.closePlugins(); - - await this.pool.close(); - await this.miner.close(); - await this.mempool.close(); - await this.chain.close(); - await this.handleClose(); - - this.logger.info('Node is closed.'); -}; - -/** - * Rescan for any missed transactions. - * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - -FullNode.prototype.scan = function scan(start, filter, iter) { - return this.chain.scan(start, filter, iter); -}; - -/** - * Broadcast a transaction (note that this will _not_ be verified - * by the mempool - use with care, lest you get banned from - * bitcoind nodes). - * @param {TX|Block} item - * @returns {Promise} - */ - -FullNode.prototype.broadcast = async function broadcast(item) { - try { - await this.pool.broadcast(item); - } catch (e) { - this.emit('error', e); + this.init(); } -}; -/** - * Add transaction to mempool, broadcast. - * @param {TX} tx - */ + /** + * Initialize the node. + * @private + */ -FullNode.prototype.sendTX = async function sendTX(tx) { - let missing; + init() { + // Bind to errors + this.chain.on('error', err => this.error(err)); + this.mempool.on('error', err => this.error(err)); + this.pool.on('error', err => this.error(err)); + this.miner.on('error', err => this.error(err)); - try { - missing = await this.mempool.addTX(tx); - } catch (err) { - if (err.type === 'VerifyError' && err.score === 0) { - this.error(err); - this.logger.warning('Verification failed for tx: %s.', tx.txid()); + if (this.http) + this.http.on('error', err => this.error(err)); + + this.mempool.on('tx', (tx) => { + this.miner.cpu.notifyEntry(); + this.emit('tx', tx); + }); + + this.chain.on('connect', async (entry, block) => { + try { + await this.mempool._addBlock(entry, block.txs); + } catch (e) { + this.error(e); + } + this.emit('block', block); + this.emit('connect', entry, block); + }); + + this.chain.on('disconnect', async (entry, block) => { + try { + await this.mempool._removeBlock(entry, block.txs); + } catch (e) { + this.error(e); + } + this.emit('disconnect', entry, block); + }); + + this.chain.on('reorganize', async (tip, competitor) => { + try { + await this.mempool._handleReorg(); + } catch (e) { + this.error(e); + } + this.emit('reorganize', tip, competitor); + }); + + this.chain.on('reset', async (tip) => { + try { + await this.mempool._reset(); + } catch (e) { + this.error(e); + } + this.emit('reset', tip); + }); + + this.loadPlugins(); + } + + /** + * Open the node and all its child objects, + * wait for the database to load. + * @alias FullNode#open + * @returns {Promise} + */ + + async open() { + assert(!this.opened, 'FullNode is already open.'); + this.opened = true; + + await this.handlePreopen(); + await this.chain.open(); + await this.mempool.open(); + await this.miner.open(); + await this.pool.open(); + + await this.openPlugins(); + + await this.http.open(); + await this.handleOpen(); + + this.logger.info('Node is loaded.'); + } + + /** + * Close the node, wait for the database to close. + * @alias FullNode#close + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'FullNode is not open.'); + this.opened = false; + + await this.handlePreclose(); + await this.http.close(); + + await this.closePlugins(); + + await this.pool.close(); + await this.miner.close(); + await this.mempool.close(); + await this.chain.close(); + await this.handleClose(); + + this.logger.info('Node is closed.'); + } + + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + scan(start, filter, iter) { + return this.chain.scan(start, filter, iter); + } + + /** + * Broadcast a transaction (note that this will _not_ be verified + * by the mempool - use with care, lest you get banned from + * bitcoind nodes). + * @param {TX|Block} item + * @returns {Promise} + */ + + async broadcast(item) { + try { + await this.pool.broadcast(item); + } catch (e) { + this.emit('error', e); + } + } + + /** + * Add transaction to mempool, broadcast. + * @param {TX} tx + */ + + async sendTX(tx) { + let missing; + + try { + missing = await this.mempool.addTX(tx); + } catch (err) { + if (err.type === 'VerifyError' && err.score === 0) { + this.error(err); + this.logger.warning('Verification failed for tx: %s.', tx.txid()); + this.logger.warning('Attempting to broadcast anyway...'); + this.broadcast(tx); + return; + } + throw err; + } + + if (missing) { + this.logger.warning('TX was orphaned in mempool: %s.', tx.txid()); this.logger.warning('Attempting to broadcast anyway...'); this.broadcast(tx); return; } - throw err; + + // We need to announce by hand if + // we're running in selfish mode. + if (this.pool.options.selfish) + this.pool.broadcast(tx); } - if (missing) { - this.logger.warning('TX was orphaned in mempool: %s.', tx.txid()); - this.logger.warning('Attempting to broadcast anyway...'); - this.broadcast(tx); - return; + /** + * Add transaction to mempool, broadcast. Silence errors. + * @param {TX} tx + * @returns {Promise} + */ + + async relay(tx) { + try { + await this.sendTX(tx); + } catch (e) { + this.error(e); + } } - // We need to announce by hand if - // we're running in selfish mode. - if (this.pool.options.selfish) - this.pool.broadcast(tx); -}; + /** + * Connect to the network. + * @returns {Promise} + */ -/** - * Add transaction to mempool, broadcast. Silence errors. - * @param {TX} tx - * @returns {Promise} - */ - -FullNode.prototype.relay = async function relay(tx) { - try { - await this.sendTX(tx); - } catch (e) { - this.error(e); - } -}; - -/** - * Connect to the network. - * @returns {Promise} - */ - -FullNode.prototype.connect = function connect() { - return this.pool.connect(); -}; - -/** - * Disconnect from the network. - * @returns {Promise} - */ - -FullNode.prototype.disconnect = function disconnect() { - return this.pool.disconnect(); -}; - -/** - * Start the blockchain sync. - */ - -FullNode.prototype.startSync = function startSync() { - return this.pool.startSync(); -}; - -/** - * Stop syncing the blockchain. - */ - -FullNode.prototype.stopSync = function stopSync() { - return this.pool.stopSync(); -}; - -/** - * Retrieve a block from the chain database. - * @param {Hash} hash - * @returns {Promise} - Returns {@link Block}. - */ - -FullNode.prototype.getBlock = function getBlock(hash) { - return this.chain.getBlock(hash); -}; - -/** - * Retrieve a coin from the mempool or chain database. - * Takes into account spent coins in the mempool. - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - Returns {@link Coin}. - */ - -FullNode.prototype.getCoin = async function getCoin(hash, index) { - const coin = this.mempool.getCoin(hash, index); - - if (coin) - return coin; - - if (this.mempool.isSpent(hash, index)) - return null; - - return await this.chain.getCoin(hash, index); -}; - -/** - * Get coins that pertain to an address from the mempool or chain database. - * Takes into account spent coins in the mempool. - * @param {Address} addrs - * @returns {Promise} - Returns {@link Coin}[]. - */ - -FullNode.prototype.getCoinsByAddress = async function getCoinsByAddress(addrs) { - const mempool = this.mempool.getCoinsByAddress(addrs); - const chain = await this.chain.getCoinsByAddress(addrs); - const out = []; - - for (const coin of chain) { - const spent = this.mempool.isSpent(coin.hash, coin.index); - - if (spent) - continue; - - out.push(coin); + connect() { + return this.pool.connect(); } - for (const coin of mempool) - out.push(coin); + /** + * Disconnect from the network. + * @returns {Promise} + */ - return out; -}; + disconnect() { + return this.pool.disconnect(); + } -/** - * Retrieve transactions pertaining to an - * address from the mempool or chain database. - * @param {Address} addrs - * @returns {Promise} - Returns {@link TXMeta}[]. - */ + /** + * Start the blockchain sync. + */ -FullNode.prototype.getMetaByAddress = async function getMetaByAddress(addrs) { - const mempool = this.mempool.getMetaByAddress(addrs); - const chain = await this.chain.getMetaByAddress(addrs); - return chain.concat(mempool); -}; + startSync() { + return this.pool.startSync(); + } -/** - * Retrieve a transaction from the mempool or chain database. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TXMeta}. - */ + /** + * Stop syncing the blockchain. + */ -FullNode.prototype.getMeta = async function getMeta(hash) { - const meta = this.mempool.getMeta(hash); + stopSync() { + return this.pool.stopSync(); + } - if (meta) - return meta; + /** + * Retrieve a block from the chain database. + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ - return await this.chain.getMeta(hash); -}; + getBlock(hash) { + return this.chain.getBlock(hash); + } -/** - * Retrieve a spent coin viewpoint from mempool or chain database. - * @param {TXMeta} meta - * @returns {Promise} - Returns {@link CoinView}. - */ + /** + * Retrieve a coin from the mempool or chain database. + * Takes into account spent coins in the mempool. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ -FullNode.prototype.getMetaView = async function getMetaView(meta) { - if (meta.height === -1) - return this.mempool.getSpentView(meta.tx); - return this.chain.getSpentView(meta.tx); -}; + async getCoin(hash, index) { + const coin = this.mempool.getCoin(hash, index); -/** - * Retrieve transactions pertaining to an - * address from the mempool or chain database. - * @param {Address} addrs - * @returns {Promise} - Returns {@link TX}[]. - */ + if (coin) + return coin; -FullNode.prototype.getTXByAddress = async function getTXByAddress(addrs) { - const mtxs = await this.getMetaByAddress(addrs); - const out = []; + if (this.mempool.isSpent(hash, index)) + return null; - for (const mtx of mtxs) - out.push(mtx.tx); + return this.chain.getCoin(hash, index); + } - return out; -}; + /** + * Get coins that pertain to an address from the mempool or chain database. + * Takes into account spent coins in the mempool. + * @param {Address} addrs + * @returns {Promise} - Returns {@link Coin}[]. + */ -/** - * Retrieve a transaction from the mempool or chain database. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ + async getCoinsByAddress(addrs) { + const mempool = this.mempool.getCoinsByAddress(addrs); + const chain = await this.chain.getCoinsByAddress(addrs); + const out = []; -FullNode.prototype.getTX = async function getTX(hash) { - const mtx = await this.getMeta(hash); + for (const coin of chain) { + const spent = this.mempool.isSpent(coin.hash, coin.index); - if (!mtx) - return null; + if (spent) + continue; - return mtx.tx; -}; + out.push(coin); + } -/** - * Test whether the mempool or chain contains a transaction. - * @param {Hash} hash - * @returns {Promise} - Returns Boolean. - */ + for (const coin of mempool) + out.push(coin); -FullNode.prototype.hasTX = async function hasTX(hash) { - if (this.mempool.hasEntry(hash)) - return true; + return out; + } - return await this.chain.hasTX(hash); -}; + /** + * Retrieve transactions pertaining to an + * address from the mempool or chain database. + * @param {Address} addrs + * @returns {Promise} - Returns {@link TXMeta}[]. + */ + + async getMetaByAddress(addrs) { + const mempool = this.mempool.getMetaByAddress(addrs); + const chain = await this.chain.getMetaByAddress(addrs); + return chain.concat(mempool); + } + + /** + * Retrieve a transaction from the mempool or chain database. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TXMeta}. + */ + + async getMeta(hash) { + const meta = this.mempool.getMeta(hash); + + if (meta) + return meta; + + return this.chain.getMeta(hash); + } + + /** + * Retrieve a spent coin viewpoint from mempool or chain database. + * @param {TXMeta} meta + * @returns {Promise} - Returns {@link CoinView}. + */ + + async getMetaView(meta) { + if (meta.height === -1) + return this.mempool.getSpentView(meta.tx); + return this.chain.getSpentView(meta.tx); + } + + /** + * Retrieve transactions pertaining to an + * address from the mempool or chain database. + * @param {Address} addrs + * @returns {Promise} - Returns {@link TX}[]. + */ + + async getTXByAddress(addrs) { + const mtxs = await this.getMetaByAddress(addrs); + const out = []; + + for (const mtx of mtxs) + out.push(mtx.tx); + + return out; + } + + /** + * Retrieve a transaction from the mempool or chain database. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + async getTX(hash) { + const mtx = await this.getMeta(hash); + + if (!mtx) + return null; + + return mtx.tx; + } + + /** + * Test whether the mempool or chain contains a transaction. + * @param {Hash} hash + * @returns {Promise} - Returns Boolean. + */ + + async hasTX(hash) { + if (this.mempool.hasEntry(hash)) + return true; + + return this.chain.hasTX(hash); + } +} /* * Expose diff --git a/lib/node/http.js b/lib/node/http.js index 49c6a96b..3f631ea9 100644 --- a/lib/node/http.js +++ b/lib/node/http.js @@ -23,15 +23,16 @@ const Outpoint = require('../primitives/outpoint'); const Network = require('../protocol/network'); const pkg = require('../pkg'); +/** + * HTTP + * @alias module:http.Server + */ + class HTTP extends Server { /** - * HTTP - * @alias module:http.Server + * Create an http server. * @constructor * @param {Object} options - * @param {Fullnode} options.node - * @see HTTPBase - * @emits HTTP#socket */ constructor(options) { diff --git a/lib/node/node.js b/lib/node/node.js index ec5989dd..3bb82029 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -16,381 +16,384 @@ const Network = require('../protocol/network'); const WorkerPool = require('../workers/workerpool'); /** + * Node * Base class from which every other * Node-like object inherits. * @alias module:node.Node - * @constructor + * @extends EventEmitter * @abstract - * @param {Object} options */ -function Node(module, config, file, options) { - if (!(this instanceof Node)) - return new Node(module, config, file, options); +class Node extends EventEmitter { + /** + * Create a node. + * @constructor + * @param {Object} options + */ - EventEmitter.call(this); + constructor(module, config, file, options) { + super(); - this.config = new Config(module, { - suffix: 'network', - fallback: 'main', - alias: { 'n': 'network' } - }); + this.config = new Config(module, { + suffix: 'network', + fallback: 'main', + alias: { 'n': 'network' } + }); - this.config.inject(options); - this.config.load(options); + this.config.inject(options); + this.config.load(options); - if (options.config) - this.config.open(config); + if (options.config) + this.config.open(config); - this.network = Network.get(this.config.getSuffix()); - this.startTime = -1; - this.bound = []; - this.plugins = Object.create(null); - this.stack = []; + this.network = Network.get(this.config.getSuffix()); + this.startTime = -1; + this.bound = []; + this.plugins = Object.create(null); + this.stack = []; - this.logger = null; - this.workers = null; + this.logger = null; + this.workers = null; - this.spv = false; - this.chain = null; - this.fees = null; - this.mempool = null; - this.pool = null; - this.miner = null; - this.http = null; + this.spv = false; + this.chain = null; + this.fees = null; + this.mempool = null; + this.pool = null; + this.miner = null; + this.http = null; - this._init(file); -} + this._init(file); + } -Object.setPrototypeOf(Node.prototype, EventEmitter.prototype); + /** + * Initialize node. + * @private + * @param {Object} options + */ -/** - * Initialize node. - * @private - * @param {Object} options - */ + _init(file) { + const config = this.config; -Node.prototype._init = function _init(file) { - const config = this.config; + let logger = new Logger(); - let logger = new Logger(); + if (config.has('logger')) + logger = config.obj('logger'); - if (config.has('logger')) - logger = config.obj('logger'); + logger.set({ + filename: config.bool('log-file') + ? config.location(file) + : null, + level: config.str('log-level'), + console: config.bool('log-console'), + shrink: config.bool('log-shrink') + }); - logger.set({ - filename: config.bool('log-file') - ? config.location(file) - : null, - level: config.str('log-level'), - console: config.bool('log-console'), - shrink: config.bool('log-shrink') - }); + this.logger = logger.context('node'); - this.logger = logger.context('node'); + this.workers = new WorkerPool({ + enabled: config.bool('workers'), + size: config.uint('workers-size'), + timeout: config.uint('workers-timeout'), + file: config.str('worker-file') + }); - this.workers = new WorkerPool({ - enabled: config.bool('workers'), - size: config.uint('workers-size'), - timeout: config.uint('workers-timeout'), - file: config.str('worker-file') - }); + this.on('error', () => {}); - this.on('error', () => {}); + this.workers.on('spawn', (child) => { + this.logger.info('Spawning worker process: %d.', child.id); + }); - this.workers.on('spawn', (child) => { - this.logger.info('Spawning worker process: %d.', child.id); - }); + this.workers.on('exit', (code, child) => { + this.logger.warning('Worker %d exited: %s.', child.id, code); + }); - this.workers.on('exit', (code, child) => { - this.logger.warning('Worker %d exited: %s.', child.id, code); - }); + this.workers.on('log', (text, child) => { + this.logger.debug('Worker %d says:', child.id); + this.logger.debug(text); + }); - this.workers.on('log', (text, child) => { - this.logger.debug('Worker %d says:', child.id); - this.logger.debug(text); - }); + this.workers.on('error', (err, child) => { + if (child) { + this.logger.error('Worker %d error: %s', child.id, err.message); + return; + } + this.emit('error', err); + }); + } - this.workers.on('error', (err, child) => { - if (child) { - this.logger.error('Worker %d error: %s', child.id, err.message); - return; + /** + * Ensure prefix directory. + * @returns {Promise} + */ + + async ensure() { + if (fs.unsupported) + return undefined; + + return fs.mkdirp(this.config.prefix); + } + + /** + * Create a file path using `prefix`. + * @param {String} file + * @returns {String} + */ + + location(name) { + return this.config.location(name); + } + + /** + * Open node. Bind all events. + * @private + */ + + async handlePreopen() { + await this.logger.open(); + await this.workers.open(); + + this._bind(this.network.time, 'offset', (offset) => { + this.logger.info('Time offset: %d (%d minutes).', offset, offset / 60 | 0); + }); + + this._bind(this.network.time, 'sample', (sample, total) => { + this.logger.debug( + 'Added time data: samples=%d, offset=%d (%d minutes).', + total, sample, sample / 60 | 0); + }); + + this._bind(this.network.time, 'mismatch', () => { + this.logger.warning('Adjusted time mismatch!'); + this.logger.warning('Please make sure your system clock is correct!'); + }); + } + + /** + * Open node. + * @private + */ + + async handleOpen() { + this.startTime = Date.now(); + + if (!this.workers.enabled) { + this.logger.warning('Warning: worker pool is disabled.'); + this.logger.warning('Verification will be slow.'); } + } + + /** + * Open node. Bind all events. + * @private + */ + + async handlePreclose() { + ; + } + + /** + * Close node. Unbind all events. + * @private + */ + + async handleClose() { + for (const [obj, event, listener] of this.bound) + obj.removeListener(event, listener); + + this.bound.length = 0; + this.startTime = -1; + + await this.workers.close(); + await this.logger.close(); + } + + /** + * Bind to an event on `obj`, save listener for removal. + * @private + * @param {EventEmitter} obj + * @param {String} event + * @param {Function} listener + */ + + _bind(obj, event, listener) { + this.bound.push([obj, event, listener]); + obj.on(event, listener); + } + + /** + * Emit and log an error. + * @private + * @param {Error} err + */ + + error(err) { + this.logger.error(err); this.emit('error', err); - }); -}; - -/** - * Ensure prefix directory. - * @returns {Promise} - */ - -Node.prototype.ensure = async function ensure() { - if (fs.unsupported) - return undefined; - - return fs.mkdirp(this.config.prefix); -}; - -/** - * Create a file path using `prefix`. - * @param {String} file - * @returns {String} - */ - -Node.prototype.location = function location(name) { - return this.config.location(name); -}; - -/** - * Open node. Bind all events. - * @private - */ - -Node.prototype.handlePreopen = async function handlePreopen() { - await this.logger.open(); - await this.workers.open(); - - this._bind(this.network.time, 'offset', (offset) => { - this.logger.info('Time offset: %d (%d minutes).', offset, offset / 60 | 0); - }); - - this._bind(this.network.time, 'sample', (sample, total) => { - this.logger.debug( - 'Added time data: samples=%d, offset=%d (%d minutes).', - total, sample, sample / 60 | 0); - }); - - this._bind(this.network.time, 'mismatch', () => { - this.logger.warning('Adjusted time mismatch!'); - this.logger.warning('Please make sure your system clock is correct!'); - }); -}; - -/** - * Open node. - * @private - */ - -Node.prototype.handleOpen = async function handleOpen() { - this.startTime = Date.now(); - - if (!this.workers.enabled) { - this.logger.warning('Warning: worker pool is disabled.'); - this.logger.warning('Verification will be slow.'); } -}; -/** - * Open node. Bind all events. - * @private - */ + /** + * Get node uptime in seconds. + * @returns {Number} + */ -Node.prototype.handlePreclose = async function handlePreclose() { - ; -}; + uptime() { + if (this.startTime === -1) + return 0; -/** - * Close node. Unbind all events. - * @private - */ + return Math.floor((Date.now() - this.startTime) / 1000); + } -Node.prototype.handleClose = async function handleClose() { - for (const [obj, event, listener] of this.bound) - obj.removeListener(event, listener); + /** + * Attach a plugin. + * @param {Object} plugin + * @returns {Object} Plugin instance. + */ - this.bound.length = 0; - this.startTime = -1; + use(plugin) { + assert(plugin, 'Plugin must be an object.'); + assert(typeof plugin.init === 'function', '`init` must be a function.'); - await this.workers.close(); - await this.logger.close(); -}; + assert(!this.loaded, 'Cannot add plugin after node is loaded.'); -/** - * Bind to an event on `obj`, save listener for removal. - * @private - * @param {EventEmitter} obj - * @param {String} event - * @param {Function} listener - */ + const instance = plugin.init(this); -Node.prototype._bind = function _bind(obj, event, listener) { - this.bound.push([obj, event, listener]); - obj.on(event, listener); -}; + assert(!instance.open || typeof instance.open === 'function', + '`open` must be a function.'); + assert(!instance.close || typeof instance.close === 'function', + '`close` must be a function.'); -/** - * Emit and log an error. - * @private - * @param {Error} err - */ + if (plugin.id) { + assert(typeof plugin.id === 'string', '`id` must be a string.'); -Node.prototype.error = function error(err) { - this.logger.error(err); - this.emit('error', err); -}; + // Reserved names + switch (plugin.id) { + case 'chain': + case 'fees': + case 'mempool': + case 'miner': + case 'pool': + case 'rpc': + case 'http': + assert(false, `${plugin.id} is already added.`); + break; + } -/** - * Get node uptime in seconds. - * @returns {Number} - */ + assert(!this.plugins[plugin.id], `${plugin.id} is already added.`); -Node.prototype.uptime = function uptime() { - if (this.startTime === -1) - return 0; + this.plugins[plugin.id] = instance; + } - return Math.floor((Date.now() - this.startTime) / 1000); -}; + this.stack.push(instance); -/** - * Attach a plugin. - * @param {Object} plugin - * @returns {Object} Plugin instance. - */ + if (typeof instance.on === 'function') + instance.on('error', err => this.error(err)); -Node.prototype.use = function use(plugin) { - assert(plugin, 'Plugin must be an object.'); - assert(typeof plugin.init === 'function', '`init` must be a function.'); + return instance; + } - assert(!this.loaded, 'Cannot add plugin after node is loaded.'); + /** + * Test whether a plugin is available. + * @param {String} name + * @returns {Boolean} + */ - const instance = plugin.init(this); + has(name) { + return this.plugins[name] != null; + } - assert(!instance.open || typeof instance.open === 'function', - '`open` must be a function.'); - assert(!instance.close || typeof instance.close === 'function', - '`close` must be a function.'); + /** + * Get a plugin. + * @param {String} name + * @returns {Object|null} + */ - if (plugin.id) { - assert(typeof plugin.id === 'string', '`id` must be a string.'); + get(name) { + assert(typeof name === 'string', 'Plugin name must be a string.'); - // Reserved names - switch (plugin.id) { + // Reserved names. + switch (name) { case 'chain': + assert(this.chain, 'chain is not loaded.'); + return this.chain; case 'fees': + assert(this.fees, 'fees is not loaded.'); + return this.fees; case 'mempool': + assert(this.mempool, 'mempool is not loaded.'); + return this.mempool; case 'miner': + assert(this.miner, 'miner is not loaded.'); + return this.miner; case 'pool': + assert(this.pool, 'pool is not loaded.'); + return this.pool; case 'rpc': + assert(this.rpc, 'rpc is not loaded.'); + return this.rpc; case 'http': - assert(false, `${plugin.id} is already added.`); - break; + assert(this.http, 'http is not loaded.'); + return this.http; } - assert(!this.plugins[plugin.id], `${plugin.id} is already added.`); - - this.plugins[plugin.id] = instance; + return this.plugins[name] || null; } - this.stack.push(instance); + /** + * Require a plugin. + * @param {String} name + * @returns {Object} + * @throws {Error} on onloaded plugin + */ - if (typeof instance.on === 'function') - instance.on('error', err => this.error(err)); - - return instance; -}; - -/** - * Test whether a plugin is available. - * @param {String} name - * @returns {Boolean} - */ - -Node.prototype.has = function has(name) { - return this.plugins[name] != null; -}; - -/** - * Get a plugin. - * @param {String} name - * @returns {Object|null} - */ - -Node.prototype.get = function get(name) { - assert(typeof name === 'string', 'Plugin name must be a string.'); - - // Reserved names. - switch (name) { - case 'chain': - assert(this.chain, 'chain is not loaded.'); - return this.chain; - case 'fees': - assert(this.fees, 'fees is not loaded.'); - return this.fees; - case 'mempool': - assert(this.mempool, 'mempool is not loaded.'); - return this.mempool; - case 'miner': - assert(this.miner, 'miner is not loaded.'); - return this.miner; - case 'pool': - assert(this.pool, 'pool is not loaded.'); - return this.pool; - case 'rpc': - assert(this.rpc, 'rpc is not loaded.'); - return this.rpc; - case 'http': - assert(this.http, 'http is not loaded.'); - return this.http; + require(name) { + const plugin = this.get(name); + assert(plugin, `${name} is not loaded.`); + return plugin; } - return this.plugins[name] || null; -}; + /** + * Load plugins. + * @private + */ -/** - * Require a plugin. - * @param {String} name - * @returns {Object} - * @throws {Error} on onloaded plugin - */ + loadPlugins() { + const plugins = this.config.array('plugins', []); + const loader = this.config.func('loader'); -Node.prototype.require = function require(name) { - const plugin = this.get(name); - assert(plugin, `${name} is not loaded.`); - return plugin; -}; - -/** - * Load plugins. - * @private - */ - -Node.prototype.loadPlugins = function loadPlugins() { - const plugins = this.config.array('plugins', []); - const loader = this.config.func('loader'); - - for (let plugin of plugins) { - if (typeof plugin === 'string') { - assert(loader, 'Must pass a loader function.'); - plugin = loader(plugin); + for (let plugin of plugins) { + if (typeof plugin === 'string') { + assert(loader, 'Must pass a loader function.'); + plugin = loader(plugin); + } + this.use(plugin); } - this.use(plugin); } -}; -/** - * Open plugins. - * @private - */ + /** + * Open plugins. + * @private + */ -Node.prototype.openPlugins = async function openPlugins() { - for (const plugin of this.stack) { - if (plugin.open) - await plugin.open(); + async openPlugins() { + for (const plugin of this.stack) { + if (plugin.open) + await plugin.open(); + } } -}; -/** - * Close plugins. - * @private - */ + /** + * Close plugins. + * @private + */ -Node.prototype.closePlugins = async function closePlugins() { - for (const plugin of this.stack) { - if (plugin.close) - await plugin.close(); + async closePlugins() { + for (const plugin of this.stack) { + if (plugin.close) + await plugin.close(); + } } -}; +} /* * Expose diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 613ae2cb..29c589dd 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -16,335 +16,332 @@ const HTTP = require('./http'); const RPC = require('../rpc'); /** + * SPV Node * Create an spv node which only maintains * a chain, a pool, and an http server. * @alias module:node.SPVNode * @extends Node - * @constructor - * @param {Object?} options - * @param {Buffer?} options.sslKey - * @param {Buffer?} options.sslCert - * @param {Number?} options.httpPort - * @param {String?} options.httpHost - * @property {Boolean} loaded - * @property {Chain} chain - * @property {Pool} pool - * @property {HTTP} http - * @emits SPVNode#block - * @emits SPVNode#tx - * @emits SPVNode#error */ -function SPVNode(options) { - if (!(this instanceof SPVNode)) - return new SPVNode(options); +class SPVNode extends Node { + /** + * Create SPV node. + * @constructor + * @param {Object?} options + * @param {Buffer?} options.sslKey + * @param {Buffer?} options.sslCert + * @param {Number?} options.httpPort + * @param {String?} options.httpHost + */ - Node.call(this, 'bcoin', 'bcoin.conf', 'debug.log', options); + constructor(options) { + super('bcoin', 'bcoin.conf', 'debug.log', options); - this.opened = false; + this.opened = false; - // SPV flag. - this.spv = true; + // SPV flag. + this.spv = true; - this.chain = new Chain({ - network: this.network, - logger: this.logger, - db: this.config.str('db'), - prefix: this.config.prefix, - maxFiles: this.config.uint('max-files'), - cacheSize: this.config.mb('cache-size'), - entryCache: this.config.uint('entry-cache'), - forceFlags: this.config.bool('force-flags'), - checkpoints: this.config.bool('checkpoints'), - bip91: this.config.bool('bip91'), - bip148: this.config.bool('bip148'), - spv: true - }); + this.chain = new Chain({ + network: this.network, + logger: this.logger, + db: this.config.str('db'), + prefix: this.config.prefix, + maxFiles: this.config.uint('max-files'), + cacheSize: this.config.mb('cache-size'), + entryCache: this.config.uint('entry-cache'), + forceFlags: this.config.bool('force-flags'), + checkpoints: this.config.bool('checkpoints'), + bip91: this.config.bool('bip91'), + bip148: this.config.bool('bip148'), + spv: true + }); - this.pool = new Pool({ - network: this.network, - logger: this.logger, - chain: this.chain, - prefix: this.config.prefix, - proxy: this.config.str('proxy'), - onion: this.config.bool('onion'), - upnp: this.config.bool('upnp'), - seeds: this.config.array('seeds'), - nodes: this.config.array('nodes'), - only: this.config.array('only'), - bip151: this.config.bool('bip151'), - bip150: this.config.bool('bip150'), - identityKey: this.config.buf('identity-key'), - maxOutbound: this.config.uint('max-outbound'), - createSocket: this.config.func('create-socket'), - persistent: this.config.bool('persistent'), - selfish: true, - listen: false - }); + this.pool = new Pool({ + network: this.network, + logger: this.logger, + chain: this.chain, + prefix: this.config.prefix, + proxy: this.config.str('proxy'), + onion: this.config.bool('onion'), + upnp: this.config.bool('upnp'), + seeds: this.config.array('seeds'), + nodes: this.config.array('nodes'), + only: this.config.array('only'), + bip151: this.config.bool('bip151'), + bip150: this.config.bool('bip150'), + identityKey: this.config.buf('identity-key'), + maxOutbound: this.config.uint('max-outbound'), + createSocket: this.config.func('create-socket'), + persistent: this.config.bool('persistent'), + selfish: true, + listen: false + }); - this.rpc = new RPC(this); + this.rpc = new RPC(this); - this.http = new HTTP({ - network: this.network, - logger: this.logger, - node: this, - prefix: this.config.prefix, - ssl: this.config.bool('ssl'), - keyFile: this.config.path('ssl-key'), - certFile: this.config.path('ssl-cert'), - host: this.config.str('http-host'), - port: this.config.uint('http-port'), - apiKey: this.config.str('api-key'), - noAuth: this.config.bool('no-auth') - }); + this.http = new HTTP({ + network: this.network, + logger: this.logger, + node: this, + prefix: this.config.prefix, + ssl: this.config.bool('ssl'), + keyFile: this.config.path('ssl-key'), + certFile: this.config.path('ssl-cert'), + host: this.config.str('http-host'), + port: this.config.uint('http-port'), + apiKey: this.config.str('api-key'), + noAuth: this.config.bool('no-auth') + }); - this.rescanJob = null; - this.scanLock = new Lock(); - this.watchLock = new Lock(); + this.rescanJob = null; + this.scanLock = new Lock(); + this.watchLock = new Lock(); - this.init(); -} + this.init(); + } -Object.setPrototypeOf(SPVNode.prototype, Node.prototype); + /** + * Initialize the node. + * @private + */ -/** - * Initialize the node. - * @private - */ + init() { + // Bind to errors + this.chain.on('error', err => this.error(err)); + this.pool.on('error', err => this.error(err)); -SPVNode.prototype.init = function init() { - // Bind to errors - this.chain.on('error', err => this.error(err)); - this.pool.on('error', err => this.error(err)); + if (this.http) + this.http.on('error', err => this.error(err)); - if (this.http) - this.http.on('error', err => this.error(err)); + this.pool.on('tx', (tx) => { + if (this.rescanJob) + return; - this.pool.on('tx', (tx) => { - if (this.rescanJob) - return; + this.emit('tx', tx); + }); - this.emit('tx', tx); - }); + this.chain.on('block', (block) => { + this.emit('block', block); + }); - this.chain.on('block', (block) => { - this.emit('block', block); - }); - - this.chain.on('connect', async (entry, block) => { - if (this.rescanJob) { - try { - await this.watchBlock(entry, block); - } catch (e) { - this.error(e); + this.chain.on('connect', async (entry, block) => { + if (this.rescanJob) { + try { + await this.watchBlock(entry, block); + } catch (e) { + this.error(e); + } + return; } - return; - } - this.emit('connect', entry, block); - }); + this.emit('connect', entry, block); + }); - this.chain.on('disconnect', (entry, block) => { - this.emit('disconnect', entry, block); - }); + this.chain.on('disconnect', (entry, block) => { + this.emit('disconnect', entry, block); + }); - this.chain.on('reorganize', (tip, competitor) => { - this.emit('reorganize', tip, competitor); - }); + this.chain.on('reorganize', (tip, competitor) => { + this.emit('reorganize', tip, competitor); + }); - this.chain.on('reset', (tip) => { - this.emit('reset', tip); - }); + this.chain.on('reset', (tip) => { + this.emit('reset', tip); + }); - this.loadPlugins(); -}; - -/** - * Open the node and all its child objects, - * wait for the database to load. - * @returns {Promise} - */ - -SPVNode.prototype.open = async function open() { - assert(!this.opened, 'SPVNode is already open.'); - this.opened = true; - - await this.handlePreopen(); - await this.chain.open(); - await this.pool.open(); - - await this.openPlugins(); - - await this.http.open(); - await this.handleOpen(); - - this.logger.info('Node is loaded.'); -}; - -/** - * Close the node, wait for the database to close. - * @returns {Promise} - */ - -SPVNode.prototype.close = async function close() { - assert(this.opened, 'SPVNode is not open.'); - this.opened = false; - - await this.handlePreclose(); - await this.http.close(); - - await this.closePlugins(); - - await this.pool.close(); - await this.chain.close(); - await this.handleClose(); -}; - -/** - * Scan for any missed transactions. - * Note that this will replay the blockchain sync. - * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - -SPVNode.prototype.scan = async function scan(start, filter, iter) { - const unlock = await this.scanLock.lock(); - const height = this.chain.height; - - try { - await this.chain.replay(start); - - if (this.chain.height < height) { - // We need to somehow defer this. - // await this.connect(); - // this.startSync(); - // await this.watchUntil(height, iter); - } - } finally { - unlock(); + this.loadPlugins(); } -}; -/** - * Watch the blockchain until a certain height. - * @param {Number} height - * @param {Function} iter - * @returns {Promise} - */ + /** + * Open the node and all its child objects, + * wait for the database to load. + * @returns {Promise} + */ -SPVNode.prototype.watchUntil = function watchUntil(height, iter) { - return new Promise((resolve, reject) => { - this.rescanJob = new RescanJob(resolve, reject, height, iter); - }); -}; + async open() { + assert(!this.opened, 'SPVNode is already open.'); + this.opened = true; -/** - * Handled watched block. - * @param {ChainEntry} entry - * @param {MerkleBlock} block - * @returns {Promise} - */ + await this.handlePreopen(); + await this.chain.open(); + await this.pool.open(); -SPVNode.prototype.watchBlock = async function watchBlock(entry, block) { - const unlock = await this.watchLock.lock(); - try { - if (entry.height < this.rescanJob.height) { - await this.rescanJob.iter(entry, block.txs); - return; + await this.openPlugins(); + + await this.http.open(); + await this.handleOpen(); + + this.logger.info('Node is loaded.'); + } + + /** + * Close the node, wait for the database to close. + * @returns {Promise} + */ + + async close() { + assert(this.opened, 'SPVNode is not open.'); + this.opened = false; + + await this.handlePreclose(); + await this.http.close(); + + await this.closePlugins(); + + await this.pool.close(); + await this.chain.close(); + await this.handleClose(); + } + + /** + * Scan for any missed transactions. + * Note that this will replay the blockchain sync. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + async scan(start, filter, iter) { + const unlock = await this.scanLock.lock(); + const height = this.chain.height; + + try { + await this.chain.replay(start); + + if (this.chain.height < height) { + // We need to somehow defer this. + // await this.connect(); + // this.startSync(); + // await this.watchUntil(height, iter); + } + } finally { + unlock(); } - this.rescanJob.resolve(); - this.rescanJob = null; - } catch (e) { - this.rescanJob.reject(e); - this.rescanJob = null; - } finally { - unlock(); } -}; -/** - * Broadcast a transaction (note that this will _not_ be verified - * by the mempool - use with care, lest you get banned from - * bitcoind nodes). - * @param {TX|Block} item - * @returns {Promise} - */ + /** + * Watch the blockchain until a certain height. + * @param {Number} height + * @param {Function} iter + * @returns {Promise} + */ -SPVNode.prototype.broadcast = async function broadcast(item) { - try { - await this.pool.broadcast(item); - } catch (e) { - this.emit('error', e); + watchUntil(height, iter) { + return new Promise((resolve, reject) => { + this.rescanJob = new RescanJob(resolve, reject, height, iter); + }); } -}; -/** - * Broadcast a transaction (note that this will _not_ be verified - * by the mempool - use with care, lest you get banned from - * bitcoind nodes). - * @param {TX} tx - * @returns {Promise} - */ + /** + * Handled watched block. + * @param {ChainEntry} entry + * @param {MerkleBlock} block + * @returns {Promise} + */ -SPVNode.prototype.sendTX = function sendTX(tx) { - return this.broadcast(tx); -}; + async watchBlock(entry, block) { + const unlock = await this.watchLock.lock(); + try { + if (entry.height < this.rescanJob.height) { + await this.rescanJob.iter(entry, block.txs); + return; + } + this.rescanJob.resolve(); + this.rescanJob = null; + } catch (e) { + this.rescanJob.reject(e); + this.rescanJob = null; + } finally { + unlock(); + } + } -/** - * Broadcast a transaction. Silence errors. - * @param {TX} tx - * @returns {Promise} - */ + /** + * Broadcast a transaction (note that this will _not_ be verified + * by the mempool - use with care, lest you get banned from + * bitcoind nodes). + * @param {TX|Block} item + * @returns {Promise} + */ -SPVNode.prototype.relay = function relay(tx) { - return this.broadcast(tx); -}; + async broadcast(item) { + try { + await this.pool.broadcast(item); + } catch (e) { + this.emit('error', e); + } + } -/** - * Connect to the network. - * @returns {Promise} - */ + /** + * Broadcast a transaction (note that this will _not_ be verified + * by the mempool - use with care, lest you get banned from + * bitcoind nodes). + * @param {TX} tx + * @returns {Promise} + */ -SPVNode.prototype.connect = function connect() { - return this.pool.connect(); -}; + sendTX(tx) { + return this.broadcast(tx); + } -/** - * Disconnect from the network. - * @returns {Promise} - */ + /** + * Broadcast a transaction. Silence errors. + * @param {TX} tx + * @returns {Promise} + */ -SPVNode.prototype.disconnect = function disconnect() { - return this.pool.disconnect(); -}; + relay(tx) { + return this.broadcast(tx); + } -/** - * Start the blockchain sync. - */ + /** + * Connect to the network. + * @returns {Promise} + */ -SPVNode.prototype.startSync = function startSync() { - return this.pool.startSync(); -}; + connect() { + return this.pool.connect(); + } -/** - * Stop syncing the blockchain. - */ + /** + * Disconnect from the network. + * @returns {Promise} + */ -SPVNode.prototype.stopSync = function stopSync() { - return this.pool.stopSync(); -}; + disconnect() { + return this.pool.disconnect(); + } + + /** + * Start the blockchain sync. + */ + + startSync() { + return this.pool.startSync(); + } + + /** + * Stop syncing the blockchain. + */ + + stopSync() { + return this.pool.stopSync(); + } +} /* * Helpers */ -function RescanJob(resolve, reject, height, iter) { - this.resolve = resolve; - this.reject = reject; - this.height = height; - this.iter = iter; +class RescanJob { + constructor(resolve, reject, height, iter) { + this.resolve = resolve; + this.reject = reject; + this.height = height; + this.iter = iter; + } } /*