diff --git a/bin/bwallet b/bin/bwallet new file mode 100755 index 00000000..dd044242 --- /dev/null +++ b/bin/bwallet @@ -0,0 +1,55 @@ +#!/bin/bash + +rl=0 +daemon=0 + +if ! type perl > /dev/null 2>& 1; then + if uname | grep -i 'darwin' > /dev/null; then + echo 'Bcoin requires perl to start on OSX.' >& 2 + exit 1 + fi + rl=1 +fi + +if test $rl -eq 1; then + file=$(readlink -f "$0") +else + # Have to do it this way + # because OSX isn't a real OS + file=$(perl -MCwd -e "print Cwd::realpath('$0')") +fi + +dir=$(dirname "$file") + +if test x"$1" = x'cli'; then + shift + exec "${dir}/cli" "wallet" "$@" + exit 1 +fi + +for arg in "$@"; do + case "$arg" in + --daemon) + daemon=1 + ;; + esac +done + +if test $daemon -eq 1; then + # And yet again, OSX doesn't support something. + if ! type setsid > /dev/null 2>& 1; then + ( + "${dir}/wallet" "$@" > /dev/null 2>& 1 & + echo "$!" + ) + exit 0 + fi + ( + setsid "${dir}/wallet" "$@" > /dev/null 2>& 1 & + echo "$!" + ) + exit 0 +else + exec "${dir}/wallet" "$@" + exit 1 +fi diff --git a/bin/cli b/bin/cli index 289c75eb..88196aea 100755 --- a/bin/cli +++ b/bin/cli @@ -17,7 +17,11 @@ const ANTIREPLAY = '' + '220456c656374726f6e696320436173682053797374656d'; function CLI() { - this.config = new Config('bcoin'); + this.config = new Config('bcoin', { + suffix: 'network', + fallback: 'main', + alias: { 'n': 'network' } + }); this.config.load({ argv: true, diff --git a/bin/wallet b/bin/wallet index 13a91a21..1ded72be 100755 --- a/bin/wallet +++ b/bin/wallet @@ -2,24 +2,46 @@ 'use strict'; -process.title = 'bcoin-wallet'; +process.title = 'bwallet'; -const server = require('../lib/wallet/server'); +if (process.argv.indexOf('--help') !== -1 + || process.argv.indexOf('-h') !== -1) { + console.error('See the bcoin wiki at: https://github.com/bcoin-org/bcoin/wiki.'); + process.exit(1); + throw new Error('Could not exit.'); +} -const wdb = server.create({ +if (process.argv.indexOf('--version') !== -1 + || process.argv.indexOf('-v') !== -1) { + const pkg = require('../package.json'); + console.log(pkg.version); + process.exit(0); + throw new Error('Could not exit.'); +} + +const Node = require('../lib/wallet/server'); + +const node = new Node({ config: true, argv: true, env: true, logFile: true, logConsole: true, logLevel: 'debug', - db: 'leveldb' + db: 'leveldb', + workers: true, + listen: true, + loader: require }); process.on('unhandledRejection', (err, promise) => { throw err; }); -wdb.open().catch((err) => { - throw err; +(async () => { + await node.ensure(); + await node.open(); +})().catch((err) => { + console.error(err.stack); + process.exit(1); }); diff --git a/lib/http/server.js b/lib/http/server.js index ae1b20c0..37288d07 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -366,7 +366,7 @@ class HTTPServer extends Server { socket.join('auth'); - this.logger.info('Successful auth from %s.', socket.remoteAddress); + this.logger.info('Successful auth from %s.', socket.host); this.handleAuth(socket); return null; @@ -465,7 +465,7 @@ class HTTPServer extends Server { if (!data) throw new Error('Bad data chunk.'); - this.filter.add(data); + socket.filter.add(data); if (this.node.spv) this.pool.watch(data); @@ -655,7 +655,7 @@ class HTTPServer extends Server { for (const tx of txs) raw.push(tx.toRaw()); - socket.fire('block rescan', block, raw); + return socket.fire('block rescan', block, raw); }); return null; } diff --git a/lib/node/config.js b/lib/node/config.js index 26fcb881..51127aba 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -18,18 +18,21 @@ const HOME = os.homedir ? os.homedir() : '/'; * @alias module:node.Config * @constructor * @param {String} module - Module name (e.g. `bcoin`). + * @param {Object?} options */ -function Config(module) { +function Config(module, options) { if (!(this instanceof Config)) - return new Config(module); + return new Config(module, options); assert(typeof module === 'string'); assert(module.length > 0); this.module = module; - this.network = 'main'; this.prefix = Path.join(HOME, `.${module}`); + this.suffix = null; + this.fallback = null; + this.alias = Object.create(null); this.options = Object.create(null); this.data = Object.create(null); @@ -39,18 +42,39 @@ function Config(module) { this.pass = []; this.query = Object.create(null); this.hash = Object.create(null); + + if (options) + this.init(options); } /** - * Option name aliases. - * @const {Object} + * Initialize options. + * @private + * @param {Object} options */ -Config.alias = { - 'seed': 'seeds', - 'node': 'nodes', - 'n': 'network' -}; +Config.prototype.init = function init(options) { + assert(options && typeof options === 'object'); + + if (options.suffix != null) { + assert(typeof options.suffix === 'string'); + this.suffix = options.suffix; + } + + if (options.fallback != null) { + assert(typeof options.fallback === 'string'); + this.fallback = options.fallback; + } + + if (options.alias) { + assert(typeof options.alias === 'object'); + for (const key of Object.keys(options.alias)) { + const value = options.alias[key]; + assert(typeof value === 'string'); + this.alias[key] = value; + } + } +} /** * Inject options. @@ -92,7 +116,6 @@ Config.prototype.load = function load(options) { if (options.argv) this.parseArg(options.argv); - this.network = this.getNetwork(); this.prefix = this.getPrefix(); }; @@ -119,7 +142,6 @@ Config.prototype.open = function open(file) { this.parseConfig(text); - this.network = this.getNetwork(); this.prefix = this.getPrefix(); }; @@ -650,20 +672,19 @@ Config.prototype.mb = function mb(key, fallback) { }; /** - * Grab network type from config data. - * @private + * Grab suffix from config data. * @returns {String} */ -Config.prototype.getNetwork = function getNetwork() { - let network = this.str('network'); +Config.prototype.getSuffix = function getSuffix() { + if (!this.suffix) + throw new Error('No suffix presented.'); - if (!network) - network = 'main'; + const suffix = this.str(this.suffix, this.fallback); - assert(isAlpha(network), 'Bad network.'); + assert(isAlpha(suffix), 'Bad suffix.'); - return network; + return suffix; }; /** @@ -678,17 +699,18 @@ Config.prototype.getPrefix = function getPrefix() { if (prefix) { if (prefix[0] === '~') prefix = Path.join(HOME, prefix.substring(1)); - return prefix; + } else { + prefix = Path.join(HOME, `.${this.module}`); } - prefix = Path.join(HOME, `.${this.module}`); + if (this.suffix) { + const suffix = this.str(this.suffix); - const network = this.str('network'); - - if (network) { - assert(isAlpha(network), 'Bad network.'); - if (network !== 'main') - prefix = Path.join(prefix, network); + if (suffix) { + assert(isAlpha(suffix), 'Bad suffix.'); + if (this.fallback && suffix !== this.fallback) + prefix = Path.join(prefix, suffix); + } } return Path.normalize(prefix); @@ -748,8 +770,6 @@ Config.prototype.parseConfig = function parseConfig(text) { text = text.replace(/\r/g, '\n'); text = text.replace(/\\\n/g, ''); - let colons = true; - let seen = false; let num = 0; for (const chunk of text.split('\n')) { @@ -763,29 +783,10 @@ Config.prototype.parseConfig = function parseConfig(text) { if (line[0] === '#') continue; - const equal = line.indexOf('='); - const colon = line.indexOf(':'); + const index = line.indexOf(':'); - let index = -1; - - if (colon !== -1 && (colon < equal || equal === -1)) { - if (seen && !colons) - throw new Error(`Expected '=' on line ${num}: "${line}".`); - - index = colon; - seen = true; - colons = true; - } else if (equal !== -1) { - if (seen && colons) - throw new Error(`Expected ':' on line ${num}: "${line}".`); - - index = equal; - seen = true; - colons = false; - } else { - const symbol = colons ? ':' : '='; - throw new Error(`Expected '${symbol}' on line ${num}: "${line}".`); - } + if (index === -1) + throw new Error(`Expected ':' on line ${num}: "${line}".`); let key = line.substring(0, index).trim(); @@ -799,7 +800,7 @@ Config.prototype.parseConfig = function parseConfig(text) { if (value.length === 0) continue; - const alias = Config.alias[key]; + const alias = this.alias[key]; if (alias) key = alias; @@ -874,7 +875,7 @@ Config.prototype.parseArg = function parseArg(argv) { // Do not allow one-letter aliases. if (key.length > 1) { - const alias = Config.alias[key]; + const alias = this.alias[key]; if (alias) key = alias; } @@ -901,7 +902,7 @@ Config.prototype.parseArg = function parseArg(argv) { throw new Error(`Invalid argument: -${key}.`); } - const alias = Config.alias[key]; + const alias = this.alias[key]; if (alias) key = alias; @@ -971,7 +972,7 @@ Config.prototype.parseEnv = function parseEnv(env) { // Do not allow one-letter aliases. if (key.length > 1) { - const alias = Config.alias[key]; + const alias = this.alias[key]; if (alias) key = alias; } @@ -1063,7 +1064,7 @@ Config.prototype.parseForm = function parseForm(query, map) { if (value.length === 0) continue; - const alias = Config.alias[key]; + const alias = this.alias[key]; if (alias) key = alias; @@ -1098,7 +1099,7 @@ function unescape(str) { } function isAlpha(str) { - return /^[a-z0-9]+$/.test(str); + return /^[a-z0-9_\-]+$/i.test(str); } function isKey(key) { diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 162983d7..ce624b57 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -41,7 +41,7 @@ function FullNode(options) { if (!(this instanceof FullNode)) return new FullNode(options); - Node.call(this, options); + Node.call(this, 'bcoin', 'bcoin.conf', 'debug.log', options); // SPV flag. this.spv = false; diff --git a/lib/node/node.js b/lib/node/node.js index dc3d7a79..d5360710 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -26,20 +26,25 @@ const Config = require('./config'); * @param {Object} options */ -function Node(options) { +function Node(module, config, file, options) { if (!(this instanceof Node)) - return new Node(options); + return new Node(module, config, file, options); AsyncObject.call(this); - this.config = new Config('bcoin'); + this.config = new Config(module, { + suffix: 'network', + fallback: 'main', + alias: { 'n': 'network' } + }); + this.config.inject(options); this.config.load(options); if (options.config) - this.config.open('bcoin.conf'); + this.config.open(config); - this.network = Network.get(this.config.network); + this.network = Network.get(this.config.getSuffix()); this.startTime = -1; this.bound = []; this.plugins = Object.create(null); @@ -56,7 +61,7 @@ function Node(options) { this.miner = null; this.http = null; - this.init(); + this.init(file); } Object.setPrototypeOf(Node.prototype, AsyncObject.prototype); @@ -67,16 +72,17 @@ Object.setPrototypeOf(Node.prototype, AsyncObject.prototype); * @param {Object} options */ -Node.prototype.initOptions = function initOptions() { - let logger = new Logger(); +Node.prototype.initOptions = function initOptions(file) { const config = this.config; + let logger = new Logger(); + if (config.has('logger')) logger = config.obj('logger'); logger.set({ filename: config.bool('log-file') - ? config.location('debug.log') + ? config.location(file) : null, level: config.str('log-level'), console: config.bool('log-console'), @@ -99,8 +105,8 @@ Node.prototype.initOptions = function initOptions() { * @param {Object} options */ -Node.prototype.init = function init() { - this.initOptions(); +Node.prototype.init = function init(file) { + this.initOptions(file); this.on('error', () => {}); diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index e266af73..9e91677e 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -38,7 +38,7 @@ function SPVNode(options) { if (!(this instanceof SPVNode)) return new SPVNode(options); - Node.call(this, options); + Node.call(this, 'bcoin', 'bcoin.conf', 'debug.log', options); // SPV flag. this.spv = true; @@ -80,21 +80,19 @@ function SPVNode(options) { this.rpc = new RPC(this); - if (!HTTPServer.unsupported) { - this.http = new HTTPServer({ - 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 HTTPServer({ + 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(); diff --git a/lib/protocol/network.js b/lib/protocol/network.js index 63aa546a..89c15fd0 100644 --- a/lib/protocol/network.js +++ b/lib/protocol/network.js @@ -48,6 +48,7 @@ function Network(options) { this.addressPrefix = options.addressPrefix; this.requireStandard = options.requireStandard; this.rpcPort = options.rpcPort; + this.walletPort = options.walletPort; this.minRelay = options.minRelay; this.feeRate = options.feeRate; this.maxFeeRate = options.maxFeeRate; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index 8031338a..c6e4c75a 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -436,6 +436,14 @@ main.requireStandard = true; main.rpcPort = 8332; +/** + * Default wallet port. + * @const {Number} + * @default + */ + +main.walletPort = 8334; + /** * Default min relay rate. * @const {Rate} @@ -646,6 +654,8 @@ testnet.requireStandard = false; testnet.rpcPort = 18332; +testnet.walletPort = 18334; + testnet.minRelay = 1000; testnet.feeRate = 20000; @@ -807,6 +817,8 @@ regtest.requireStandard = false; regtest.rpcPort = 48332; +regtest.walletPort = 48334; + regtest.minRelay = 1000; regtest.feeRate = 20000; @@ -970,6 +982,8 @@ simnet.requireStandard = false; simnet.rpcPort = 18556; +simnet.walletPort = 18558; + simnet.minRelay = 1000; simnet.feeRate = 20000; diff --git a/lib/wallet/client.js b/lib/wallet/client.js index 83fdaf13..c04d8707 100644 --- a/lib/wallet/client.js +++ b/lib/wallet/client.js @@ -10,7 +10,7 @@ const assert = require('assert'); const {NodeClient} = require('bclient'); const TX = require('../primitives/tx'); -const digest = require('bcrypto/lib/digest'); +const hash256 = require('bcrypto/lib/hash256'); const util = require('../utils/util'); class WalletClient extends NodeClient { @@ -77,7 +77,7 @@ function parseEntry(data) { assert(Buffer.isBuffer(data)); assert(data.length >= 84); - const h = digest.hash256(data.slice(0, 80)); + const h = hash256.digest(data.slice(0, 80)); return { hash: h.toString('hex'), @@ -90,10 +90,8 @@ function parseBlock(entry, txs) { const block = parseEntry(entry); const out = []; - for (const raw of txs) { - const tx = TX.fromRaw(raw); - out.push(tx); - } + for (const tx of txs) + out.push(TX.fromRaw(tx)); return [block, out]; } diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 43f8c5d3..a71367fb 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -41,8 +41,9 @@ class HTTPServer extends Server { this.network = this.options.network; this.logger = this.options.logger.context('http'); - this.wdb = this.options.walletdb; - this.rpc = this.wdb.rpc; + this.wdb = this.options.node.wdb; + this.rpc = this.options.node.rpc; + this.plugin = this.options.node.plugin; this.init(); } @@ -90,7 +91,11 @@ class HTTPServer extends Server { })); this.use(this.jsonRPC()); - this.use(this.router()); + + if (!this.plugin) + this.use('/wallet', this.router()); + else + this.use(this.router()); this.error((err, req, res) => { const code = err.statusCode || 500; @@ -118,6 +123,11 @@ class HTTPServer extends Server { const id = valid.str('id'); const token = valid.buf('token'); + if (!id) { + res.json(403); + return; + } + if (!this.options.walletAuth) { const wallet = await this.wdb.get(id); @@ -893,7 +903,7 @@ class HTTPServer extends Server { socket.join('wallet auth'); - this.logger.info('Successful auth from %s.', socket.remoteAddress); + this.logger.info('Successful auth from %s.', socket.host); this.handleAuth(socket); @@ -967,7 +977,7 @@ class HTTPOptions { constructor(options) { this.network = Network.primary; this.logger = null; - this.walletdb = null; + this.node = null; this.apiKey = base58.encode(random.randomBytes(20)); this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); this.serviceHash = this.apiHash; @@ -993,13 +1003,13 @@ class HTTPOptions { fromOptions(options) { assert(options); - assert(options.walletdb && typeof options.walletdb === 'object', + assert(options.node && typeof options.node === 'object', 'HTTP Server requires a WalletDB.'); - this.walletdb = options.walletdb; - this.network = options.walletdb.network; - this.logger = options.walletdb.logger; - this.port = this.network.rpcPort + 2; + this.node = options.node; + this.network = options.node.network; + this.logger = options.node.logger; + this.port = this.network.walletPort; if (options.logger != null) { assert(typeof options.logger === 'object'); diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index acfbc569..db37fd70 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -73,6 +73,7 @@ NodeClient.prototype._init = function _init() { NodeClient.prototype._open = function _open(options) { this.listen = true; + this.emit('open'); return Promise.resolve(); }; @@ -83,6 +84,7 @@ NodeClient.prototype._open = function _open(options) { NodeClient.prototype._close = function _close() { this.listen = false; + this.emit('close'); return Promise.resolve(); }; diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 9c96ec96..ad28c7a6 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -6,8 +6,11 @@ 'use strict'; +const EventEmitter = require('events'); const WalletDB = require('./walletdb'); const NodeClient = require('./nodeclient'); +const HTTPServer = require('./http'); +const RPC = require('./rpc'); /** * @exports wallet/plugin @@ -15,6 +18,74 @@ const NodeClient = require('./nodeclient'); const plugin = exports; +/** + * Plugin + * @constructor + * @param {Node} node + */ + +function Plugin(node) { + if (!(this instanceof Plugin)) + return new Plugin(node); + + const config = node.config; + + this.network = node.network; + this.logger = node.logger; + + this.client = new NodeClient(node); + this.plugin = true; + + this.wdb = new WalletDB({ + network: node.network, + logger: node.logger, + workers: node.workers, + client: this.client, + prefix: config.prefix, + db: config.str(['wallet-db', 'db']), + maxFiles: config.uint('wallet-max-files'), + cacheSize: config.mb('wallet-cache-size'), + witness: config.bool('wallet-witness'), + checkpoints: config.bool('wallet-checkpoints'), + startHeight: config.uint('wallet-start-height'), + wipeNoReally: config.bool('wallet-wipe-no-really'), + spv: node.spv + }); + + this.rpc = new RPC(this); + + this.http = new HTTPServer({ + network: node.network, + logger: node.logger, + node: this, + apiKey: config.str(['wallet-api-key', 'api-key']), + walletAuth: config.bool('wallet-auth'), + noAuth: config.bool(['wallet-no-auth', 'no-auth']) + }); + + this.http.attach('/wallet', node.http); + + this.init(); +} + +Object.setPrototypeOf(Plugin.prototype, EventEmitter.prototype); + +Plugin.prototype.init = function init() { + this.client.on('error', err => this.emit('error', err)); + this.wdb.on('error', err => this.emit('error', err)); + this.http.on('error', err => this.emit('error', err)); +}; + +Plugin.prototype.open = async function open() { + await this.wdb.open(); + this.rpc.wallet = this.wdb.primary; +}; + +Plugin.prototype.close = async function close() { + this.rpc.wallet = this.wdb.primary; + await this.wdb.open(); +}; + /** * Plugin name. * @const {String} @@ -29,35 +100,5 @@ plugin.id = 'walletdb'; */ plugin.init = function init(node) { - const config = node.config; - const client = new NodeClient(node); - - const wdb = new WalletDB({ - network: node.network, - logger: node.logger, - workers: node.workers, - client: client, - prefix: config.prefix, - db: config.str(['wallet-db', 'db']), - maxFiles: config.uint('wallet-max-files'), - cacheSize: config.mb('wallet-cache-size'), - witness: config.bool('wallet-witness'), - checkpoints: config.bool('wallet-checkpoints'), - startHeight: config.uint('wallet-start-height'), - wipeNoReally: config.bool('wallet-wipe-no-really'), - apiKey: config.str(['wallet-api-key', 'api-key']), - walletAuth: config.bool('wallet-auth'), - noAuth: config.bool(['wallet-no-auth', 'no-auth']), - ssl: config.str('wallet-ssl'), - host: config.str('wallet-host'), - port: config.uint('wallet-port'), - spv: node.spv, - verify: node.spv, - listen: true, - plugin: true - }); - - wdb.http.attach('/wallet', node.http); - - return wdb; + return new Plugin(node); }; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 8249d7ea..f95f20d7 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -81,15 +81,15 @@ class RPC extends RPCBase { * @param {WalletDB} wdb */ - constructor(wdb) { + constructor(node) { super(); - assert(wdb, 'RPC requires a WalletDB.'); + assert(node, 'RPC requires a WalletDB.'); - this.wdb = wdb; - this.network = wdb.network; - this.logger = wdb.logger.context('rpc'); - this.client = wdb.client; + this.wdb = node.wdb; + this.network = node.network; + this.logger = node.logger.context('rpc'); + this.client = node.client; this.locker = new Lock(); this.wallet = null; diff --git a/lib/wallet/server.js b/lib/wallet/server.js index e1610db1..4d44ef91 100644 --- a/lib/wallet/server.js +++ b/lib/wallet/server.js @@ -6,104 +6,128 @@ 'use strict'; +const Node = require('../node/node'); const WalletDB = require('./walletdb'); -const WorkerPool = require('../workers/workerpool'); -const Config = require('../node/config'); -const Logger = require('../node/logger'); +const HTTPServer = require('./http'); const Client = require('./client'); +const RPC = require('./rpc'); /** - * @exports wallet/server + * Wallet Node + * @extends Node + * @constructor */ -const server = exports; +function WalletNode(options) { + if (!(this instanceof WalletNode)) + return new WalletNode(options); + + Node.call(this, 'bcoin', 'wallet.conf', 'wallet.log', options); + + this.client = new Client({ + network: this.network, + url: this.config.str('node-url'), + host: this.config.str('node-host'), + port: this.config.str('node-port', this.network.rpcPort), + ssl: this.config.str('node-ssl'), + apiKey: this.config.str('node-api-key') + }); + + this.wdb = new WalletDB({ + network: this.network, + logger: this.logger, + workers: this.workers, + client: this.client, + prefix: this.config.prefix, + db: this.config.str('db'), + maxFiles: this.config.uint('max-files'), + cacheSize: this.config.mb('cache-size'), + witness: this.config.bool('witness'), + checkpoints: this.config.bool('checkpoints'), + startHeight: this.config.uint('start-height'), + wipeNoReally: this.config.bool('wipe-no-really'), + apiKey: this.config.str('api-key'), + walletAuth: this.config.bool('auth'), + noAuth: this.config.bool('no-auth'), + ssl: this.config.str('ssl'), + host: this.config.str('host'), + port: this.config.uint('port'), + spv: this.config.bool('spv') + }); + + this.rpc = new RPC(this); + + this.http = new HTTPServer({ + 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'), + walletAuth: this.config.bool('wallet-auth') + }); + + this._init(); +} + +Object.setPrototypeOf(WalletNode.prototype, Node.prototype); /** - * Create a wallet server. - * @param {Object} options - * @returns {WalletDB} + * Initialize the node. + * @private */ -server.create = function create(options) { - const config = new Config('bcoin'); - let logger = new Logger('debug'); +WalletNode.prototype._init = function _init() { + this.client.on('error', err => this.error(err)); + this.wdb.on('error', err => this.error(err)); + this.http.on('error', err => this.error(err)); - config.inject(options); - config.load(options); - - if (options.config) - config.open('wallet.conf'); - - if (config.has('logger')) - logger = config.obj('logger'); - - const client = new Client({ - network: config.network, - uri: config.str('node-uri'), - apiKey: config.str('node-api-key') - }); - - logger.set({ - filename: config.bool('log-file') - ? config.location('wallet.log') - : null, - level: config.str('log-level'), - console: config.bool('log-console'), - shrink: config.bool('log-shrink') - }); - - const workers = new WorkerPool({ - enabled: config.str('workers-enabled'), - size: config.uint('workers-size'), - timeout: config.uint('workers-timeout') - }); - - const wdb = new WalletDB({ - network: config.network, - logger: logger, - workers: workers, - client: client, - prefix: config.prefix, - db: config.str('db'), - maxFiles: config.uint('max-files'), - cacheSize: config.mb('cache-size'), - witness: config.bool('witness'), - checkpoints: config.bool('checkpoints'), - startHeight: config.uint('start-height'), - wipeNoReally: config.bool('wipe-no-really'), - apiKey: config.str('api-key'), - walletAuth: config.bool('auth'), - noAuth: config.bool('no-auth'), - ssl: config.str('ssl'), - host: config.str('host'), - port: config.uint('port'), - spv: config.bool('spv'), - verify: config.bool('spv'), - listen: true - }); - - wdb.on('error', () => {}); - - workers.on('spawn', (child) => { - logger.info('Spawning worker process: %d.', child.id); - }); - - workers.on('exit', (code, child) => { - logger.warning('Worker %d exited: %s.', child.id, code); - }); - - workers.on('log', (text, child) => { - logger.debug('Worker %d says:', child.id); - logger.debug(text); - }); - - workers.on('error', (err, child) => { - if (child) { - logger.error('Worker %d error: %s', child.id, err.message); - return; - } - wdb.emit('error', err); - }); - - return wdb; + this.loadPlugins(); }; + +/** + * Open the node and all its child objects, + * wait for the database to load. + * @alias WalletNode#open + * @returns {Promise} + */ + +WalletNode.prototype._open = async function _open(callback) { + // await this.client.open(); + await this.wdb.open(); + + this.rpc.wallet = this.wdb.primary; + + await this.openPlugins(); + + await this.http.open(); + + this.logger.info('Node is loaded.'); +}; + +/** + * Close the node, wait for the database to close. + * @alias WalletNode#close + * @returns {Promise} + */ + +WalletNode.prototype._close = async function _close() { + await this.http.close(); + + await this.closePlugins(); + + this.rpc.wallet = null; + await this.wdb.close(); + // await this.client.close(); +}; + +/* + * Expose + */ + +module.exports = WalletNode; diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 5b67dbfb..197dffc1 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -27,8 +27,6 @@ const Logger = require('../node/logger'); const Outpoint = require('../primitives/outpoint'); const layouts = require('./layout'); const records = require('./records'); -const HTTPServer = require('./http'); -const RPC = require('./rpc'); const StaticWriter = require('../utils/staticwriter'); const layout = layouts.walletdb; const ChainState = records.ChainState; @@ -55,27 +53,11 @@ function WalletDB(options) { this.network = this.options.network; this.logger = this.options.logger.context('wallet'); this.workers = this.options.workers; - this.client = this.options.client; this.feeRate = this.options.feeRate; - this.db = LDB(this.options); - this.rpc = new RPC(this); + this.primary = null; - - this.http = new HTTPServer({ - walletdb: this, - network: this.network, - logger: this.logger, - prefix: this.options.prefix, - apiKey: this.options.apiKey, - walletAuth: this.options.walletAuth, - noAuth: this.options.noAuth, - host: this.options.host, - port: this.options.port, - ssl: this.options.ssl - }); - this.state = new ChainState(); this.height = 0; this.wallets = new Map(); @@ -86,6 +68,7 @@ function WalletDB(options) { this.readLock = new MappedLock(); this.writeLock = new Lock(); this.txLock = new Lock(); + this.scanLock = new Lock(); this.filter = new Bloom(); @@ -130,9 +113,6 @@ WalletDB.prototype._init = function _init() { */ WalletDB.prototype._open = async function _open() { - if (!this.options.plugin) - await this.logger.open(); - await this.db.open(); await this.db.checkVersion('V', 7); @@ -158,10 +138,6 @@ WalletDB.prototype._open = async function _open() { wallet.id, wallet.wid, await wallet.receiveAddress()); this.primary = wallet; - this.rpc.wallet = wallet; - - if (this.options.listen) - await this.http.open(); }; /** @@ -173,18 +149,12 @@ WalletDB.prototype._open = async function _open() { WalletDB.prototype._close = async function _close() { await this.disconnect(); - if (this.options.listen) - await this.http.close(); - for (const wallet of this.wallets.values()) { await wallet.destroy(); this.unregister(wallet); } await this.db.close(); - - if (!this.options.plugin) - await this.logger.close(); }; /** @@ -195,9 +165,23 @@ WalletDB.prototype._close = async function _close() { WalletDB.prototype.load = async function load() { const unlock = await this.txLock.lock(); try { + await this.watch(); await this.connect(); await this.init(); - await this.watch(); + } finally { + unlock(); + } +}; + +/** + * Initialize state with server on every connect. + * @returns {Promise} + */ + +WalletDB.prototype.resync = async function resync() { + const unlock = await this.txLock.lock(); + try { + await this.setFilter(); await this.sync(); await this.resend(); } finally { @@ -223,6 +207,14 @@ WalletDB.prototype.bind = function bind() { this.emit('error', err); }); + this.client.on('open', async () => { + try { + await this.resync(); + } catch (e) { + this.emit('error', e); + } + }); + this.client.on('block connect', async (entry, txs) => { try { await this.addBlock(entry, txs); @@ -239,7 +231,7 @@ WalletDB.prototype.bind = function bind() { } }); - this.client.hook('block rescan', async (entry, txs) => { + this.client.on('block rescan', async (entry, txs) => { try { await this.rescanBlock(entry, txs); } catch (e) { @@ -276,7 +268,6 @@ WalletDB.prototype.connect = async function connect() { this.bind(); await this.client.open(); - await this.setFilter(); }; /** @@ -381,8 +372,6 @@ WalletDB.prototype.watch = async function watch() { }); this.logger.info('Added %d outpoints to WalletDB filter.', outpoints); - - await this.setFilter(); }; /** @@ -1787,6 +1776,8 @@ WalletDB.prototype._addBlock = async function _addBlock(entry, txs) { return 0; } + this.logger.debug('Adding block: %d.', entry.height); + if (tip.height === this.state.height) { // We let blocks of the same height // through specifically for rescans: @@ -1796,7 +1787,9 @@ WalletDB.prototype._addBlock = async function _addBlock(entry, txs) { // processed (in the case of a crash). this.logger.warning('Already saw WalletDB block (%d).', tip.height); } else if (tip.height !== this.state.height + 1) { - throw new Error('WDB: Bad connection (height mismatch).'); + // throw new Error('WDB: Bad connection (height mismatch).'); + await this.scan(this.state.height); + return; } // Sync the state to the new tip. @@ -1898,11 +1891,33 @@ WalletDB.prototype._removeBlock = async function _removeBlock(entry) { */ WalletDB.prototype.rescanBlock = async function rescanBlock(entry, txs) { + const unlock = await this.scanLock.lock(); + try { + return await this._rescanBlock(entry, txs); + } finally { + unlock(); + } +}; + +/** + * Rescan a block. + * @private + * @param {ChainEntry} entry + * @param {TX[]} txs + * @returns {Promise} + */ + +WalletDB.prototype._rescanBlock = async function _rescanBlock(entry, txs) { if (!this.rescanning) { this.logger.warning('Unsolicited rescan block: %s.', entry.height); return; } + if (entry.height > this.state.height + 1) { + this.logger.warning('Unsolicited rescan block: %s.', entry.height); + return; + } + try { await this._addBlock(entry, txs); } catch (e) { @@ -2034,14 +2049,6 @@ function WalletOptions(options) { this.checkpoints = false; this.startHeight = 0; this.wipeNoReally = false; - this.apiKey = null; - this.walletAuth = false; - this.noAuth = false; - this.ssl = false; - this.host = '127.0.0.1'; - this.port = this.network.rpcPort + 2; - this.plugin = false; - this.listen = false; if (options) this.fromOptions(options); @@ -2137,46 +2144,6 @@ WalletOptions.prototype.fromOptions = function fromOptions(options) { this.wipeNoReally = options.wipeNoReally; } - if (options.apiKey != null) { - assert(typeof options.apiKey === 'string'); - this.apiKey = options.apiKey; - } - - if (options.walletAuth != null) { - assert(typeof options.walletAuth === 'boolean'); - this.walletAuth = options.walletAuth; - } - - if (options.noAuth != null) { - assert(typeof options.noAuth === 'boolean'); - this.noAuth = options.noAuth; - } - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.host != null) { - assert(typeof options.host === 'string'); - this.host = options.host; - } - - if (options.port != null) { - assert(typeof options.port === 'number'); - this.port = options.port; - } - - if (options.plugin != null) { - assert(typeof options.plugin === 'boolean'); - this.plugin = options.plugin; - } - - if (options.listen != null) { - assert(typeof options.listen === 'boolean'); - this.listen = options.listen; - } - return this; }; diff --git a/test/http-test.js b/test/http-test.js index 02c9004e..6c6532b1 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -37,7 +37,7 @@ const wallet = new WalletClient({ apiKey: 'foo' }); -const wdb = node.require('walletdb'); +const {wdb} = node.require('walletdb'); let addr = null; let hash = null; diff --git a/test/node-test.js b/test/node-test.js index 923c0ae2..1bd77657 100644 --- a/test/node-test.js +++ b/test/node-test.js @@ -24,7 +24,7 @@ const node = new FullNode({ const chain = node.chain; const miner = node.miner; -const wdb = node.require('walletdb'); +const {wdb} = node.require('walletdb'); let wallet = null; let tip1 = null;