diff --git a/bin/cli b/bin/cli index ef04e66f..cde162a7 100755 --- a/bin/cli +++ b/bin/cli @@ -534,7 +534,7 @@ CLI.prototype.rpc = async function rpc() { CLI.prototype.handleWallet = async function handleWallet() { this.wallet = new Wallet({ - uri: this.config.str(['url', 'uri']), + url: this.config.str(['url', 'uri']), apiKey: this.config.str('api-key'), network: this.config.str('network'), id: this.config.str('id', 'primary'), @@ -693,7 +693,7 @@ CLI.prototype.handleWallet = async function handleWallet() { CLI.prototype.handleNode = async function handleNode() { this.client = new Client({ - uri: this.config.str(['url', 'uri']), + url: this.config.str(['url', 'uri']), apiKey: this.config.str('api-key'), network: this.config.str('network') }); @@ -776,14 +776,12 @@ CLI.prototype.open = async function open() { } }; -CLI.prototype.destroy = function destroy() { - if (this.wallet) - this.wallet.client.destroy(); +CLI.prototype.destroy = async function destroy() { + if (this.wallet && this.wallet.opened) + await this.wallet.close(); - if (this.client) - this.client.destroy(); - - return Promise.resolve(); + if (this.client && this.client.opened) + await this.client.close(); }; (async () => { diff --git a/lib/http/base-browser.js b/lib/http/base-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/http/base-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/http/base.js b/lib/http/base.js deleted file mode 100644 index 5fe9b45c..00000000 --- a/lib/http/base.js +++ /dev/null @@ -1,1751 +0,0 @@ -/*! - * http.js - http server for bcoin - * Copyright (c) 2014-2015, Fedor Indutny (MIT License) - * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const assert = require('assert'); -const fs = require('fs'); -const URL = require('url'); -const path = require('path'); -const crypto = require('crypto'); -const EventEmitter = require('events'); -const {StringDecoder} = require('string_decoder'); - -/** - * HTTPBase - * @alias module:http.Base - * @constructor - * @param {Object?} options - * @emits HTTPBase#socket - */ - -function HTTPBase(options) { - if (!(this instanceof HTTPBase)) - return new HTTPBase(options); - - EventEmitter.call(this); - - this.config = new HTTPBaseOptions(options); - this.config.load(); - - this.server = null; - this.io = null; - this.sockets = new Set(); - this.channels = new Map(); - this.routes = new Routes(); - this.mounts = []; - this.stack = []; - this.hooks = []; - - this._init(); -} - -Object.setPrototypeOf(HTTPBase.prototype, EventEmitter.prototype); - -/** - * Initialize server. - * @private - */ - -HTTPBase.prototype._init = function _init() { - const backend = this.config.getBackend(); - const options = this.config.toHTTP(); - - this.server = backend.createServer(options); - - this._initRouter(); - this._initSockets(); - - this.server.on('connection', (socket) => { - socket.on('error', (err) => { - if (err.message === 'Parse Error') { - let msg = 'http_parser.execute failure'; - msg += ` (parsed=${err.bytesParsed || -1}`; - msg += ` code=${err.code})`; - err = new Error(msg); - } - - this.emit('error', err); - - try { - socket.destroy(); - } catch (e) { - ; - } - }); - }); - - this.server.on('error', (err) => { - this.emit('error', err); - }); -}; - -/** - * Initialize router. - * @private - */ - -HTTPBase.prototype._initRouter = function _initRouter() { - this.server.on('request', async (hreq, hres) => { - let req = null; - let res = null; - - try { - req = new Request(hreq, hres, hreq.url); - res = new Response(hreq, hres); - - req.on('error', () => {}); - } catch (e) { - this.emit('error', e); - return; - } - - try { - req.pause(); - await this.handleRequest(req, res); - } catch (e) { - res.error(e.statusCode || 500, e); - this.emit('error', e); - } - }); -}; - -/** - * Handle a request. - * @private - * @param {ServerRequest} req - * @param {ServerResponse} res - * @returns {Promise} - */ - -HTTPBase.prototype.handleRequest = async function handleRequest(req, res) { - if (await this.handleMounts(req, res)) - return; - - this.emit('request', req, res); - - if (await this.handleStack(req, res)) - return; - - const routes = this.routes.getHandlers(req.method); - - if (!routes) - throw new Error(`No routes found for method: ${req.method}.`); - - for (const route of routes) { - const params = route.match(req.pathname); - - if (!params) - continue; - - req.params = params; - - if (await this.handleHooks(req, res)) - return; - - if (await route.call(req, res)) - return; - } - - throw new Error(`No routes found for path: ${req.pathname}.`); -}; - -/** - * CORS middleware. - * @returns {Function} - */ - -HTTPBase.prototype.cors = function cors() { - return async (req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.setHeader( - 'Access-Control-Allow-Methods', - 'GET,HEAD,PUT,PATCH,POST,DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'Authorization'); - - if (req.method === 'OPTIONS') { - res.setStatus(200); - res.end(); - return; - } - }; -}; - -/** - * Basic auth middleware. - * @param {Object} options - * @returns {Function} - */ - -HTTPBase.prototype.basicAuth = function basicAuth(options) { - assert(options, 'Basic auth requires options.'); - - let user = options.username; - let pass = options.password; - let realm = options.realm; - - if (user != null) { - assert(typeof user === 'string'); - assert(user.length <= 255, 'Username too long.'); - assert(isAscii(user), 'Username must be ASCII.'); - user = sha256(user, 'ascii'); - } - - assert(typeof pass === 'string'); - assert(pass.length <= 255, 'Password too long.'); - assert(isAscii(pass), 'Password must be ASCII.'); - pass = sha256(pass, 'ascii'); - - if (!realm) - realm = 'server'; - - assert(typeof realm === 'string'); - - const fail = (res) => { - res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); - res.setStatus(401); - res.end(); - }; - - return async (req, res) => { - const hdr = req.headers['authorization']; - - if (!hdr) { - fail(res); - return; - } - - if (hdr.length > 674) { - fail(res); - return; - } - - const parts = hdr.split(' '); - - if (parts.length !== 2) { - fail(res); - return; - } - - const [type, b64] = parts; - - if (type !== 'Basic') { - fail(res); - return; - } - - const auth = Buffer.from(b64, 'base64').toString('ascii'); - const items = auth.split(':'); - - const username = items.shift(); - const password = items.join(':'); - - if (user) { - if (username.length > 255) { - fail(res); - return; - } - - const hash = sha256(username, 'ascii'); - - if (!ccmp(hash, user)) { - fail(res); - return; - } - } - - if (password.length > 255) { - fail(res); - return; - } - - const hash = sha256(password, 'ascii'); - - if (!ccmp(hash, pass)) { - fail(res); - return; - } - - req.username = username; - }; -}; - -/** - * Body parser middleware. - * @param {Object} options - * @returns {Function} - */ - -HTTPBase.prototype.bodyParser = function bodyParser(options) { - const opt = new BodyParserOptions(options); - - return async (req, res) => { - if (req.hasBody) - return; - - try { - req.resume(); - req.body = await this.parseBody(req, opt); - } finally { - req.pause(); - } - - req.hasBody = true; - }; -}; - -/** - * Parse request body. - * @private - * @param {ServerRequest} req - * @param {Object} options - * @returns {Promise} - */ - -HTTPBase.prototype.parseBody = async function parseBody(req, options) { - let body = Object.create(null); - - if (req.method === 'GET') - return body; - - let type = req.contentType; - - if (options.contentType) - type = options.contentType; - - if (type === 'bin') - return body; - - const data = await this.readBody(req, 'utf8', options); - - if (!data) - return body; - - switch (type) { - case 'json': - body = JSON.parse(data); - if (!body || typeof body !== 'object' || Array.isArray(body)) - throw new Error('JSON body must be an object.'); - break; - case 'form': - body = parsePairs(data, options.keyLimit); - break; - } - - return body; -}; - -/** - * Read and buffer request body. - * @param {ServerRequest} req - * @param {String} enc - * @param {Object} options - * @returns {Promise} - */ - -HTTPBase.prototype.readBody = function readBody(req, enc, options) { - return new Promise((resolve, reject) => { - return this._readBody(req, enc, options, resolve, reject); - }); -}; - -/** - * Read and buffer request body. - * @private - * @param {ServerRequest} req - * @param {String} enc - * @param {Object} options - * @param {Function} resolve - * @param {Function} reject - */ - -HTTPBase.prototype._readBody = function _readBody(req, enc, options, resolve, reject) { - const decode = new StringDecoder(enc); - - let hasData = false; - let total = 0; - let body = ''; - - const cleanup = () => { - /* eslint-disable */ - req.removeListener('data', onData); - req.removeListener('error', onError); - req.removeListener('end', onEnd); - - if (timer != null) { - timer = null; - clearTimeout(timer); - } - /* eslint-enable */ - }; - - const onData = (data) => { - total += data.length; - hasData = true; - - if (total > options.bodyLimit) { - reject(new Error('Request body overflow.')); - return; - } - - body += decode.write(data); - }; - - const onError = (err) => { - cleanup(); - reject(err); - }; - - const onEnd = () => { - cleanup(); - - if (hasData) { - resolve(body); - return; - } - - resolve(null); - }; - - let timer = setTimeout(() => { - timer = null; - cleanup(); - reject(new Error('Request body timed out.')); - }, options.timeout); - - req.on('data', onData); - req.on('error', onError); - req.on('end', onEnd); -}; - -/** - * JSON rpc middleware. - * @param {RPCBase} rpc - * @returns {Function} - */ - -HTTPBase.prototype.jsonRPC = function jsonRPC(rpc) { - return async (req, res) => { - if (req.method !== 'POST') - return; - - if (req.pathname !== '/') - return; - - if (typeof req.body.method !== 'string') - return; - - let json = await rpc.call(req.body, req.query); - - if (json == null) - json = null; - - json = JSON.stringify(json); - json += '\n'; - - res.setHeader('X-Long-Polling', '/?longpoll=1'); - - res.send(200, json, 'json'); - }; -}; - -/** - * Handle mount stack. - * @private - * @param {HTTPRequest} req - * @param {HTTPResponse} res - * @returns {Promise} - */ - -HTTPBase.prototype.handleMounts = async function handleMounts(req, res) { - let url = req.url; - - for (const route of this.mounts) { - const server = route.handler; - - if (!route.hasPrefix(req.pathname)) - continue; - - assert(url.indexOf(route.path) === 0); - - url = url.substring(route.path.length); - req = req.rewrite(url); - - await server.handleRequest(req, res); - - return true; - } - - return false; -}; - -/** - * Handle middleware stack. - * @private - * @param {HTTPRequest} req - * @param {HTTPResponse} res - * @returns {Promise} - */ - -HTTPBase.prototype.handleStack = async function handleStack(req, res) { - for (const route of this.stack) { - if (!route.hasPrefix(req.pathname)) - continue; - - if (await route.call(req, res)) - return true; - } - - return false; -}; - -/** - * Handle hook stack. - * @private - * @param {HTTPRequest} req - * @param {HTTPResponse} res - * @returns {Promise} - */ - -HTTPBase.prototype.handleHooks = async function handleHooks(req, res) { - for (const route of this.hooks) { - if (!route.hasPrefix(req.pathname)) - continue; - - if (await route.call(req, res)) - return true; - } - - return false; -}; - -/** - * Initialize websockets. - * @private - */ - -HTTPBase.prototype._initSockets = function _initSockets() { - if (!this.config.sockets) - return; - - let IOServer; - try { - IOServer = require('socket.io'); - } catch (e) { - ; - } - - if (!IOServer) - return; - - this.io = new IOServer({ - transports: ['websocket'], - serveClient: false - }); - - this.io.attach(this.server); - - this.io.on('connection', (ws) => { - this.addSocket(ws); - }); -}; - -/** - * Broadcast event to channel. - * @param {String} name - * @param {String} type - * @param {...Object} args - */ - -HTTPBase.prototype.to = function to(name, ...args) { - const sockets = this.channels.get(name); - - if (!sockets) - return; - - assert(sockets.size > 0); - - for (const socket of sockets) - socket.emit(...args); -}; - -/** - * Broadcast event to all connections. - * @param {String} channel - * @param {String} type - * @param {...Object} args - */ - -HTTPBase.prototype.all = function all() { - for (const socket of this.sockets) - socket.emit.apply(socket, arguments); -}; - -/** - * Add and initialize a websocket. - * @private - * @param {SocketIO.Socket} ws - */ - -HTTPBase.prototype.addSocket = function addSocket(ws) { - const socket = new WebSocket(ws, this); - - socket.on('error', (err) => { - this.emit('error', err); - }); - - socket.on('close', () => { - this.removeSocket(socket); - }); - - socket.on('join channel', (name) => { - this.joinChannel(socket, name); - }); - - socket.on('leave channel', (name) => { - this.leaveChannel(socket, name); - }); - - this.sockets.add(socket); - - for (const route of this.mounts) - route.handler.addSocket(ws); - - this.emit('socket', socket); -}; - -/** - * Remove a socket from lists. - * @private - * @param {WebSocket} socket - */ - -HTTPBase.prototype.removeSocket = function removeSocket(socket) { - for (const key of socket.channels) - this.leaveChannel(socket, key); - - assert(this.sockets.delete(socket)); -}; - -/** - * Add a socket to channel list. - * @private - * @param {WebSocket} socket - * @param {String} name - */ - -HTTPBase.prototype.joinChannel = function joinChannel(socket, name) { - if (socket.channels.has(name)) - return false; - - if (!this.channels.has(name)) - this.channels.set(name, new Set()); - - const sockets = this.channels.get(name); - - sockets.add(socket); - socket.channels.add(name); - - return true; -}; - -/** - * Remove a socket from channel list. - * @private - * @param {WebSocket} socket - * @param {String} name - */ - -HTTPBase.prototype.leaveChannel = function leaveChannel(socket, name) { - if (!socket.channels.has(name)) - return false; - - const sockets = this.channels.get(name); - - assert(sockets); - assert(sockets.delete(socket)); - - if (sockets.size === 0) - this.channels.delete(name); - - socket.channels.delete(name); - - return true; -}; - -/** - * Get channel list. - * @private - * @param {String} name - */ - -HTTPBase.prototype.channel = function channel(name) { - const sockets = this.channels.get(name); - - if (!sockets) - return null; - - assert(sockets.size > 0); - - return sockets; -}; - -/** - * Open the server. - * @alias HTTPBase#open - * @returns {Promise} - */ - -HTTPBase.prototype.open = function open() { - return this.listen(this.config.port, this.config.host); -}; - -/** - * Close the server. - * @alias HTTPBase#close - * @returns {Promise} - */ - -HTTPBase.prototype.close = function close() { - return new Promise((resolve, reject) => { - if (this.io) { - this.server.once('close', resolve); - this.io.close(); - return; - } - - this.server.close((err) => { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); -}; - -/** - * Mount a server. - * @param {String?} path - * @param {HTTPBase} server - * @param {Object?} ctx - */ - -HTTPBase.prototype.mount = function mount(path, server, ctx) { - if (!server) { - server = path; - path = null; - } - this.mounts.push(new Route(ctx || this, path, server)); -}; - -/** - * Add a middleware to the stack. - * @param {String?} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.use = function use(path, handler, ctx) { - if (!handler) { - handler = path; - path = null; - } - this.stack.push(new Route(ctx || this, path, handler)); -}; - -/** - * Add a hook to the stack. - * @param {String?} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.hook = function hook(path, handler, ctx) { - if (!handler) { - handler = path; - path = null; - } - this.hooks.push(new Route(ctx || this, path, handler)); -}; - -/** - * Add a GET route. - * @param {String} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.get = function get(path, handler, ctx) { - this.routes.get.push(new Route(ctx || this, path, handler)); -}; - -/** - * Add a POST route. - * @param {String} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.post = function post(path, handler, ctx) { - this.routes.post.push(new Route(ctx || this, path, handler)); -}; - -/** - * Add a PUT route. - * @param {String} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.put = function put(path, handler, ctx) { - this.routes.put.push(new Route(ctx || this, path, handler)); -}; - -/** - * Add a DELETE route. - * @param {String} path - * @param {Function} handler - * @param {Object?} ctx - */ - -HTTPBase.prototype.del = function del(path, handler, ctx) { - this.routes.del.push(new Route(ctx || this, path, handler)); -}; - -/** - * Get server address. - * @returns {Object} - */ - -HTTPBase.prototype.address = function address() { - return this.server.address(); -}; - -/** - * Listen on port and host. - * @param {Number} port - * @param {String} host - * @returns {Promise} - */ - -HTTPBase.prototype.listen = function listen(port, host) { - return new Promise((resolve, reject) => { - this.server.once('error', reject); - this.server.listen(port, host, () => { - const addr = this.address(); - - this.emit('listening', addr); - - this.server.removeListener('error', reject); - resolve(addr); - }); - }); -}; - -/** - * HTTP Base Options - * @alias module:http.HTTPBaseOptions - * @constructor - * @param {Object} options - */ - -function HTTPBaseOptions(options) { - if (!(this instanceof HTTPBaseOptions)) - return new HTTPBaseOptions(options); - - this.host = '127.0.0.1'; - this.port = 8080; - this.sockets = true; - - this.ssl = false; - this.keyFile = null; - this.certFile = null; - this.key = null; - this.cert = null; - this.ca = null; - - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {HTTPBaseOptions} - */ - -HTTPBaseOptions.prototype.fromOptions = function fromOptions(options) { - assert(options); - - if (options.host != null) { - assert(typeof options.host === 'string'); - this.host = options.host; - } - - if (options.port != null) { - assert((options.port & 0xffff) === options.port, 'Port must be a number.'); - this.port = options.port; - } - - if (options.sockets != null) { - assert(typeof options.sockets === 'boolean'); - this.sockets = options.sockets; - } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.keyFile = path.join(options.prefix, 'key.pem'); - this.certFile = path.join(options.prefix, 'cert.pem'); - } - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.keyFile != null) { - assert(typeof options.keyFile === 'string'); - this.keyFile = options.keyFile; - } - - if (options.certFile != null) { - assert(typeof options.certFile === 'string'); - this.certFile = options.certFile; - } - - if (options.key != null) { - assert(typeof options.key === 'string' || Buffer.isBuffer(options.key)); - this.key = options.key; - } - - if (options.cert != null) { - assert(typeof options.cert === 'string' || Buffer.isBuffer(options.cert)); - this.cert = options.cert; - } - - if (options.ca != null) { - assert(Array.isArray(options.ca)); - this.ca = options.ca; - } - - if (this.ssl) { - assert(this.key || this.keyFile, 'SSL specified with no provided key.'); - assert(this.cert || this.certFile, 'SSL specified with no provided cert.'); - } - - return this; -}; - -/** - * Instantiate http server options from object. - * @param {Object} options - * @returns {HTTPBaseOptions} - */ - -HTTPBaseOptions.fromOptions = function fromOptions(options) { - return new HTTPBaseOptions().fromOptions(options); -}; - -/** - * Load key and cert file. - * @private - */ - -HTTPBaseOptions.prototype.load = function load() { - if (!this.ssl) - return; - - if (this.keyFile) - this.key = fs.readFileSync(this.keyFile); - - if (this.certFile) - this.cert = fs.readFileSync(this.certFile); -}; - -/** - * Get HTTP server backend. - * @private - * @returns {Object} - */ - -HTTPBaseOptions.prototype.getBackend = function getBackend() { - return this.ssl ? require('https') : require('http'); -}; - -/** - * Get HTTP server options. - * @private - * @returns {Object} - */ - -HTTPBaseOptions.prototype.toHTTP = function toHTTP() { - if (!this.ssl) - return undefined; - - return { - key: this.key, - cert: this.cert, - ca: this.ca - }; -}; - -/** - * HTTP Base Options - * @alias module:http.BodyParserOptions - * @constructor - * @param {Object} options - */ - -function BodyParserOptions(options) { - if (!(this instanceof BodyParserOptions)) - return new BodyParserOptions(options); - - this.keyLimit = 100; - this.bodyLimit = 20 << 20; - this.contentType = null; - this.timeout = 10 * 1000; - - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {BodyParserOptions} - */ - -BodyParserOptions.prototype.fromOptions = function fromOptions(options) { - assert(options); - - if (options.keyLimit != null) { - assert(typeof options.keyLimit === 'number'); - this.keyLimit = options.keyLimit; - } - - if (options.bodyLimit != null) { - assert(typeof options.bodyLimit === 'number'); - this.bodyLimit = options.bodyLimit; - } - - if (options.contentType != null) { - assert(typeof options.contentType === 'string'); - this.contentType = options.contentType; - } - - return this; -}; - -/** - * Route - * @constructor - * @ignore - */ - -function Route(ctx, path, handler) { - if (!(this instanceof Route)) - return new Route(ctx, path, handler); - - this.ctx = null; - this.path = null; - this.handler = null; - - this.regex = /^/; - this.map = []; - this.compiled = false; - - if (ctx) { - assert(typeof ctx === 'object'); - this.ctx = ctx; - } - - if (path) { - if (path instanceof RegExp) { - this.regex = path; - } else { - assert(typeof path === 'string'); - assert(path.length > 0); - this.path = path; - } - } - - assert(handler); - assert(typeof handler === 'function' || typeof handler === 'object'); - - this.handler = handler; -} - -Route.prototype.compile = function compile() { - let path = this.path; - const map = this.map; - - if (this.compiled) - return; - - this.compiled = true; - - if (!path) - return; - - path = path.replace(/(\/[^\/]+)\?/g, '(?:$1)?'); - path = path.replace(/\.(?!\+)/g, '\\.'); - path = path.replace(/\*/g, '.*?'); - path = path.replace(/%/g, '\\'); - - path = path.replace(/:(\w+)/g, (str, name) => { - map.push(name); - return '([^/]+)'; - }); - - this.regex = new RegExp('^' + path + '$'); -}; - -Route.prototype.match = function match(pathname) { - this.compile(); - - assert(this.regex); - - const matches = this.regex.exec(pathname); - - if (!matches) - return null; - - const params = Object.create(null); - - for (let i = 1; i < matches.length; i++) { - const item = matches[i]; - const key = this.map[i - 1]; - - if (key) - params[key] = item; - - params[i - 1] = item; - } - - return params; -}; - -Route.prototype.hasPrefix = function hasPrefix(pathname) { - if (!this.path) - return true; - - if (pathname.startsWith) - return pathname.startsWith(this.path); - - return pathname.indexOf(this.path) === 0; -}; - -Route.prototype.call = async function call(req, res) { - await this.handler.call(this.ctx, req, res); - return res.sent; -}; - -/** - * Routes - * @constructor - * @ignore - */ - -function Routes() { - if (!(this instanceof Routes)) - return new Routes(); - - this.get = []; - this.post = []; - this.put = []; - this.del = []; -} - -Routes.prototype.getHandlers = function getHandlers(method) { - if (!method) - return null; - - method = method.toUpperCase(); - - switch (method) { - case 'GET': - return this.get; - case 'POST': - return this.post; - case 'PUT': - return this.put; - case 'DELETE': - return this.del; - default: - return null; - } -}; - -/** - * Request - * @constructor - * @ignore - */ - -function Request(req, res, url) { - if (!(this instanceof Request)) - return new Request(req, res, url); - - EventEmitter.call(this); - - this.req = null; - this.res = null; - this.socket = null; - this.method = 'GET'; - this.headers = Object.create(null); - this.contentType = 'bin'; - this.url = '/'; - this.pathname = '/'; - this.path = []; - this.trailing = false; - this.query = Object.create(null); - this.params = Object.create(null); - this.body = Object.create(null); - this.hasBody = false; - this.username = null; - this.readable = true; - this.writable = false; - - if (req) - this.init(req, res, url); -} - -Object.setPrototypeOf(Request.prototype, EventEmitter.prototype); - -Request.prototype.init = function init(req, res, url) { - assert(req); - assert(res); - - this.req = req; - this.res = res; - this.socket = req.socket; - this.method = req.method; - this.headers = req.headers; - this.contentType = parseType(req.headers['content-type']); - - req.on('error', (err) => { - this.emit('error', err); - }); - - req.on('data', (data) => { - this.emit('data', data); - }); - - req.on('end', () => { - this.emit('end'); - }); - - if (url != null) { - try { - this.parse(url); - } catch (e) { - ; - } - } -}; - -Request.prototype.parse = function parse(url) { - const uri = URL.parse(url); - - let pathname = uri.pathname; - let query = Object.create(null); - let trailing = false; - - if (pathname) { - pathname = pathname.replace(/\/{2,}/g, '/'); - - if (pathname[0] !== '/') - pathname = '/' + pathname; - - if (pathname.length > 1) { - if (pathname[pathname.length - 1] === '/') { - pathname = pathname.slice(0, -1); - trailing = true; - } - } - - pathname = pathname.replace(/%2f/gi, ''); - pathname = unescape(pathname); - } else { - pathname = '/'; - } - - assert(pathname.length > 0); - assert(pathname[0] === '/'); - - if (pathname.length > 1) - assert(pathname[pathname.length - 1] !== '/'); - - let path = pathname; - - if (path[0] === '/') - path = path.substring(1); - - let parts = path.split('/'); - - if (parts.length === 1) { - if (parts[0].length === 0) - parts = []; - } - - url = pathname; - - if (uri.search && uri.search.length > 1) { - assert(uri.search[0] === '?'); - url += uri.search; - } - - if (uri.hash && uri.hash.length > 1) { - assert(uri.hash[0] === '#'); - url += uri.hash; - } - - if (uri.query) - query = parsePairs(uri.query, 100); - - this.url = url; - this.pathname = pathname; - this.path = parts; - this.query = query; - this.trailing = trailing; -}; - -Request.prototype.rewrite = function rewrite(url) { - const req = new Request(); - req.init(this.req, this.res, url); - req.body = this.body; - req.hasBody = this.hasBody; - return req; -}; - -Request.prototype.pipe = function pipe(dest) { - return this.req.pipe(dest); -}; - -Request.prototype.pause = function pause() { - return this.req.pause(); -}; - -Request.prototype.resume = function resume() { - return this.req.resume(); -}; - -Request.prototype.destroy = function destroy() { - return this.req.destroy(); -}; - -/** - * Response - * @constructor - * @ignore - */ - -function Response(req, res) { - if (!(this instanceof Response)) - return new Response(req, res); - - EventEmitter.call(this); - - this.req = req; - this.res = res; - this.sent = false; - this.readable = false; - this.writable = true; - this.statusCode = 200; - this.res.statusCode = 200; - - if (req) - this.init(req, res); -} - -Object.setPrototypeOf(Response.prototype, EventEmitter.prototype); - -Response.prototype.init = function init(req, res) { - assert(req); - assert(res); - - res.on('error', (err) => { - this.emit('error', err); - }); - - res.on('drain', () => { - this.emit('drain'); - }); - - res.on('close', () => { - this.emit('close'); - }); -}; - -Response.prototype.setStatus = function setStatus(code) { - this.statusCode = code; - this.res.statusCode = code; -}; - -Response.prototype.setType = function setType(type) { - this.setHeader('Content-Type', getType(type)); -}; - -Response.prototype.hasType = function hasType() { - return this.getHeader('Content-Type') != null; -}; - -Response.prototype.destroy = function destroy() { - return this.res.destroy(); -}; - -Response.prototype.setHeader = function setHeader(key, value) { - return this.res.setHeader(key, value); -}; - -Response.prototype.getHeader = function getHeader(key) { - return this.res.getHeader(key); -}; - -Response.prototype.writeHead = function writeHead(code, headers) { - return this.res.writeHead(code, headers); -}; - -Response.prototype.write = function write(data, enc) { - return this.res.write(data, enc); -}; - -Response.prototype.end = function end(data, enc) { - this.sent = true; - return this.res.end(data, enc); -}; - -Response.prototype.error = function error(code, err) { - if (this.sent) - return; - - if (!code) - code = 400; - - this.send(code, { - error: { - type: err.type || 'Error', - message: err.message, - code: err.code - } - }); -}; - -Response.prototype.redirect = function redirect(code, url) { - if (!url) { - url = code; - code = 301; - } - - this.setStatus(code); - this.setHeader('Location', url); - this.end(); -}; - -Response.prototype.send = function send(code, msg, type) { - if (this.sent) - return; - - assert(typeof code === 'number', 'Code must be a number.'); - - if (msg == null) { - msg = { - error: { - type: 'Error', - message: 'No message.' - } - }; - } - - if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) { - msg = JSON.stringify(msg, null, 2) + '\n'; - if (!type) - type = 'json'; - assert(type === 'json', 'Bad type passed with json object.'); - } - - if (!type && !this.hasType()) - type = typeof msg === 'string' ? 'txt' : 'bin'; - - this.setStatus(code); - - if (type) - this.setType(type); - - if (typeof msg === 'string') { - const len = Buffer.byteLength(msg, 'utf8'); - this.setHeader('Content-Length', len.toString(10)); - try { - this.write(msg, 'utf8'); - this.end(); - } catch (e) { - ; - } - return; - } - - if (Buffer.isBuffer(msg)) { - this.setHeader('Content-Length', msg.length.toString(10)); - try { - this.write(msg); - this.end(); - } catch (e) { - ; - } - return; - } - - assert(false, 'Bad object passed to send.'); -}; - -/** - * WebSocket - * @constructor - * @ignore - * @param {SocketIO.Socket} - */ - -function WebSocket(socket, ctx) { - if (!(this instanceof WebSocket)) - return new WebSocket(socket, ctx); - - EventEmitter.call(this); - - this.context = ctx; - this.socket = socket; - this.remoteAddress = socket.conn.remoteAddress; - this.hooks = Object.create(null); - this.channels = new Set(); - this.auth = false; - this.filter = null; - - this.init(); -} - -Object.setPrototypeOf(WebSocket.prototype, EventEmitter.prototype); - -WebSocket.prototype.init = function init() { - const socket = this.socket; - const onevent = socket.onevent.bind(socket); - - socket.onevent = (packet) => { - const result = onevent(packet); - this.onevent(packet); - return result; - }; - - socket.on('error', (err) => { - this.dispatch('error', err); - }); - - socket.on('disconnect', () => { - this.dispatch('close'); - }); -}; - -WebSocket.prototype.onevent = async function onevent(packet) { - const args = (packet.data || []).slice(); - const type = args.shift() || ''; - - let ack; - if (typeof args[args.length - 1] === 'function') - ack = args.pop(); - else - ack = this.socket.ack(packet.id); - - let result; - try { - result = await this.fire(type, args); - } catch (e) { - ack({ - type: e.type || 'Error', - message: e.message, - code: e.code - }); - return; - } - - if (result === undefined) - return; - - ack(null, result); -}; - -WebSocket.prototype.hook = function hook(type, handler) { - assert(!this.hooks[type], 'Event already added.'); - this.hooks[type] = handler; -}; - -WebSocket.prototype.fire = async function fire(type, args) { - const handler = this.hooks[type]; - - if (!handler) - return undefined; - - return await handler.call(this.context, args); -}; - -WebSocket.prototype.join = function join(name) { - this.dispatch('join channel', name); -}; - -WebSocket.prototype.leave = function leave(name) { - this.dispatch('leave channel', name); -}; - -WebSocket.prototype.dispatch = function dispatch() { - const emit = EventEmitter.prototype.emit; - return emit.apply(this, arguments); -}; - -WebSocket.prototype.emit = function emit() { - return this.socket.emit.apply(this.socket, arguments); -}; - -WebSocket.prototype.call = function call(...args) { - const socket = this.socket; - return new Promise((resolve, reject) => { - args.push((err, result) => { - if (err) { - reject(err); - return; - } - resolve(result); - }); - socket.emit(...args); - }); -}; - -WebSocket.prototype.destroy = function destroy() { - return this.socket.disconnect(); -}; - -/* - * Helpers - */ - -function parsePairs(str, limit) { - const parts = str.split('&'); - const data = Object.create(null); - - if (parts.length > limit) - return data; - - assert(!limit || parts.length <= limit, 'Too many keys in querystring.'); - - for (const pair of parts) { - const index = pair.indexOf('='); - - let key, value; - if (index === -1) { - key = pair; - value = ''; - } else { - key = pair.substring(0, index); - value = pair.substring(index + 1); - } - - key = unescape(key); - - if (key.length === 0) - continue; - - value = unescape(value); - - if (value.length === 0) - continue; - - data[key] = value; - } - - return data; -} - -function unescape(str) { - try { - str = decodeURIComponent(str); - str = str.replace(/\+/g, ' '); - } catch (e) { - ; - } - str = str.replace(/\0/g, ''); - return str; -} - -function getType(type) { - switch (type) { - case 'json': - return 'application/json'; - case 'form': - return 'application/x-www-form-urlencoded; charset=utf-8'; - case 'html': - return 'text/html; charset=utf-8'; - case 'xml': - return 'application/xml; charset=utf-8'; - case 'js': - return 'application/javascript; charset=utf-8'; - case 'css': - return 'text/css; charset=utf-8'; - case 'txt': - return 'text/plain; charset=utf-8'; - case 'bin': - return 'application/octet-stream'; - default: - return type; - } -} - -function parseType(hdr) { - let type = hdr || ''; - type = type.split(';')[0]; - type = type.toLowerCase(); - type = type.trim(); - - switch (type) { - case 'text/x-json': - case 'application/json': - return 'json'; - case 'application/x-www-form-urlencoded': - return 'form'; - case 'text/html': - case 'application/xhtml+xml': - return 'html'; - case 'text/javascript': - case 'application/javascript': - return 'js'; - case 'text/css': - return 'css'; - case 'text/plain': - return 'txt'; - case 'application/octet-stream': - return 'bin'; - default: - return 'bin'; - } -} - -function isAscii(str) { - return typeof str === 'string' && /^[\t\n\r -~]*$/.test(str); -} - -function sha256(data, enc) { - return crypto.createHash('sha256').update(data, enc).digest(); -} - -function ccmp(a, b) { - return crypto.timingSafeEqual(a, b); -} - -/* - * Expose - */ - -module.exports = HTTPBase; diff --git a/lib/http/client-browser.js b/lib/http/client-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/http/client-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/http/client.js b/lib/http/client.js index d59cfd4f..2ff2050c 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -8,987 +8,283 @@ 'use strict'; const assert = require('assert'); -const Network = require('../protocol/network'); -const AsyncObject = require('../utils/asyncobject'); -const RPCClient = require('./rpcclient'); -const request = require('./request'); +const {Client} = require('bcurl'); -/** - * Bcoin HTTP client. - * @alias module:http.Client - * @constructor - * @param {String} uri - * @param {Object?} options - */ +class HTTPClient extends Client { + /** + * Bcoin HTTP client. + * @alias module:http.Client + * @constructor + * @param {String} uri + * @param {Object?} options + */ -function HTTPClient(options) { - if (!(this instanceof HTTPClient)) - return new HTTPClient(options); - - if (!options) - options = {}; - - if (typeof options === 'string') - options = { uri: options }; - - AsyncObject.call(this); - - this.options = options; - this.network = Network.get(options.network); - - this.uri = options.uri || `http://localhost:${this.network.rpcPort}`; - this.socket = null; - this.apiKey = options.apiKey; - this.auth = options.auth; - this.rpc = new RPCClient(options); -} - -Object.setPrototypeOf(HTTPClient.prototype, AsyncObject.prototype); - -/** - * Open the client, wait for socket to connect. - * @alias HTTPClient#open - * @returns {Promise} - */ - -HTTPClient.prototype._open = async function _open() { - let IOClient; - - try { - IOClient = require('socket.io-client'); - } catch (e) { - ; + constructor(options) { + super(options); } - if (!IOClient) - return; + /** + * Auth with server. + * @returns {Promise} + */ - this.socket = new IOClient(this.uri, { - transports: ['websocket'], - forceNew: true - }); - - this.socket.on('error', (err) => { - this.emit('error', err); - }); - - this.socket.on('version', (info) => { - if (info.network !== this.network.type) - this.emit('error', new Error('Wrong network.')); - }); - - this.socket.on('wallet tx', (details) => { - this.emit('tx', details); - }); - - this.socket.on('wallet confirmed', (details) => { - this.emit('confirmed', details); - }); - - this.socket.on('wallet unconfirmed', (details) => { - this.emit('unconfirmed', details); - }); - - this.socket.on('wallet conflict', (details) => { - this.emit('conflict', details); - }); - - this.socket.on('wallet updated', (details) => { - this.emit('updated', details); - }); - - this.socket.on('wallet address', (receive) => { - this.emit('address', receive); - }); - - this.socket.on('wallet balance', (balance) => { - this.emit('balance', balance); - }); - - await this.onConnect(); - await this.sendAuth(); -}; - -/** - * Close the client, wait for the socket to close. - * @alias HTTPClient#close - * @returns {Promise} - */ - -HTTPClient.prototype._close = function _close() { - if (!this.socket) - return Promise.resolve(); - - this.socket.disconnect(); - this.socket = null; - - return Promise.resolve(); -}; - -/** - * Wait for websocket connection. - * @private - * @returns {Promise} - */ - -HTTPClient.prototype.onConnect = function onConnect() { - return new Promise((resolve, reject) => { - this.socket.once('connect', resolve); - }); -}; - -/** - * Wait for websocket auth. - * @private - * @returns {Promise} - */ - -HTTPClient.prototype.sendAuth = function sendAuth() { - return new Promise((resolve, reject) => { - this.socket.emit('auth', this.apiKey, (err) => { - if (err) { - reject(new Error(err.message)); - return; - } - resolve(); - }); - }); -}; - -/** - * Wait for websocket auth. - * @private - * @returns {Promise} - */ - -HTTPClient.prototype.sendWalletAuth = function sendWalletAuth() { - return new Promise((resolve, reject) => { - this.socket.emit('wallet auth', this.apiKey, (err) => { - if (err) { - reject(new Error(err.message)); - return; - } - resolve(); - }); - }); -}; - -/** - * Wait for websocket disconnection. - * @private - * @returns {Promise} - */ - -HTTPClient.prototype.onDisconnect = function onDisconnect() { - return new Promise((resolve, reject) => { - this.socket.once('disconnect', resolve); - }); -}; - -/** - * Make an http request to endpoint. - * @private - * @param {String} method - * @param {String} endpoint - Path. - * @param {Object} json - Body or query depending on method. - * @returns {Promise} - Returns Object?. - */ - -HTTPClient.prototype._request = async function _request(method, endpoint, json) { - if (this.token) { - if (!json) - json = {}; - json.token = this.token; + async auth() { + return this.call('auth', this.password); } - let query; - if (json && method === 'get') { - query = json; - json = null; + /** + * Make an RPC call. + * @returns {Promise} + */ + + execute(name, params) { + return super.execute('/', name, params); } - const res = await request({ - method: method, - uri: this.uri + endpoint, - pool: true, - query: query, - json: json, - auth: { - username: 'bitcoinrpc', - password: this.apiKey || '' - } - }); - - if (res.statusCode === 404) - return null; - - if (res.statusCode === 401) - throw new Error('Unauthorized (bad API key).'); - - if (res.type !== 'json') - throw new Error('Bad response (wrong content-type).'); - - if (!res.body) - throw new Error('Bad response (no body).'); - - if (res.body.error) - throw new Error(res.body.error.message); - - if (res.statusCode !== 200) - throw new Error(`Status code: ${res.statusCode}.`); - - const network = res.headers['x-bcoin-network']; - - if (network && network !== this.network.type) - throw new Error('Bad response (wrong network).'); - - return res.body; -}; - -/** - * Make a GET http request to endpoint. - * @private - * @param {String} endpoint - Path. - * @param {Object} json - Querystring. - * @returns {Promise} - Returns Object?. - */ - -HTTPClient.prototype._get = function _get(endpoint, json) { - return this._request('get', endpoint, json); -}; - -/** - * Make a POST http request to endpoint. - * @private - * @param {String} endpoint - Path. - * @param {Object} json - Body. - * @returns {Promise} - Returns Object?. - */ - -HTTPClient.prototype._post = function _post(endpoint, json) { - return this._request('post', endpoint, json); -}; - -/** - * Make a PUT http request to endpoint. - * @private - * @param {String} endpoint - Path. - * @param {Object} json - Body. - * @returns {Promise} - Returns Object?. - */ - -HTTPClient.prototype._put = function _put(endpoint, json) { - return this._request('put', endpoint, json); -}; - -/** - * Make a DELETE http request to endpoint. - * @private - * @param {String} endpoint - Path. - * @param {Object} json - Body. - * @returns {Promise} - Returns Object?. - */ - -HTTPClient.prototype._del = function _del(endpoint, json) { - return this._request('delete', endpoint, json); -}; - -/** - * Get a mempool snapshot. - * @returns {Promise} - Returns {@link Hash}[]. - */ - -HTTPClient.prototype.getMempool = function getMempool() { - return this._get('/mempool'); -}; - -/** - * Get some info about the server (network and version). - * @returns {Promise} - Returns Object. - */ - -HTTPClient.prototype.getInfo = function getInfo() { - return this._get('/'); -}; - -/** - * Get coins that pertain to an address from the mempool or chain database. - * Takes into account spent coins in the mempool. - * @param {String} address - * @returns {Promise} - Returns {@link Coin}[]. - */ - -HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address) { - return this._get(`/coin/address/${address}`); -}; - -/** - * Get coins that pertain to addresses from the mempool or chain database. - * Takes into account spent coins in the mempool. - * @param {String[]} addresses - * @returns {Promise} - Returns {@link Coin}[]. - */ - -HTTPClient.prototype.getCoinsByAddresses = function getCoinsByAddresses(addresses) { - return this._post('/coin/address', { addresses }); -}; - -/** - * 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}. - */ - -HTTPClient.prototype.getCoin = function getCoin(hash, index) { - return this._get(`/coin/${hash}/${index}`); -}; - -/** - * Retrieve transactions pertaining to an - * address from the mempool or chain database. - * @param {String} address - * @returns {Promise} - Returns {@link TX}[]. - */ - -HTTPClient.prototype.getTXByAddress = function getTXByAddress(address) { - return this._get(`/tx/address/${address}`); -}; - -/** - * Retrieve transactions pertaining to - * addresses from the mempool or chain database. - * @param {String[]} addresses - * @returns {Promise} - Returns {@link TX}[]. - */ - -HTTPClient.prototype.getTXByAddresses = function getTXByAddresses(addresses) { - return this._post('/tx/address', { addresses }); -}; - -/** - * Retrieve a transaction from the mempool or chain database. - * @param {Hash} hash - * @returns {Promise} - Returns {@link TX}. - */ - -HTTPClient.prototype.getTX = function getTX(hash) { - return this._get(`/tx/${hash}`); -}; - -/** - * Retrieve a block from the chain database. - * @param {Hash|Number} block - * @returns {Promise} - Returns {@link Block}. - */ - -HTTPClient.prototype.getBlock = function getBlock(block) { - return this._get(`/block/${block}`); -}; - -/** - * Add a transaction to the mempool and broadcast it. - * @param {TX} tx - * @returns {Promise} - */ - -HTTPClient.prototype.broadcast = function broadcast(tx) { - return this._post('/broadcast', { tx: toHex(tx) }); -}; - -/** - * Rescan the chain. - * @param {Number} height - * @returns {Promise} - */ - -HTTPClient.prototype.rescan = function rescan(height) { - return this._post('/wallet/_admin/rescan', { height }); -}; - -/** - * Reset the chain. - * @param {Number} height - * @returns {Promise} - */ - -HTTPClient.prototype.reset = function reset(height) { - return this._post('/reset', { height }); -}; - -/** - * Resend pending transactions. - * @returns {Promise} - */ - -HTTPClient.prototype.resend = function resend() { - return this._post('/wallet/_admin/resend', {}); -}; - -/** - * Backup the walletdb. - * @param {String} path - * @returns {Promise} - */ - -HTTPClient.prototype.backup = function backup(path) { - return this._post('/wallet/_admin/backup', { path }); -}; - -/** - * Listen for events on wallet id. - * @param {String} id - * @param {String?} token - * @returns {Promise} - */ - -HTTPClient.prototype.join = function join(id, token) { - if (!this.socket) - return Promise.resolve(); - - return new Promise((resolve, reject) => { - this.socket.emit('wallet join', id, token, (err) => { - if (err) { - reject(new Error(err.message)); - return; - } - resolve(); - }); - }); -}; - -/** - * Unlisten for events on wallet id. - * @param {String} id - */ - -HTTPClient.prototype.leave = function leave(id) { - if (!this.socket) - return Promise.resolve(); - - return new Promise((resolve, reject) => { - this.socket.emit('wallet leave', id, (err) => { - if (err) { - reject(new Error(err.message)); - return; - } - resolve(); - }); - }); -}; - -/** - * Get list of all wallet IDs. - * @returns {Promise} - */ - -HTTPClient.prototype.getWallets = function getWallets() { - return this._get('/wallet/_admin/wallets'); -}; - -/** - * Create a wallet. - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createWallet = function createWallet(options) { - assert(options.id, 'Must pass an id parameter'); - return this._put(`/wallet/${options.id}`, options); -}; - -/** - * Get the raw wallet JSON. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getWallet = function getWallet(id) { - return this._get(`/wallet/${id}`); -}; - -/** - * Get wallet transaction history. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getHistory = function getHistory(id, account) { - return this._get(`/wallet/${id}/tx/history`, { account }); -}; - -/** - * Get wallet coins. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getCoins = function getCoins(id, account) { - return this._get(`/wallet/${id}/coin`, { account }); -}; - -/** - * Get all unconfirmed transactions. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getPending = function getPending(id, account) { - return this._get(`/wallet/${id}/tx/unconfirmed`, { account }); -}; - -/** - * Calculate wallet balance. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getBalance = function getBalance(id, account) { - return this._get(`/wallet/${id}/balance`, { account }); -}; - -/** - * Get last N wallet transactions. - * @param {String} id - * @param {Number} limit - Max number of transactions. - * @returns {Promise} - */ - -HTTPClient.prototype.getLast = function getLast(id, account, limit) { - return this._get(`/wallet/${id}/tx/last`, { account, limit }); -}; - -/** - * Get wallet transactions by timestamp range. - * @param {String} id - * @param {Object} options - * @param {Number} options.start - Start time. - * @param {Number} options.end - End time. - * @param {Number?} options.limit - Max number of records. - * @param {Boolean?} options.reverse - Reverse order. - * @returns {Promise} - */ - -HTTPClient.prototype.getRange = function getRange(id, account, options) { - const body = { - account: account, - start: options.start, - end: options.end , - limit: options.limit, - reverse: options.reverse - }; - return this._get(`/wallet/${id}/tx/range`, body); -}; - -/** - * Get transaction (only possible if the transaction - * is available in the wallet history). - * @param {String} id - * @param {Hash} hash - * @returns {Promise} - */ - -HTTPClient.prototype.getWalletTX = function getWalletTX(id, hash) { - return this._get(`/wallet/${id}/tx/${hash}`); -}; - -/** - * Get wallet blocks. - * @param {String} id - * @param {Number} height - * @returns {Promise} - */ - -HTTPClient.prototype.getWalletBlocks = function getWalletBlocks(id) { - return this._get(`/wallet/${id}/block`); -}; - -/** - * Get wallet block. - * @param {String} id - * @param {Number} height - * @returns {Promise} - */ - -HTTPClient.prototype.getWalletBlock = function getWalletBlock(id, height) { - return this._get(`/wallet/${id}/block/${height}`); -}; - -/** - * Get unspent coin (only possible if the transaction - * is available in the wallet history). - * @param {String} id - * @param {Hash} hash - * @param {Number} index - * @returns {Promise} - */ - -HTTPClient.prototype.getWalletCoin = function getWalletCoin(id, hash, index) { - return this._get(`/wallet/${id}/coin/${hash}/${index}`); -}; - -/** - * Create a transaction, fill, sign, and broadcast. - * @param {String} id - * @param {Object} options - * @param {String} options.address - * @param {Amount} options.value - * @returns {Promise} - */ - -HTTPClient.prototype.send = function send(id, options) { - const body = Object.assign({}, options); - - if (!body.outputs) - body.outputs = []; - - body.outputs = body.outputs.map((output) => { - return { - value: output.value, - address: output.address, - script: toHex(output.script) - }; - }); - - return this._post(`/wallet/${id}/send`, body); -}; - -/** - * Generate a new token. - * @param {(String|Buffer)?} passphrase - * @returns {Promise} - */ - -HTTPClient.prototype.retoken = async function retoken(id, passphrase) { - const body = await this._post(`/wallet/${id}/retoken`, { passphrase }); - return body.token; -}; - -/** - * Change or set master key's passphrase. - * @param {String|Buffer} passphrase - * @param {(String|Buffer)?} old - * @returns {Promise} - */ - -HTTPClient.prototype.setPassphrase = function setPassphrase(id, passphrase, old) { - const body = { passphrase , old }; - return this._post(`/wallet/${id}/passphrase`, body); -}; - -/** - * Create a transaction, fill. - * @param {String} id - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createTX = function createTX(id, options) { - const body = Object.assign({}, options); - - if (!body.outputs) - body.outputs = []; - - body.outputs = body.outputs.map((output) => { - return { - value: output.value, - address: output.address, - script: toHex(output.script) - }; - }); - - return this._post(`/wallet/${id}/create`, body); -}; - -/** - * Sign a transaction. - * @param {String} id - * @param {TX} tx - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.sign = function sign(id, tx, options) { - const body = Object.assign({}, options); - body.tx = toHex(tx); - return this._post(`/wallet/${id}/sign`, body); -}; - -/** - * @param {String} id - * @param {Number} now - Current time. - * @param {Number} age - Age delta (delete transactions older than `now - age`). - * @returns {Promise} - */ - -HTTPClient.prototype.zapWallet = function zapWallet(id, account, age) { - return this._post(`/wallet/${id}/zap`, { account, age }); -}; - -/** - * Get wallet key. - * @param {String} id - * @param {String} address - * @returns {Promise} - */ - -HTTPClient.prototype.getKey = function getKey(id, address) { - return this._get(`/wallet/${id}/key/${address}`); -}; - -/** - * Get wallet key WIF dump. - * @param {String} id - * @param {String} address - * @param {String?} passphrase - * @returns {Promise} - */ - -HTTPClient.prototype.getWIF = function getWIF(id, address, passphrase) { - return this._get(`/wallet/${id}/wif/${address}`, { passphrase }); -}; - -/** - * Add a public account/purpose key to the wallet for multisig. - * @param {String} id - * @param {(String|Number)?} account - * @param {Base58String} key - Account (bip44) or - * Purpose (bip45) key (can be in base58 form). - * @returns {Promise} - */ - -HTTPClient.prototype.addSharedKey = function addSharedKey(id, account, key) { - const body = { account: account, accountKey: key }; - return this._put(`/wallet/${id}/shared-key`, body); -}; - -/** - * Remove a public account/purpose key to the wallet for multisig. - * @param {String} id - * @param {(String|Number)?} account - * @param {Base58String} key - Account (bip44) or Purpose - * (bip45) key (can be in base58 form). - * @returns {Promise} - */ - -HTTPClient.prototype.removeSharedKey = function removeSharedKey(id, account, key) { - const body = { account: account, accountKey: key }; - return this._del(`/wallet/${id}/shared-key`, body); -}; - -/** - * Import private key. - * @param {String} id - * @param {Number|String} account - * @param {String} key - * @returns {Promise} - */ - -HTTPClient.prototype.importPrivate = function importPrivate(id, account, key, passphrase) { - const body = { account: account, privateKey: key, passphrase: passphrase }; - return this._post(`/wallet/${id}/import`, body); -}; - -/** - * Import public key. - * @param {String} id - * @param {Number|String} account - * @param {String} key - * @returns {Promise} - */ - -HTTPClient.prototype.importPublic = function importPublic(id, account, key) { - const body = { account: account, publicKey: key }; - return this._post(`/wallet/${id}/import`, body); -}; - -/** - * Import address. - * @param {String} id - * @param {Number|String} account - * @param {String} address - * @returns {Promise} - */ - -HTTPClient.prototype.importAddress = function importAddress(id, account, address) { - return this._post(`/wallet/${id}/import`, { account, address }); -}; - -/** - * Lock a coin. - * @param {String} id - * @param {String} hash - * @param {Number} index - * @returns {Promise} - */ - -HTTPClient.prototype.lockCoin = function lockCoin(id, hash, index) { - return this._put(`/wallet/${id}/locked/${hash}/${index}`, {}); -}; - -/** - * Unlock a coin. - * @param {String} id - * @param {String} hash - * @param {Number} index - * @returns {Promise} - */ - -HTTPClient.prototype.unlockCoin = function unlockCoin(id, hash, index) { - return this._del(`/wallet/${id}/locked/${hash}/${index}`, {}); -}; - -/** - * Get locked coins. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getLocked = function getLocked(id) { - return this._get(`/wallet/${id}/locked`); -}; - -/** - * Lock wallet. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.lock = function lock(id) { - return this._post(`/wallet/${id}/lock`, {}); -}; - -/** - * Unlock wallet. - * @param {String} id - * @param {String} passphrase - * @param {Number} timeout - * @returns {Promise} - */ - -HTTPClient.prototype.unlock = function unlock(id, passphrase, timeout) { - return this._post(`/wallet/${id}/unlock`, { passphrase, timeout }); -}; - -/** - * Resend pending wallet transactions. - * @returns {Promise} - */ - -HTTPClient.prototype.resendWallet = function resendWallet(id) { - return this._post(`/wallet/${id}/resend`, {}); -}; - -/** - * Get wallet accounts. - * @param {String} id - * @returns {Promise} - Returns Array. - */ - -HTTPClient.prototype.getAccounts = function getAccounts(id) { - return this._get(`/wallet/${id}/account`); -}; - -/** - * Get wallet master key. - * @param {String} id - * @returns {Promise} - */ - -HTTPClient.prototype.getMaster = function getMaster(id) { - return this._get(`/wallet/${id}/master`); -}; - -/** - * Get wallet account. - * @param {String} id - * @param {String} account - * @returns {Promise} - */ - -HTTPClient.prototype.getAccount = function getAccount(id, account) { - return this._get(`/wallet/${id}/account/${account}`); -}; - -/** - * Create account. - * @param {String} id - * @param {String} name - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createAccount = function createAccount(id, name, options) { - return this._put(`/wallet/${id}/account/${name}`, options || {}); -}; - -/** - * Create address. - * @param {String} id - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createAddress = function createAddress(id, options) { - if (!options) - options = {}; - - if (typeof options === 'string') - options = { account: options }; - - return this._post(`/wallet/${id}/address`, options); -}; - -/** - * Create change address. - * @param {String} id - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createChange = function createChange(id, options) { - if (!options) - options = {}; - - if (typeof options === 'string') - options = { account: options }; - - return this._post(`/wallet/${id}/change`, options); -}; - -/** - * Create nested address. - * @param {String} id - * @param {Object} options - * @returns {Promise} - */ - -HTTPClient.prototype.createNested = function createNested(id, options) { - if (!options) - options = {}; - - if (typeof options === 'string') - options = { account: options }; - - return this._post(`/wallet/${id}/nested`, options); -}; - -/* - * Helpers - */ - -function toHex(obj) { - if (!obj) - return null; - - if (obj.toRaw) - obj = obj.toRaw(); - - if (Buffer.isBuffer(obj)) - obj = obj.toString('hex'); - - return obj; + /** + * Get a mempool snapshot. + * @returns {Promise} - Returns {@link Hash}[]. + */ + + getMempool() { + return this.get('/mempool'); + } + + /** + * Get some info about the server (network and version). + * @returns {Promise} - Returns Object. + */ + + getInfo() { + return this.get('/'); + } + + /** + * Get coins that pertain to an address from the mempool or chain database. + * Takes into account spent coins in the mempool. + * @param {String} address + * @returns {Promise} - Returns {@link Coin}[]. + */ + + getCoinsByAddress(address) { + assert(typeof address === 'string'); + return this.get(`/coin/address/${address}`); + } + + /** + * Get coins that pertain to addresses from the mempool or chain database. + * Takes into account spent coins in the mempool. + * @param {String[]} addresses + * @returns {Promise} - Returns {@link Coin}[]. + */ + + getCoinsByAddresses(addresses) { + assert(Array.isArray(addresses)); + return this.post('/coin/address', { addresses }); + } + + /** + * 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}. + */ + + getCoin(hash, index) { + assert(typeof hash === 'string'); + assert((index >>> 0) === index); + return this.get(`/coin/${hash}/${index}`); + } + + /** + * Retrieve transactions pertaining to an + * address from the mempool or chain database. + * @param {String} address + * @returns {Promise} - Returns {@link TX}[]. + */ + + getTXByAddress(address) { + assert(typeof address === 'string'); + return this.get(`/tx/address/${address}`); + } + + /** + * Retrieve transactions pertaining to + * addresses from the mempool or chain database. + * @param {String[]} addresses + * @returns {Promise} - Returns {@link TX}[]. + */ + + getTXByAddresses(addresses) { + assert(Array.isArray(addresses)); + return this.post('/tx/address', { addresses }); + } + + /** + * Retrieve a transaction from the mempool or chain database. + * @param {Hash} hash + * @returns {Promise} - Returns {@link TX}. + */ + + getTX(hash) { + assert(typeof hash === 'string'); + return this.get(`/tx/${hash}`); + } + + /** + * Retrieve a block from the chain database. + * @param {Hash|Number} block + * @returns {Promise} - Returns {@link Block}. + */ + + getBlock(block) { + assert(typeof block === 'string' || typeof block === 'number'); + return this.get(`/block/${block}`); + } + + /** + * Add a transaction to the mempool and broadcast it. + * @param {TX} tx + * @returns {Promise} + */ + + broadcast(tx) { + assert(typeof tx === 'string'); + return this.post('/broadcast', { tx }); + } + + /** + * Reset the chain. + * @param {Number} height + * @returns {Promise} + */ + + reset(height) { + return this.post('/reset', { height }); + } + + /** + * Watch the blockchain. + * @private + * @returns {Promise} + */ + + watchChain() { + return this.call('watch chain'); + } + + /** + * Watch the blockchain. + * @private + * @returns {Promise} + */ + + watchMempool() { + return this.call('watch mempool'); + } + + /** + * Get chain tip. + * @returns {Promise} + */ + + getTip() { + return this.call('get tip'); + } + + /** + * Get chain entry. + * @param {Hash} hash + * @returns {Promise} + */ + + getEntry(block) { + return this.call('get entry', block); + } + + /** + * Get hashes. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ + + getHashes(start, end) { + return this.call('get hashes', start, end); + } + + /** + * Send a transaction. Do not wait for promise. + * @param {TX} tx + * @returns {Promise} + */ + + send(tx) { + assert(Buffer.isBuffer(tx)); + return this.call('send', tx); + } + + /** + * Set bloom filter. + * @param {Bloom} filter + * @returns {Promise} + */ + + setFilter(filter) { + assert(Buffer.isBuffer(filter)); + return this.call('set filter', filter); + } + + /** + * Add data to filter. + * @param {Buffer} data + * @returns {Promise} + */ + + addFilter(chunks) { + if (!Array.isArray(chunks)) + chunks = [chunks]; + + return this.call('add filter', chunks); + } + + /** + * Reset filter. + * @returns {Promise} + */ + + resetFilter() { + return this.call('reset filter'); + } + + /** + * Esimate smart fee. + * @param {Number?} blocks + * @returns {Promise} + */ + + estimateFee(blocks) { + assert(blocks == null || typeof blocks === 'number'); + return this.call('estimate fee', blocks); + } + + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + rescan(start) { + if (start == null) + start = 0; + + assert(typeof start === 'number' || typeof start === 'string'); + + return this.call('rescan', start); + } } /* diff --git a/lib/http/index.js b/lib/http/index.js index 99c6cdec..366b1b17 100644 --- a/lib/http/index.js +++ b/lib/http/index.js @@ -11,11 +11,7 @@ * @module http */ -exports.Base = require('./base'); exports.Client = require('./client'); -exports.request = require('./request'); -exports.RPCBase = require('./rpcbase'); -exports.RPCClient = require('./rpcclient'); exports.RPC = require('./rpc'); exports.Server = require('./server'); exports.Wallet = require('./wallet'); diff --git a/lib/http/request-browser.js b/lib/http/request-browser.js deleted file mode 100644 index b4b96fb0..00000000 --- a/lib/http/request-browser.js +++ /dev/null @@ -1,458 +0,0 @@ -/*! - * request.js - http request for bcoin - * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const assert = require('assert'); -const EventEmitter = require('events'); -const URL = require('url'); -const qs = require('querystring'); -const fetch = global.fetch; -const FetchHeaders = global.Headers; - -/* - * Constants - */ - -const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)' - + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36'; - -/** - * Request Options - * @constructor - * @ignore - * @param {Object} options - */ - -function RequestOptions(options) { - if (!(this instanceof RequestOptions)) - return new RequestOptions(options); - - this.uri = 'http://localhost:80/'; - this.host = 'localhost'; - this.path = '/'; - this.port = 80; - this.ssl = false; - this.method = 'GET'; - this.strictSSL = true; - this.agent = USER_AGENT; - - this.type = null; - this.expect = null; - this.query = null; - this.body = null; - this.auth = null; - this.limit = 10 << 20; - this.timeout = 5000; - this.buffer = false; - - if (options) - this.fromOptions(options); -} - -RequestOptions.prototype.setURI = function setURI(uri) { - assert(typeof uri === 'string'); - - if (!/:\/\//.test(uri)) - uri = (this.ssl ? 'https://' : 'http://') + uri; - - uri = URL.parse(uri); - - assert(uri.protocol === 'http:' || uri.protocol === 'https:'); - - this.uri = uri; - this.ssl = uri.protocol === 'https:'; - - if (uri.search) - this.query = qs.parse(uri.search); - - this.host = uri.hostname; - this.path = uri.pathname; - this.port = uri.port || (this.ssl ? 443 : 80); - - if (uri.auth) { - const parts = uri.auth.split(':'); - this.auth = { - username: parts[0] || '', - password: parts[1] || '' - }; - } -}; - -RequestOptions.prototype.fromOptions = function fromOptions(options) { - if (typeof options === 'string') - options = { uri: options }; - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.uri != null) - this.setURI(options.uri); - - if (options.url != null) - this.setURI(options.url); - - if (options.method != null) { - assert(typeof options.method === 'string'); - this.method = options.method.toUpperCase(); - } - - if (options.strictSSL != null) { - assert(typeof options.strictSSL === 'boolean'); - this.strictSSL = options.strictSSL; - } - - if (options.agent != null) { - assert(typeof options.agent === 'string'); - this.agent = options.agent; - } - - if (options.auth != null) { - assert(typeof options.auth === 'object'); - assert(typeof options.auth.username === 'string'); - assert(typeof options.auth.password === 'string'); - this.auth = options.auth; - } - - if (options.query != null) { - if (typeof options.query === 'string') { - this.query = qs.stringify(options.query); - } else { - assert(typeof options.query === 'object'); - this.query = options.query; - } - } - - if (options.json != null) { - assert(typeof options.json === 'object'); - this.body = Buffer.from(JSON.stringify(options.json), 'utf8'); - this.type = 'json'; - } - - if (options.form != null) { - assert(typeof options.form === 'object'); - this.body = Buffer.from(qs.stringify(options.form), 'utf8'); - this.type = 'form'; - } - - if (options.type != null) { - assert(typeof options.type === 'string'); - assert(getType(options.type)); - this.type = options.type; - } - - if (options.expect != null) { - assert(typeof options.expect === 'string'); - assert(getType(options.expect)); - this.expect = options.expect; - } - - if (options.body != null) { - if (typeof options.body === 'string') { - this.body = Buffer.from(options.body, 'utf8'); - } else { - assert(Buffer.isBuffer(options.body)); - this.body = options.body; - } - } - - if (options.timeout != null) { - assert(typeof options.timeout === 'number'); - this.timeout = options.timeout; - } - - if (options.limit != null) { - assert(typeof options.limit === 'number'); - this.limit = options.limit; - } - - if (options.buffer != null) { - assert(typeof options.buffer === 'boolean'); - this.buffer = options.buffer; - } -}; - -RequestOptions.prototype.isExpected = function isExpected(type) { - if (!this.expect) - return true; - - return this.expect === type; -}; - -RequestOptions.prototype.isOverflow = function isOverflow(hdr) { - if (!hdr) - return false; - - if (!this.buffer) - return false; - - const length = parseInt(hdr, 10); - - if (!isFinite(length)) - return true; - - return length > this.limit; -}; - -RequestOptions.prototype.getHeaders = function getHeaders() { - const headers = new FetchHeaders(); - - headers.append('User-Agent', this.agent); - - if (this.type) - headers.append('Content-Type', getType(this.type)); - - if (this.body) - headers.append('Content-Length', this.body.length.toString(10)); - - if (this.auth) { - const auth = `${this.auth.username}:${this.auth.password}`; - const data = Buffer.from(auth, 'utf8'); - headers.append('Authorization', `Basic ${data.toString('base64')}`); - } - - return headers; -}; - -RequestOptions.prototype.toURL = function toURL() { - let url = ''; - - if (this.ssl) - url += 'https://'; - else - url += 'http://'; - - url += this.host; - url += ':' + this.port; - url += this.path; - - if (this.query) - url += '?' + qs.stringify(this.query); - - return url; -}; - -RequestOptions.prototype.toHTTP = function toHTTP() { - return { - method: this.method, - headers: this.getHeaders(), - body: this.body.buffer, - mode: 'cors', - credentials: 'include', - cache: 'no-cache', - redirect: 'follow', - referrer: 'no-referrer' - }; -}; - -/** - * Response - * @constructor - * @ignore - */ - -function Response() { - this.statusCode = 0; - this.headers = Object.create(null); - this.type = 'bin'; - this.body = null; -} - -Response.fromFetch = function fromFetch(response) { - const res = new Response(); - - res.statusCode = response.status; - - for (const [key, value] of response.headers.entries()) - res.headers[key.toLowerCase()] = value; - - const contentType = res.headers['content-type']; - - res.type = parseType(contentType); - - return res; -}; - -/** - * Make an HTTP request. - * @private - * @param {Object} options - * @returns {Promise} - */ - -async function _request(options) { - if (typeof fetch !== 'function') - throw new Error('Fetch API not available.'); - - const opt = new RequestOptions(options); - const response = await fetch(opt.toURL(), opt.toHTTP()); - const res = Response.fromFetch(response); - - if (!opt.isExpected(res.type)) - throw new Error('Wrong content-type for response.'); - - const length = res.headers['content-length']; - - if (opt.isOverflow(length)) - throw new Error('Response exceeded limit.'); - - if (opt.buffer) { - switch (res.type) { - case 'bin': { - const data = await response.arrayBuffer(); - res.body = Buffer.from(data.buffer); - if (opt.limit && res.body.length > opt.limit) - throw new Error('Response exceeded limit.'); - break; - } - case 'json': { - res.body = await response.json(); - break; - } - case 'form': { - const data = await response.formData(); - res.body = Object.create(null); - for (const [key, value] of data.entries()) - res.body[key] = value; - break; - } - default: { - res.body = await response.text(); - if (opt.limit && res.body.length > opt.limit) - throw new Error('Response exceeded limit.'); - break; - } - } - } else { - res.body = await response.arrayBuffer(); - } - - return res; -} - -/** - * Make an HTTP request. - * @alias module:http.request - * @param {Object} options - * @param {String} options.uri - * @param {Object?} options.query - * @param {Object?} options.body - * @param {Object?} options.json - * @param {Object?} options.form - * @param {String?} options.type - One of `"json"`, - * `"form"`, `"text"`, or `"bin"`. - * @param {String?} options.agent - User agent string. - * @param {Object?} [options.strictSSL=true] - Whether to accept bad certs. - * @param {Object?} options.method - HTTP method. - * @param {Object?} options.auth - * @param {String?} options.auth.username - * @param {String?} options.auth.password - * @param {String?} options.expect - Type to expect (see options.type). - * Error will be returned if the response is not of this type. - * @param {Number?} options.limit - Byte limit on response. - * @returns {Promise} - */ - -async function request(options) { - if (typeof options === 'string') - options = { uri: options }; - - options.buffer = true; - - return _request(options); -} - -request.stream = function stream(options) { - const s = new EventEmitter(); - - s.write = (data) => { - options.body = data; - return true; - }; - - s.end = () => { - _request(options).then((res) => { - s.emit('headers', res.headers); - s.emit('type', res.type); - s.emit('response', res); - s.emit('data', res.body); - s.emit('end'); - s.emit('close'); - }).catch((err) => { - s.emit('error', err); - }); - return true; - }; - - return s; -}; - -/* - * Helpers - */ - -function parseType(hdr) { - let type = hdr || ''; - type = type.split(';')[0]; - type = type.toLowerCase(); - type = type.trim(); - - switch (type) { - case 'text/x-json': - case 'application/json': - return 'json'; - case 'application/x-www-form-urlencoded': - return 'form'; - case 'text/html': - case 'application/xhtml+xml': - return 'html'; - case 'text/xml': - case 'application/xml': - return 'xml'; - case 'text/javascript': - case 'application/javascript': - return 'js'; - case 'text/css': - return 'css'; - case 'text/plain': - return 'txt'; - case 'application/octet-stream': - return 'bin'; - default: - return 'bin'; - } -} - -function getType(type) { - switch (type) { - case 'json': - return 'application/json; charset=utf-8'; - case 'form': - return 'application/x-www-form-urlencoded; charset=utf-8'; - case 'html': - return 'text/html; charset=utf-8'; - case 'xml': - return 'application/xml; charset=utf-8'; - case 'js': - return 'application/javascript; charset=utf-8'; - case 'css': - return 'text/css; charset=utf-8'; - case 'txt': - return 'text/plain; charset=utf-8'; - case 'bin': - return 'application/octet-stream'; - default: - throw new Error(`Unknown type: ${type}.`); - } -} - -/* - * Expose - */ - -module.exports = request; diff --git a/lib/http/request.js b/lib/http/request.js deleted file mode 100644 index 52598fbc..00000000 --- a/lib/http/request.js +++ /dev/null @@ -1,672 +0,0 @@ -/*! - * request.js - http request for bcoin - * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const assert = require('assert'); -const {Stream} = require('stream'); - -/* - * Lazily Loaded - */ - -let url = null; -let qs = null; -let http = null; -let https = null; -let StringDecoder = null; - -/* - * Constants - */ - -const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)' - + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36'; - -/** - * Request Options - * @constructor - * @ignore - * @param {Object} options - */ - -function RequestOptions(options) { - if (!(this instanceof RequestOptions)) - return new RequestOptions(options); - - this.uri = 'http://localhost:80/'; - this.host = 'localhost'; - this.path = '/'; - this.port = 80; - this.ssl = false; - this.method = 'GET'; - this.strictSSL = true; - this.pool = false; - this.agent = USER_AGENT; - - this.type = null; - this.expect = null; - this.query = null; - this.body = null; - this.auth = null; - this.limit = 10 << 20; - this.maxRedirects = 5; - this.timeout = 5000; - this.buffer = false; - this.headers = null; - - // Hack - ensureRequires(); - - if (options) - this.fromOptions(options); -} - -RequestOptions.prototype.setURI = function setURI(uri) { - assert(typeof uri === 'string'); - - if (!/:\/\//.test(uri)) - uri = (this.ssl ? 'https://' : 'http://') + uri; - - uri = url.parse(uri); - - assert(uri.protocol === 'http:' || uri.protocol === 'https:'); - - this.uri = uri; - this.ssl = uri.protocol === 'https:'; - - if (uri.search) - this.query = qs.parse(uri.search); - - this.host = uri.hostname; - this.path = uri.pathname; - this.port = uri.port || (this.ssl ? 443 : 80); - - if (uri.auth) { - const parts = uri.auth.split(':'); - this.auth = { - username: parts[0] || '', - password: parts[1] || '' - }; - } -}; - -RequestOptions.prototype.fromOptions = function fromOptions(options) { - if (typeof options === 'string') - options = { uri: options }; - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.uri != null) - this.setURI(options.uri); - - if (options.url != null) - this.setURI(options.url); - - if (options.method != null) { - assert(typeof options.method === 'string'); - this.method = options.method.toUpperCase(); - } - - if (options.strictSSL != null) { - assert(typeof options.strictSSL === 'boolean'); - this.strictSSL = options.strictSSL; - } - - if (options.pool != null) { - assert(typeof options.pool === 'boolean'); - this.pool = options.pool; - } - - if (options.agent != null) { - assert(typeof options.agent === 'string'); - this.agent = options.agent; - } - - if (options.auth != null) { - assert(typeof options.auth === 'object'); - assert(typeof options.auth.username === 'string'); - assert(typeof options.auth.password === 'string'); - this.auth = options.auth; - } - - if (options.query != null) { - if (typeof options.query === 'string') { - this.query = qs.stringify(options.query); - } else { - assert(typeof options.query === 'object'); - this.query = options.query; - } - } - - if (options.json != null) { - assert(typeof options.json === 'object'); - this.body = Buffer.from(JSON.stringify(options.json), 'utf8'); - this.type = 'json'; - } - - if (options.form != null) { - assert(typeof options.form === 'object'); - this.body = Buffer.from(qs.stringify(options.form), 'utf8'); - this.type = 'form'; - } - - if (options.type != null) { - assert(typeof options.type === 'string'); - assert(getType(options.type)); - this.type = options.type; - } - - if (options.expect != null) { - assert(typeof options.expect === 'string'); - assert(getType(options.expect)); - this.expect = options.expect; - } - - if (options.body != null) { - if (typeof options.body === 'string') { - this.body = Buffer.from(options.body, 'utf8'); - } else { - assert(Buffer.isBuffer(options.body)); - this.body = options.body; - } - } - - if (options.timeout != null) { - assert(typeof options.timeout === 'number'); - this.timeout = options.timeout; - } - - if (options.limit != null) { - assert(typeof options.limit === 'number'); - this.limit = options.limit; - } - - if (options.maxRedirects != null) { - assert(typeof options.maxRedirects === 'number'); - this.maxRedirects = options.maxRedirects; - } - - if (options.buffer != null) { - assert(typeof options.buffer === 'boolean'); - this.buffer = options.buffer; - } - - if (options.headers != null) { - assert(typeof options.headers === 'object'); - this.headers = options.headers; - } -}; - -RequestOptions.prototype.isExpected = function isExpected(type) { - if (!this.expect) - return true; - - return this.expect === type; -}; - -RequestOptions.prototype.isOverflow = function isOverflow(hdr) { - if (!hdr) - return false; - - if (!this.buffer) - return false; - - const length = parseInt(hdr, 10); - - if (!isFinite(length)) - return true; - - return length > this.limit; -}; - -RequestOptions.prototype.getBackend = function getBackend() { - ensureRequires(this.ssl); - return this.ssl ? https : http; -}; - -RequestOptions.prototype.getHeaders = function getHeaders() { - if (this.headers) - return this.headers; - - const headers = Object.create(null); - - headers['User-Agent'] = this.agent; - - if (this.type) - headers['Content-Type'] = getType(this.type); - - if (this.body) - headers['Content-Length'] = this.body.length.toString(10); - - if (this.auth) { - const auth = `${this.auth.username}:${this.auth.password}`; - const data = Buffer.from(auth, 'utf8'); - headers['Authorization'] = `Basic ${data.toString('base64')}`; - } - - return headers; -}; - -RequestOptions.prototype.toHTTP = function toHTTP() { - let query = ''; - - if (this.query) - query = '?' + qs.stringify(this.query); - - return { - method: this.method, - host: this.host, - port: this.port, - path: this.path + query, - headers: this.getHeaders(), - agent: this.pool ? null : false, - rejectUnauthorized: this.strictSSL - }; -}; - -/** - * Request - * @alias module:http.Request - * @constructor - * @private - * @param {Object} options - */ - -function Request(options) { - if (!(this instanceof Request)) - return new Request(options); - - Stream.call(this); - - this.options = new RequestOptions(options); - this.request = null; - this.response = null; - this.statusCode = 0; - this.headers = null; - this.type = 'bin'; - this.redirects = 0; - this.timeout = null; - this.finished = false; - - this.onResponse = this._onResponse.bind(this); - this.onData = this._onData.bind(this); - this.onEnd = this._onEnd.bind(this); - - this.total = 0; - this.decoder = null; - this.body = null; - this.buffer = null; -} - -Object.setPrototypeOf(Request.prototype, Stream.prototype); - -Request.prototype.startTimeout = function startTimeout() { - if (!this.options.timeout) - return; - - this.timeout = setTimeout(() => { - this.finish(new Error('Request timed out.')); - }, this.options.timeout); -}; - -Request.prototype.stopTimeout = function stopTimeout() { - if (this.timeout != null) { - clearTimeout(this.timeout); - this.timeout = null; - } -}; - -Request.prototype.cleanup = function cleanup() { - this.stopTimeout(); - - if (this.request) { - this.request.removeListener('response', this.onResponse); - this.request.removeListener('error', this.onEnd); - this.request.addListener('error', () => {}); - } - - if (this.response) { - this.response.removeListener('data', this.onData); - this.response.removeListener('error', this.onEnd); - this.response.removeListener('end', this.onEnd); - this.response.addListener('error', () => {}); - } -}; - -Request.prototype.close = function close() { - if (this.request) { - try { - this.request.abort(); - } catch (e) { - ; - } - } - - if (this.response) { - try { - this.response.destroy(); - } catch (e) { - ; - } - } - - this.cleanup(); - - this.request = null; - this.response = null; -}; - -Request.prototype.destroy = function destroy() { - this.close(); -}; - -Request.prototype.start = function start() { - const backend = this.options.getBackend(); - const options = this.options.toHTTP(); - - this.startTimeout(); - - this.request = backend.request(options); - this.response = null; - - if (this.options.body) - this.request.write(this.options.body); - - this.request.on('response', this.onResponse); - this.request.on('error', this.onEnd); -}; - -Request.prototype.write = function write(data) { - return this.request.write(data); -}; - -Request.prototype.end = function end() { - return this.request.end(); -}; - -Request.prototype.finish = function finish(err) { - if (this.finished) - return; - - this.finished = true; - - if (err) { - this.destroy(); - this.emit('error', err); - return; - } - - this.cleanup(); - - if (this.options.buffer) { - assert(this.buffer != null); - switch (this.type) { - case 'bin': { - this.body = Buffer.concat(this.buffer); - this.buffer = null; - break; - } - case 'json': { - const buffer = this.buffer.trim(); - - this.buffer = null; - - if (buffer.length === 0) - break; - - let body; - try { - body = JSON.parse(buffer); - } catch (e) { - this.emit('error', e); - return; - } - - if (!body || typeof body !== 'object') { - this.emit('error', new Error('JSON body is a non-object.')); - return; - } - - this.body = body; - - break; - } - case 'form': { - const buffer = this.buffer; - - this.buffer = null; - - try { - this.body = qs.parse(buffer); - } catch (e) { - this.emit('error', e); - return; - } - - break; - } - default: { - this.body = this.buffer; - this.buffer = null; - break; - } - } - } - - this.emit('end'); - this.emit('close'); -}; - -Request.prototype._onResponse = function _onResponse(response) { - const location = response.headers['location']; - - if (location) { - if (++this.redirects > this.options.maxRedirects) { - this.finish(new Error('Too many redirects.')); - return; - } - this.close(); - this.options.setURI(location); - this.start(); - this.end(); - return; - } - - const contentType = response.headers['content-type']; - const type = parseType(contentType); - - if (!this.options.isExpected(type)) { - this.finish(new Error('Wrong content-type for response.')); - return; - } - - const length = response.headers['content-length']; - - if (this.options.isOverflow(length)) { - this.finish(new Error('Response exceeded limit.')); - return; - } - - this.response = response; - this.statusCode = response.statusCode; - this.headers = response.headers; - this.type = type; - - this.response.on('data', this.onData); - this.response.on('error', this.onEnd); - this.response.on('end', this.onEnd); - - this.emit('headers', response.headers); - this.emit('type', this.type); - this.emit('response', response); - - if (this.options.buffer) { - if (this.type !== 'bin') { - this.decoder = new StringDecoder('utf8'); - this.buffer = ''; - } else { - this.buffer = []; - } - } -}; - -Request.prototype._onData = function _onData(data) { - this.total += data.length; - - this.emit('data', data); - - if (this.options.buffer) { - if (this.options.limit) { - if (this.total > this.options.limit) { - this.finish(new Error('Response exceeded limit.')); - return; - } - } - - if (this.decoder) { - this.buffer += this.decoder.write(data); - return; - } - - this.buffer.push(data); - } -}; - -Request.prototype._onEnd = function _onEnd(err) { - this.finish(err); -}; - -/** - * Make an HTTP request. - * @alias module:http.request - * @param {Object} options - * @param {String} options.uri - * @param {Object?} options.query - * @param {Object?} options.body - * @param {Object?} options.json - * @param {Object?} options.form - * @param {String?} options.type - One of `"json"`, - * `"form"`, `"text"`, or `"bin"`. - * @param {String?} options.agent - User agent string. - * @param {Object?} [options.strictSSL=true] - Whether to accept bad certs. - * @param {Object?} options.method - HTTP method. - * @param {Object?} options.auth - * @param {String?} options.auth.username - * @param {String?} options.auth.password - * @param {String?} options.expect - Type to expect (see options.type). - * Error will be returned if the response is not of this type. - * @param {Number?} options.limit - Byte limit on response. - * @returns {Promise} - */ - -function request(options) { - if (typeof options === 'string') - options = { uri: options }; - - options.buffer = true; - - return new Promise((resolve, reject) => { - const req = new Request(options); - - req.on('error', err => reject(err)); - req.on('end', () => resolve(req)); - - req.start(); - req.end(); - }); -} - -request.stream = function stream(options) { - const req = new Request(options); - req.start(); - return req; -}; - -/* - * Helpers - */ - -function parseType(hdr) { - let type = hdr || ''; - type = type.split(';')[0]; - type = type.toLowerCase(); - type = type.trim(); - - switch (type) { - case 'text/x-json': - case 'application/json': - return 'json'; - case 'application/x-www-form-urlencoded': - return 'form'; - case 'text/html': - case 'application/xhtml+xml': - return 'html'; - case 'text/xml': - case 'application/xml': - return 'xml'; - case 'text/javascript': - case 'application/javascript': - return 'js'; - case 'text/css': - return 'css'; - case 'text/plain': - return 'txt'; - case 'application/octet-stream': - return 'bin'; - default: - return 'bin'; - } -} - -function getType(type) { - switch (type) { - case 'json': - return 'application/json; charset=utf-8'; - case 'form': - return 'application/x-www-form-urlencoded; charset=utf-8'; - case 'html': - return 'text/html; charset=utf-8'; - case 'xml': - return 'application/xml; charset=utf-8'; - case 'js': - return 'application/javascript; charset=utf-8'; - case 'css': - return 'text/css; charset=utf-8'; - case 'txt': - return 'text/plain; charset=utf-8'; - case 'bin': - return 'application/octet-stream'; - default: - throw new Error(`Unknown type: ${type}.`); - } -} - -function ensureRequires(ssl) { - if (!url) - url = require('url'); - - if (!qs) - qs = require('querystring'); - - if (!http) - http = require('http'); - - if (ssl && !https) - https = require('https'); - - if (!StringDecoder) - StringDecoder = require('string_decoder').StringDecoder; -} - -/* - * Expose - */ - -module.exports = request; diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 4f3e2fe7..4957f263 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -7,6 +7,7 @@ 'use strict'; const assert = require('assert'); +const bweb = require('bweb'); const util = require('../utils/util'); const co = require('../utils/co'); const digest = require('../crypto/digest'); @@ -31,1164 +32,1533 @@ const IP = require('../utils/ip'); const encoding = require('../utils/encoding'); const consensus = require('../protocol/consensus'); const Validator = require('../utils/validator'); -const RPCBase = require('./rpcbase'); const pkg = require('../pkg'); const Lock = require('../utils/lock'); -const RPCError = RPCBase.RPCError; -const errs = RPCBase.errors; -const MAGIC_STRING = RPCBase.MAGIC_STRING; +const RPCBase = bweb.RPC; +const RPCError = bweb.RPCError; +const errs = bweb.errors; +const MAGIC_STRING = 'Bitcoin Signed Message:\n'; -/** - * Bitcoin Core RPC - * @alias module:http.RPC - * @constructor - * @param {Node} node - */ +class RPC extends RPCBase { + /** + * Bitcoin Core RPC + * @alias module:http.RPC + * @constructor + * @param {Node} node + */ -function RPC(node) { - if (!(this instanceof RPC)) - return new RPC(node); + constructor(node) { + super(); - RPCBase.call(this); + assert(node, 'RPC requires a Node.'); - assert(node, 'RPC requires a Node.'); + this.node = node; + this.network = node.network; + this.workers = node.workers; + this.chain = node.chain; + this.mempool = node.mempool; + this.pool = node.pool; + this.fees = node.fees; + this.miner = node.miner; + this.logger = node.logger.context('rpc'); + this.locker = new Lock(); - this.node = node; - this.network = node.network; - this.workers = node.workers; - this.chain = node.chain; - this.mempool = node.mempool; - this.pool = node.pool; - this.fees = node.fees; - this.miner = node.miner; - this.logger = node.logger.context('rpc'); - this.locker = new Lock(); + this.mining = false; + this.procLimit = 0; + this.attempt = null; + this.lastActivity = 0; + this.boundChain = false; + this.nonce1 = 0; + this.nonce2 = 0; + this.merkleMap = new Map(); + this.pollers = []; - this.mining = false; - this.procLimit = 0; - this.attempt = null; - this.lastActivity = 0; - this.boundChain = false; - this.nonce1 = 0; - this.nonce2 = 0; - this.merkleMap = new Map(); - this.pollers = []; + this.init(); + } - this.init(); -} + init() { + this.add('stop', this.stop); + this.add('help', this.help); -Object.setPrototypeOf(RPC.prototype, RPCBase.prototype); + this.add('getblockchaininfo', this.getBlockchainInfo); + this.add('getbestblockhash', this.getBestBlockHash); + this.add('getblockcount', this.getBlockCount); + this.add('getblock', this.getBlock); + this.add('getblockbyheight', this.getBlockByHeight); + this.add('getblockhash', this.getBlockHash); + this.add('getblockheader', this.getBlockHeader); + this.add('getchaintips', this.getChainTips); + this.add('getdifficulty', this.getDifficulty); + this.add('getmempoolancestors', this.getMempoolAncestors); + this.add('getmempooldescendants', this.getMempoolDescendants); + this.add('getmempoolentry', this.getMempoolEntry); + this.add('getmempoolinfo', this.getMempoolInfo); + this.add('getrawmempool', this.getRawMempool); + this.add('gettxout', this.getTXOut); + this.add('gettxoutsetinfo', this.getTXOutSetInfo); + this.add('pruneblockchain', this.pruneBlockchain); + this.add('verifychain', this.verifyChain); -RPC.prototype.init = function init() { - this.add('stop', this.stop); - this.add('help', this.help); + this.add('invalidateblock', this.invalidateBlock); + this.add('reconsiderblock', this.reconsiderBlock); - this.add('getblockchaininfo', this.getBlockchainInfo); - this.add('getbestblockhash', this.getBestBlockHash); - this.add('getblockcount', this.getBlockCount); - this.add('getblock', this.getBlock); - this.add('getblockbyheight', this.getBlockByHeight); - this.add('getblockhash', this.getBlockHash); - this.add('getblockheader', this.getBlockHeader); - this.add('getchaintips', this.getChainTips); - this.add('getdifficulty', this.getDifficulty); - this.add('getmempoolancestors', this.getMempoolAncestors); - this.add('getmempooldescendants', this.getMempoolDescendants); - this.add('getmempoolentry', this.getMempoolEntry); - this.add('getmempoolinfo', this.getMempoolInfo); - this.add('getrawmempool', this.getRawMempool); - this.add('gettxout', this.getTXOut); - this.add('gettxoutsetinfo', this.getTXOutSetInfo); - this.add('pruneblockchain', this.pruneBlockchain); - this.add('verifychain', this.verifyChain); + this.add('getnetworkhashps', this.getNetworkHashPS); + this.add('getmininginfo', this.getMiningInfo); + this.add('prioritisetransaction', this.prioritiseTransaction); + this.add('getwork', this.getWork); + this.add('getworklp', this.getWorkLongpoll); + this.add('getblocktemplate', this.getBlockTemplate); + this.add('submitblock', this.submitBlock); + this.add('verifyblock', this.verifyBlock); - this.add('invalidateblock', this.invalidateBlock); - this.add('reconsiderblock', this.reconsiderBlock); + this.add('setgenerate', this.setGenerate); + this.add('getgenerate', this.getGenerate); + this.add('generate', this.generate); + this.add('generatetoaddress', this.generateToAddress); - this.add('getnetworkhashps', this.getNetworkHashPS); - this.add('getmininginfo', this.getMiningInfo); - this.add('prioritisetransaction', this.prioritiseTransaction); - this.add('getwork', this.getWork); - this.add('getworklp', this.getWorkLongpoll); - this.add('getblocktemplate', this.getBlockTemplate); - this.add('submitblock', this.submitBlock); - this.add('verifyblock', this.verifyBlock); + this.add('estimatefee', this.estimateFee); + this.add('estimatepriority', this.estimatePriority); + this.add('estimatesmartfee', this.estimateSmartFee); + this.add('estimatesmartpriority', this.estimateSmartPriority); - this.add('setgenerate', this.setGenerate); - this.add('getgenerate', this.getGenerate); - this.add('generate', this.generate); - this.add('generatetoaddress', this.generateToAddress); + this.add('getinfo', this.getInfo); + this.add('validateaddress', this.validateAddress); + this.add('createmultisig', this.createMultisig); + this.add('createwitnessaddress', this.createWitnessAddress); + this.add('verifymessage', this.verifyMessage); + this.add('signmessagewithprivkey', this.signMessageWithPrivkey); - this.add('estimatefee', this.estimateFee); - this.add('estimatepriority', this.estimatePriority); - this.add('estimatesmartfee', this.estimateSmartFee); - this.add('estimatesmartpriority', this.estimateSmartPriority); + this.add('setmocktime', this.setMockTime); - this.add('getinfo', this.getInfo); - this.add('validateaddress', this.validateAddress); - this.add('createmultisig', this.createMultisig); - this.add('createwitnessaddress', this.createWitnessAddress); - this.add('verifymessage', this.verifyMessage); - this.add('signmessagewithprivkey', this.signMessageWithPrivkey); + this.add('getconnectioncount', this.getConnectionCount); + this.add('ping', this.ping); + this.add('getpeerinfo', this.getPeerInfo); + this.add('addnode', this.addNode); + this.add('disconnectnode', this.disconnectNode); + this.add('getaddednodeinfo', this.getAddedNodeInfo); + this.add('getnettotals', this.getNetTotals); + this.add('getnetworkinfo', this.getNetworkInfo); + this.add('setban', this.setBan); + this.add('listbanned', this.listBanned); + this.add('clearbanned', this.clearBanned); - this.add('setmocktime', this.setMockTime); + this.add('getrawtransaction', this.getRawTransaction); + this.add('createrawtransaction', this.createRawTransaction); + this.add('decoderawtransaction', this.decodeRawTransaction); + this.add('decodescript', this.decodeScript); + this.add('sendrawtransaction', this.sendRawTransaction); + this.add('signrawtransaction', this.signRawTransaction); - this.add('getconnectioncount', this.getConnectionCount); - this.add('ping', this.ping); - this.add('getpeerinfo', this.getPeerInfo); - this.add('addnode', this.addNode); - this.add('disconnectnode', this.disconnectNode); - this.add('getaddednodeinfo', this.getAddedNodeInfo); - this.add('getnettotals', this.getNetTotals); - this.add('getnetworkinfo', this.getNetworkInfo); - this.add('setban', this.setBan); - this.add('listbanned', this.listBanned); - this.add('clearbanned', this.clearBanned); + this.add('gettxoutproof', this.getTXOutProof); + this.add('verifytxoutproof', this.verifyTXOutProof); - this.add('getrawtransaction', this.getRawTransaction); - this.add('createrawtransaction', this.createRawTransaction); - this.add('decoderawtransaction', this.decodeRawTransaction); - this.add('decodescript', this.decodeScript); - this.add('sendrawtransaction', this.sendRawTransaction); - this.add('signrawtransaction', this.signRawTransaction); + this.add('getmemoryinfo', this.getMemoryInfo); + this.add('setloglevel', this.setLogLevel); - this.add('gettxoutproof', this.getTXOutProof); - this.add('verifytxoutproof', this.verifyTXOutProof); - - this.add('getmemoryinfo', this.getMemoryInfo); - this.add('setloglevel', this.setLogLevel); - - this.on('error', (err) => { - this.logger.error('RPC internal error.'); - this.logger.error(err); - }); - - this.on('call', (cmd, query) => { - if (cmd.method !== 'getwork' - && cmd.method !== 'getblocktemplate' - && cmd.method !== 'getbestblockhash') { - this.logger.debug('Handling RPC call: %s.', cmd.method); - if (cmd.method !== 'submitblock' - && cmd.method !== 'getmemorypool') { - this.logger.debug(cmd.params); - } - } - - if (cmd.method === 'getwork') { - if (query.longpoll) - cmd.method = 'getworklp'; - } - }); -}; - -/* - * Overall control/query calls - */ - -RPC.prototype.getInfo = async function getInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getinfo'); - - return { - version: pkg.version, - protocolversion: this.pool.options.version, - walletversion: 0, - balance: 0, - blocks: this.chain.height, - timeoffset: this.network.time.offset, - connections: this.pool.peers.size(), - proxy: '', - difficulty: toDifficulty(this.chain.tip.bits), - testnet: this.network !== Network.main, - keypoololdest: 0, - keypoolsize: 0, - unlocked_until: 0, - paytxfee: Amount.btc(this.network.feeRate, true), - relayfee: Amount.btc(this.network.minRelay, true), - errors: '' - }; -}; - -RPC.prototype.help = async function help(args, _help) { - if (args.length === 0) - return 'Select a command.'; - - const json = { - method: args[0], - params: [] - }; - - return await this.execute(json, true); -}; - -RPC.prototype.stop = async function stop(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'stop'); - - this.node.close().catch((err) => { - setImmediate(() => { - throw err; + this.on('error', (err) => { + this.logger.error('RPC internal error.'); + this.logger.error(err); }); - }); - return 'Stopping.'; -}; + this.on('call', (cmd, query) => { + if (cmd.method !== 'getwork' + && cmd.method !== 'getblocktemplate' + && cmd.method !== 'getbestblockhash') { + this.logger.debug('Handling RPC call: %s.', cmd.method); + if (cmd.method !== 'submitblock' + && cmd.method !== 'getmemorypool') { + this.logger.debug(cmd.params); + } + } -/* - * P2P networking - */ - -RPC.prototype.getNetworkInfo = async function getNetworkInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getnetworkinfo'); - - const hosts = this.pool.hosts; - const locals = []; - - for (const local of hosts.local.values()) { - locals.push({ - address: local.addr.host, - port: local.addr.port, - score: local.score + if (cmd.method === 'getwork') { + if (query.longpoll) + cmd.method = 'getworklp'; + } }); } - return { - version: pkg.version, - subversion: this.pool.options.agent, - protocolversion: this.pool.options.version, - localservices: util.hex32(this.pool.options.services), - localrelay: !this.pool.options.noRelay, - timeoffset: this.network.time.offset, - networkactive: this.pool.connected, - connections: this.pool.peers.size(), - networks: [], - relayfee: Amount.btc(this.network.minRelay, true), - incrementalfee: 0, - localaddresses: locals, - warnings: '' - }; -}; + /* + * Overall control/query calls + */ -RPC.prototype.addNode = async function addNode(args, help) { - if (help || args.length !== 2) - throw new RPCError(errs.MISC_ERROR, 'addnode "node" "add|remove|onetry"'); + async getInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getinfo'); - const valid = new Validator(args); - const node = valid.str(0, ''); - const cmd = valid.str(1, ''); - - switch (cmd) { - case 'add': { - this.pool.hosts.addNode(node); - ; // fall through - } - case 'onetry': { - const addr = parseNetAddress(node, this.network); - - if (!this.pool.peers.get(addr.hostname)) { - const peer = this.pool.createOutbound(addr); - this.pool.peers.add(peer); - } - - break; - } - case 'remove': { - this.pool.hosts.removeNode(node); - break; - } + return { + version: pkg.version, + protocolversion: this.pool.options.version, + walletversion: 0, + balance: 0, + blocks: this.chain.height, + timeoffset: this.network.time.offset, + connections: this.pool.peers.size(), + proxy: '', + difficulty: toDifficulty(this.chain.tip.bits), + testnet: this.network !== Network.main, + keypoololdest: 0, + keypoolsize: 0, + unlocked_until: 0, + paytxfee: Amount.btc(this.network.feeRate, true), + relayfee: Amount.btc(this.network.minRelay, true), + errors: '' + }; } - return null; -}; + async help(args, _help) { + if (args.length === 0) + return 'Select a command.'; -RPC.prototype.disconnectNode = async function disconnectNode(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'disconnectnode "node"'); + const json = { + method: args[0], + params: [] + }; - const valid = new Validator(args); - const str = valid.str(0, ''); + return await this.execute(json, true); + } - const addr = parseIP(str, this.network); - const peer = this.pool.peers.get(addr.hostname); + async stop(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'stop'); - if (peer) - peer.destroy(); + this.node.close().catch((err) => { + setImmediate(() => { + throw err; + }); + }); - return null; -}; + return 'Stopping.'; + } -RPC.prototype.getAddedNodeInfo = async function getAddedNodeInfo(args, help) { - if (help || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'getaddednodeinfo ( "node" )'); + /* + * P2P networking + */ - const hosts = this.pool.hosts; - const valid = new Validator(args); - const addr = valid.str(0, ''); + async getNetworkInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getnetworkinfo'); - let target; - if (args.length === 1) - target = parseIP(addr, this.network); + const hosts = this.pool.hosts; + const locals = []; - const result = []; - - for (const node of hosts.nodes) { - if (target) { - if (node.host !== target.host) - continue; - - if (node.port !== target.port) - continue; + for (const local of hosts.local.values()) { + locals.push({ + address: local.addr.host, + port: local.addr.port, + score: local.score + }); } - const peer = this.pool.peers.get(node.hostname); + return { + version: pkg.version, + subversion: this.pool.options.agent, + protocolversion: this.pool.options.version, + localservices: util.hex32(this.pool.options.services), + localrelay: !this.pool.options.noRelay, + timeoffset: this.network.time.offset, + networkactive: this.pool.connected, + connections: this.pool.peers.size(), + networks: [], + relayfee: Amount.btc(this.network.minRelay, true), + incrementalfee: 0, + localaddresses: locals, + warnings: '' + }; + } + + async addNode(args, help) { + if (help || args.length !== 2) + throw new RPCError(errs.MISC_ERROR, 'addnode "node" "add|remove|onetry"'); + + const valid = new Validator(args); + const node = valid.str(0, ''); + const cmd = valid.str(1, ''); + + switch (cmd) { + case 'add': { + this.pool.hosts.addNode(node); + ; // fall through + } + case 'onetry': { + const addr = parseNetAddress(node, this.network); + + if (!this.pool.peers.get(addr.hostname)) { + const peer = this.pool.createOutbound(addr); + this.pool.peers.add(peer); + } + + break; + } + case 'remove': { + this.pool.hosts.removeNode(node); + break; + } + } + + return null; + } + + async disconnectNode(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'disconnectnode "node"'); + + const valid = new Validator(args); + const str = valid.str(0, ''); + + const addr = parseIP(str, this.network); + const peer = this.pool.peers.get(addr.hostname); + + if (peer) + peer.destroy(); + + return null; + } + + async getAddedNodeInfo(args, help) { + if (help || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'getaddednodeinfo ( "node" )'); + + const hosts = this.pool.hosts; + const valid = new Validator(args); + const addr = valid.str(0, ''); + + let target; + if (args.length === 1) + target = parseIP(addr, this.network); + + const result = []; + + for (const node of hosts.nodes) { + if (target) { + if (node.host !== target.host) + continue; + + if (node.port !== target.port) + continue; + } + + const peer = this.pool.peers.get(node.hostname); + + if (!peer || !peer.connected) { + result.push({ + addednode: node.hostname, + connected: false, + addresses: [] + }); + continue; + } - if (!peer || !peer.connected) { result.push({ addednode: node.hostname, - connected: false, - addresses: [] + connected: peer.connected, + addresses: [ + { + address: peer.hostname(), + connected: peer.outbound + ? 'outbound' + : 'inbound' + } + ] }); - continue; } - result.push({ - addednode: node.hostname, - connected: peer.connected, - addresses: [ - { - address: peer.hostname(), - connected: peer.outbound - ? 'outbound' - : 'inbound' - } - ] - }); - } - - if (target && result.length === 0) { - throw new RPCError(errs.CLIENT_NODE_NOT_ADDED, - 'Node has not been added.'); - } - - return result; -}; - -RPC.prototype.getConnectionCount = async function getConnectionCount(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getconnectioncount'); - - return this.pool.peers.size(); -}; - -RPC.prototype.getNetTotals = async function getNetTotals(args, help) { - let sent = 0; - let recv = 0; - - if (help || args.length > 0) - throw new RPCError(errs.MISC_ERROR, 'getnettotals'); - - for (let peer = this.pool.peers.head(); peer; peer = peer.next) { - sent += peer.socket.bytesWritten; - recv += peer.socket.bytesRead; - } - - return { - totalbytesrecv: recv, - totalbytessent: sent, - timemillis: util.ms() - }; -}; - -RPC.prototype.getPeerInfo = async function getPeerInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getpeerinfo'); - - const peers = []; - - for (let peer = this.pool.peers.head(); peer; peer = peer.next) { - let offset = this.network.time.known.get(peer.hostname()); - const hashes = []; - - if (offset == null) - offset = 0; - - for (const hash in peer.blockMap.keys()) { - const str = util.revHex(hash); - hashes.push(str); + if (target && result.length === 0) { + throw new RPCError(errs.CLIENT_NODE_NOT_ADDED, + 'Node has not been added.'); } - peers.push({ - id: peer.id, - addr: peer.hostname(), - addrlocal: !peer.local.isNull() - ? peer.local.hostname - : undefined, - services: util.hex32(peer.services), - relaytxes: !peer.noRelay, - lastsend: peer.lastSend / 1000 | 0, - lastrecv: peer.lastRecv / 1000 | 0, - bytessent: peer.socket.bytesWritten, - bytesrecv: peer.socket.bytesRead, - conntime: peer.time !== 0 ? (util.ms() - peer.time) / 1000 | 0 : 0, - timeoffset: offset, - pingtime: peer.lastPong !== -1 - ? (peer.lastPong - peer.lastPing) / 1000 - : -1, - minping: peer.minPing !== -1 ? peer.minPing / 1000 : -1, - version: peer.version, - subver: peer.agent, - inbound: !peer.outbound, - startingheight: peer.height, - besthash: peer.bestHash ? util.revHex(peer.bestHash) : null, - bestheight: peer.bestHeight, - banscore: peer.banScore, - inflight: hashes, - whitelisted: false - }); + return result; } - return peers; -}; + async getConnectionCount(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getconnectioncount'); -RPC.prototype.ping = async function ping(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'ping'); - - for (let peer = this.pool.peers.head(); peer; peer = peer.next) - peer.sendPing(); - - return null; -}; - -RPC.prototype.setBan = async function setBan(args, help) { - const valid = new Validator(args); - const str = valid.str(0, ''); - const action = valid.str(1, ''); - - if (help - || args.length < 2 - || (action !== 'add' && action !== 'remove')) { - throw new RPCError(errs.MISC_ERROR, - 'setban "ip(/netmask)" "add|remove" (bantime) (absolute)'); + return this.pool.peers.size(); } - const addr = parseNetAddress(str, this.network); + async getNetTotals(args, help) { + let sent = 0; + let recv = 0; - switch (action) { - case 'add': - this.pool.ban(addr); - break; - case 'remove': - this.pool.unban(addr); - break; + if (help || args.length > 0) + throw new RPCError(errs.MISC_ERROR, 'getnettotals'); + + for (let peer = this.pool.peers.head(); peer; peer = peer.next) { + sent += peer.socket.bytesWritten; + recv += peer.socket.bytesRead; + } + + return { + totalbytesrecv: recv, + totalbytessent: sent, + timemillis: util.ms() + }; } - return null; -}; + async getPeerInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getpeerinfo'); -RPC.prototype.listBanned = async function listBanned(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'listbanned'); + const peers = []; - const banned = []; + for (let peer = this.pool.peers.head(); peer; peer = peer.next) { + let offset = this.network.time.known.get(peer.hostname()); + const hashes = []; - for (const [host, time] of this.pool.hosts.banned) { - banned.push({ - address: host, - banned_until: time + this.pool.options.banTime, - ban_created: time, - ban_reason: '' - }); + if (offset == null) + offset = 0; + + for (const hash in peer.blockMap.keys()) { + const str = util.revHex(hash); + hashes.push(str); + } + + peers.push({ + id: peer.id, + addr: peer.hostname(), + addrlocal: !peer.local.isNull() + ? peer.local.hostname + : undefined, + services: util.hex32(peer.services), + relaytxes: !peer.noRelay, + lastsend: peer.lastSend / 1000 | 0, + lastrecv: peer.lastRecv / 1000 | 0, + bytessent: peer.socket.bytesWritten, + bytesrecv: peer.socket.bytesRead, + conntime: peer.time !== 0 ? (util.ms() - peer.time) / 1000 | 0 : 0, + timeoffset: offset, + pingtime: peer.lastPong !== -1 + ? (peer.lastPong - peer.lastPing) / 1000 + : -1, + minping: peer.minPing !== -1 ? peer.minPing / 1000 : -1, + version: peer.version, + subver: peer.agent, + inbound: !peer.outbound, + startingheight: peer.height, + besthash: peer.bestHash ? util.revHex(peer.bestHash) : null, + bestheight: peer.bestHeight, + banscore: peer.banScore, + inflight: hashes, + whitelisted: false + }); + } + + return peers; } - return banned; -}; + async ping(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'ping'); -RPC.prototype.clearBanned = async function clearBanned(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'clearbanned'); + for (let peer = this.pool.peers.head(); peer; peer = peer.next) + peer.sendPing(); - this.pool.hosts.clearBanned(); - - return null; -}; - -/* Block chain and UTXO */ -RPC.prototype.getBlockchainInfo = async function getBlockchainInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getblockchaininfo'); - - return { - chain: this.network.type !== 'testnet' - ? this.network.type - : 'test', - blocks: this.chain.height, - headers: this.chain.height, - bestblockhash: this.chain.tip.rhash(), - difficulty: toDifficulty(this.chain.tip.bits), - mediantime: await this.chain.getMedianTime(this.chain.tip), - verificationprogress: this.chain.getProgress(), - chainwork: this.chain.tip.chainwork.toString('hex', 64), - pruned: this.chain.options.prune, - softforks: this.getSoftforks(), - bip9_softforks: await this.getBIP9Softforks(), - pruneheight: this.chain.options.prune - ? Math.max(0, this.chain.height - this.network.block.keepBlocks) - : null - }; -}; - -RPC.prototype.getBestBlockHash = async function getBestBlockHash(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getbestblockhash'); - - return this.chain.tip.rhash(); -}; - -RPC.prototype.getBlockCount = async function getBlockCount(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getblockcount'); - - return this.chain.tip.height; -}; - -RPC.prototype.getBlock = async function getBlock(args, help) { - if (help || args.length < 1 || args.length > 3) - throw new RPCError(errs.MISC_ERROR, 'getblock "hash" ( verbose )'); - - const valid = new Validator(args); - const hash = valid.rhash(0); - const verbose = valid.bool(1, true); - const details = valid.bool(2, false); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); - - const entry = await this.chain.getEntry(hash); - - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Block not found'); - - const block = await this.chain.getBlock(entry.hash); - - if (!block) { - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)'); - - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Block not available (pruned data)'); - - throw new RPCError(errs.MISC_ERROR, 'Can\'t read block from disk'); + return null; } - if (!verbose) - return block.toRaw().toString('hex'); + async setBan(args, help) { + const valid = new Validator(args); + const str = valid.str(0, ''); + const action = valid.str(1, ''); - return await this.blockToJSON(entry, block, details); -}; + if (help + || args.length < 2 + || (action !== 'add' && action !== 'remove')) { + throw new RPCError(errs.MISC_ERROR, + 'setban "ip(/netmask)" "add|remove" (bantime) (absolute)'); + } -RPC.prototype.getBlockByHeight = async function getBlockByHeight(args, help) { - if (help || args.length < 1 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'getblockbyheight "height" ( verbose )'); + const addr = parseNetAddress(str, this.network); + + switch (action) { + case 'add': + this.pool.ban(addr); + break; + case 'remove': + this.pool.unban(addr); + break; + } + + return null; } - const valid = new Validator(args); - const height = valid.u32(0, -1); - const verbose = valid.bool(1, true); - const details = valid.bool(2, false); + async listBanned(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'listbanned'); - if (height === -1) - throw new RPCError(errs.TYPE_ERROR, 'Invalid block height.'); + const banned = []; - const entry = await this.chain.getEntry(height); + for (const [host, time] of this.pool.hosts.banned) { + banned.push({ + address: host, + banned_until: time + this.pool.options.banTime, + ban_created: time, + ban_reason: '' + }); + } - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Block not found'); - - const block = await this.chain.getBlock(entry.hash); - - if (!block) { - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)'); - - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Block not available (pruned data)'); - - throw new RPCError(errs.DATABASE_ERROR, 'Can\'t read block from disk'); + return banned; } - if (!verbose) - return block.toRaw().toString('hex'); + async clearBanned(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'clearbanned'); - return await this.blockToJSON(entry, block, details); -}; + this.pool.hosts.clearBanned(); -RPC.prototype.getBlockHash = async function getBlockHash(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getblockhash index'); + return null; + } - const valid = new Validator(args); - const height = valid.u32(0); + /* Block chain and UTXO */ + async getBlockchainInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getblockchaininfo'); - if (height == null || height > this.chain.height) - throw new RPCError(errs.INVALID_PARAMETER, 'Block height out of range.'); + return { + chain: this.network.type !== 'testnet' + ? this.network.type + : 'test', + blocks: this.chain.height, + headers: this.chain.height, + bestblockhash: this.chain.tip.rhash(), + difficulty: toDifficulty(this.chain.tip.bits), + mediantime: await this.chain.getMedianTime(this.chain.tip), + verificationprogress: this.chain.getProgress(), + chainwork: this.chain.tip.chainwork.toString('hex', 64), + pruned: this.chain.options.prune, + softforks: this.getSoftforks(), + bip9_softforks: await this.getBIP9Softforks(), + pruneheight: this.chain.options.prune + ? Math.max(0, this.chain.height - this.network.block.keepBlocks) + : null + }; + } - const hash = await this.chain.getHash(height); + async getBestBlockHash(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getbestblockhash'); - if (!hash) - throw new RPCError(errs.MISC_ERROR, 'Not found.'); + return this.chain.tip.rhash(); + } - return util.revHex(hash); -}; + async getBlockCount(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getblockcount'); -RPC.prototype.getBlockHeader = async function getBlockHeader(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'getblockheader "hash" ( verbose )'); + return this.chain.tip.height; + } - const valid = new Validator(args); - const hash = valid.rhash(0); - const verbose = valid.bool(1, true); + async getBlock(args, help) { + if (help || args.length < 1 || args.length > 3) + throw new RPCError(errs.MISC_ERROR, 'getblock "hash" ( verbose )'); - if (!hash) - throw new RPCError(errs.MISC_ERROR, 'Invalid block hash.'); + const valid = new Validator(args); + const hash = valid.rhash(0); + const verbose = valid.bool(1, true); + const details = valid.bool(2, false); - const entry = await this.chain.getEntry(hash); + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Block not found'); - - if (!verbose) - return entry.toRaw().toString('hex', 0, 80); - - return await this.headerToJSON(entry); -}; - -RPC.prototype.getChainTips = async function getChainTips(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getchaintips'); - - const tips = await this.chain.getTips(); - const result = []; - - for (const hash of tips) { const entry = await this.chain.getEntry(hash); - assert(entry); + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Block not found'); - const fork = await this.findFork(entry); - const main = await this.chain.isMainChain(entry); + const block = await this.chain.getBlock(entry.hash); - result.push({ - height: entry.height, - hash: entry.rhash(), - branchlen: entry.height - fork.height, - status: main ? 'active' : 'valid-headers' - }); + if (!block) { + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)'); + + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Block not available (pruned data)'); + + throw new RPCError(errs.MISC_ERROR, 'Can\'t read block from disk'); + } + + if (!verbose) + return block.toRaw().toString('hex'); + + return await this.blockToJSON(entry, block, details); } - return result; -}; + async getBlockByHeight(args, help) { + if (help || args.length < 1 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'getblockbyheight "height" ( verbose )'); + } -RPC.prototype.getDifficulty = async function getDifficulty(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getdifficulty'); + const valid = new Validator(args); + const height = valid.u32(0, -1); + const verbose = valid.bool(1, true); + const details = valid.bool(2, false); - return toDifficulty(this.chain.tip.bits); -}; + if (height === -1) + throw new RPCError(errs.TYPE_ERROR, 'Invalid block height.'); -RPC.prototype.getMempoolInfo = async function getMempoolInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getmempoolinfo'); + const entry = await this.chain.getEntry(height); - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Block not found'); - return { - size: this.mempool.map.size, - bytes: this.mempool.getSize(), - usage: this.mempool.getSize(), - maxmempool: this.mempool.options.maxSize, - mempoolminfee: Amount.btc(this.mempool.options.minRelay, true) - }; -}; + const block = await this.chain.getBlock(entry.hash); -RPC.prototype.getMempoolAncestors = async function getMempoolAncestors(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'getmempoolancestors txid (verbose)'); + if (!block) { + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)'); - const valid = new Validator(args); - const hash = valid.rhash(0); - const verbose = valid.bool(1, false); + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Block not available (pruned data)'); - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + throw new RPCError(errs.DATABASE_ERROR, 'Can\'t read block from disk'); + } - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + if (!verbose) + return block.toRaw().toString('hex'); - const entry = this.mempool.getEntry(hash); - - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - - const entries = this.mempool.getAncestors(entry); - const out = []; - - if (verbose) { - for (const entry of entries) - out.push(this.entryToJSON(entry)); - } else { - for (const entry of entries) - out.push(entry.txid()); + return await this.blockToJSON(entry, block, details); } - return out; -}; + async getBlockHash(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'getblockhash index'); -RPC.prototype.getMempoolDescendants = async function getMempoolDescendants(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'getmempooldescendants txid (verbose)'); + const valid = new Validator(args); + const height = valid.u32(0); - const valid = new Validator(args); - const hash = valid.rhash(0); - const verbose = valid.bool(1, false); + if (height == null || height > this.chain.height) + throw new RPCError(errs.INVALID_PARAMETER, 'Block height out of range.'); - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + const hash = await this.chain.getHash(height); - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + if (!hash) + throw new RPCError(errs.MISC_ERROR, 'Not found.'); - const entry = this.mempool.getEntry(hash); - - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - - const entries = this.mempool.getDescendants(entry); - const out = []; - - if (verbose) { - for (const entry of entries) - out.push(this.entryToJSON(entry)); - } else { - for (const entry of entries) - out.push(entry.txid()); + return util.revHex(hash); } - return out; -}; + async getBlockHeader(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getblockheader "hash" ( verbose )'); -RPC.prototype.getMempoolEntry = async function getMempoolEntry(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getmempoolentry txid'); + const valid = new Validator(args); + const hash = valid.rhash(0); + const verbose = valid.bool(1, true); - const valid = new Validator(args); - const hash = valid.rhash(0); + if (!hash) + throw new RPCError(errs.MISC_ERROR, 'Invalid block hash.'); - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + const entry = await this.chain.getEntry(hash); - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Block not found'); - const entry = this.mempool.getEntry(hash); + if (!verbose) + return entry.toRaw().toString('hex', 0, 80); - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - - return this.entryToJSON(entry); -}; - -RPC.prototype.getRawMempool = async function getRawMempool(args, help) { - if (help || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'getrawmempool ( verbose )'); - - const valid = new Validator(args); - const verbose = valid.bool(0, false); - - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); - - if (verbose) { - const out = {}; - - for (const entry of this.mempool.map.values()) - out[entry.txid()] = this.entryToJSON(entry); - - return out; + return await this.headerToJSON(entry); } - const hashes = this.mempool.getSnapshot(); + async getChainTips(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getchaintips'); - return hashes.map(util.revHex); -}; + const tips = await this.chain.getTips(); + const result = []; -RPC.prototype.getTXOut = async function getTXOut(args, help) { - if (help || args.length < 2 || args.length > 3) - throw new RPCError(errs.MISC_ERROR, 'gettxout "txid" n ( includemempool )'); + for (const hash of tips) { + const entry = await this.chain.getEntry(hash); - const valid = new Validator(args); - const hash = valid.rhash(0); - const index = valid.u32(1); - const mempool = valid.bool(2, true); + assert(entry); - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.'); + const fork = await this.findFork(entry); + const main = await this.chain.isMainChain(entry); - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.'); + result.push({ + height: entry.height, + hash: entry.rhash(), + branchlen: entry.height - fork.height, + status: main ? 'active' : 'valid-headers' + }); + } - if (!hash || index == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.'); + return result; + } + + async getDifficulty(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getdifficulty'); + + return toDifficulty(this.chain.tip.bits); + } + + async getMempoolInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getmempoolinfo'); - let coin; - if (mempool) { if (!this.mempool) throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); - coin = this.mempool.getCoin(hash, index); + + return { + size: this.mempool.map.size, + bytes: this.mempool.getSize(), + usage: this.mempool.getSize(), + maxmempool: this.mempool.options.maxSize, + mempoolminfee: Amount.btc(this.mempool.options.minRelay, true) + }; } - if (!coin) - coin = await this.chain.getCoin(hash, index); + async getMempoolAncestors(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getmempoolancestors txid (verbose)'); - if (!coin) - return null; + const valid = new Validator(args); + const hash = valid.rhash(0); + const verbose = valid.bool(1, false); - return { - bestblock: this.chain.tip.rhash(), - confirmations: coin.getDepth(this.chain.height), - value: Amount.btc(coin.value, true), - scriptPubKey: this.scriptToJSON(coin.script, true), - version: coin.version, - coinbase: coin.coinbase - }; -}; - -RPC.prototype.getTXOutProof = async function getTXOutProof(args, help) { - if (help || (args.length !== 1 && args.length !== 2)) { - throw new RPCError(errs.MISC_ERROR, - 'gettxoutproof ["txid",...] ( blockhash )'); - } - - const valid = new Validator(args); - const txids = valid.array(0); - const hash = valid.rhash(1); - - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.'); - - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.'); - - if (!txids || txids.length === 0) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid TXIDs.'); - - const items = new Validator(txids); - const set = new Set(); - const hashes = []; - - let last = null; - - for (let i = 0; i < txids.length; i++) { - const hash = items.rhash(i); + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); if (!hash) throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); - if (set.has(hash)) - throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate txid.'); + const entry = this.mempool.getEntry(hash); - set.add(hash); - hashes.push(hash); + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - last = hash; - } + const entries = this.mempool.getAncestors(entry); + const out = []; - let block = null; - - if (hash) { - block = await this.chain.getBlock(hash); - } else if (this.chain.options.indexTX) { - const tx = await this.chain.getMeta(last); - if (tx) - block = await this.chain.getBlock(tx.block); - } else { - const coin = await this.chain.getCoin(last, 0); - if (coin) - block = await this.chain.getBlock(coin.height); - } - - if (!block) - throw new RPCError(errs.MISC_ERROR, 'Block not found.'); - - for (const hash of hashes) { - if (!block.hasTX(hash)) { - throw new RPCError(errs.VERIFY_ERROR, - 'Block does not contain all txids.'); + if (verbose) { + for (const entry of entries) + out.push(this.entryToJSON(entry)); + } else { + for (const entry of entries) + out.push(entry.txid()); } + + return out; } - block = MerkleBlock.fromHashes(block, hashes); + async getMempoolDescendants(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getmempooldescendants txid (verbose)'); - return block.toRaw().toString('hex'); -}; + const valid = new Validator(args); + const hash = valid.rhash(0); + const verbose = valid.bool(1, false); -RPC.prototype.verifyTXOutProof = async function verifyTXOutProof(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'verifytxoutproof "proof"'); + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); - const valid = new Validator(args); - const data = valid.buf(0); + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); + const entry = this.mempool.getEntry(hash); - const block = MerkleBlock.fromRaw(data); + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - if (!block.verify()) - return []; + const entries = this.mempool.getDescendants(entry); + const out = []; - const entry = await this.chain.getEntry(block.hash('hex')); - - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Block not found in chain.'); - - const tree = block.getTree(); - const out = []; - - for (const hash of tree.matches) - out.push(util.revHex(hash.toString('hex'))); - - return out; -}; - -RPC.prototype.getTXOutSetInfo = async function getTXOutSetInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'gettxoutsetinfo'); - - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Chainstate not available (SPV mode).'); - - return { - height: this.chain.height, - bestblock: this.chain.tip.rhash(), - transactions: this.chain.db.state.tx, - txouts: this.chain.db.state.coin, - bytes_serialized: 0, - hash_serialized: 0, - total_amount: Amount.btc(this.chain.db.state.value, true) - }; -}; - -RPC.prototype.pruneBlockchain = async function pruneBlockchain(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'pruneblockchain'); - - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Cannot prune chain in SPV mode.'); - - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Chain is already pruned.'); - - if (this.chain.height < this.network.block.pruneAfterHeight) - throw new RPCError(errs.MISC_ERROR, 'Chain is too short for pruning.'); - - try { - await this.chain.prune(); - } catch (e) { - throw new RPCError(errs.DATABASE_ERROR, e.message); - } -}; - -RPC.prototype.verifyChain = async function verifyChain(args, help) { - if (help || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'verifychain ( checklevel numblocks )'); - - const valid = new Validator(args); - const level = valid.u32(0); - const blocks = valid.u32(1); - - if (level == null || blocks == null) - throw new RPCError(errs.TYPE_ERROR, 'Missing parameters.'); - - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain in SPV mode.'); - - if (this.chain.options.prune) - throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain when pruned.'); - - return null; -}; - -/* - * Mining - */ - -RPC.prototype.submitWork = async function submitWork(data) { - const unlock = await this.locker.lock(); - try { - return await this._submitWork(data); - } finally { - unlock(); - } -}; - -RPC.prototype._submitWork = async function _submitWork(data) { - const attempt = this.attempt; - - if (!attempt) - return false; - - if (data.length !== 128) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid work size.'); - - const raw = data.slice(0, 80); - swap32(raw); - - const header = Headers.fromHead(raw); - - if (header.prevBlock !== attempt.prevBlock - || header.bits !== attempt.bits) { - return false; - } - - if (!header.verify()) - return false; - - const nonces = this.merkleMap.get(header.merkleRoot); - - if (!nonces) - return false; - - const [n1, n2] = nonces; - const nonce = header.nonce; - const time = header.time; - - const proof = attempt.getProof(n1, n2, time, nonce); - - if (!proof.verify(attempt.target)) - return false; - - const block = attempt.commit(proof); - - let entry; - try { - entry = await this.chain.add(block); - } catch (err) { - if (err.type === 'VerifyError') { - this.logger.warning('RPC block rejected: %s (%s).', - block.rhash(), err.reason); - return false; + if (verbose) { + for (const entry of entries) + out.push(this.entryToJSON(entry)); + } else { + for (const entry of entries) + out.push(entry.txid()); } - throw err; + + return out; } - if (!entry) { - this.logger.warning('RPC block rejected: %s (bad-prevblk).', - block.rhash()); - return false; + async getMempoolEntry(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'getmempoolentry txid'); + + const valid = new Validator(args); + const hash = valid.rhash(0); + + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + + const entry = this.mempool.getEntry(hash); + + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); + + return this.entryToJSON(entry); } - return true; -}; + async getRawMempool(args, help) { + if (help || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'getrawmempool ( verbose )'); -RPC.prototype.createWork = async function createWork(data) { - const unlock = await this.locker.lock(); - try { - return await this._createWork(data); - } finally { - unlock(); + const valid = new Validator(args); + const verbose = valid.bool(0, false); + + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + + if (verbose) { + const out = {}; + + for (const entry of this.mempool.map.values()) + out[entry.txid()] = this.entryToJSON(entry); + + return out; + } + + const hashes = this.mempool.getSnapshot(); + + return hashes.map(util.revHex); } -}; -RPC.prototype._createWork = async function _createWork() { - const attempt = await this.updateWork(); - const n1 = this.nonce1; - const n2 = this.nonce2; - const time = attempt.time; + async getTXOut(args, help) { + if (help || args.length < 2 || args.length > 3) + throw new RPCError(errs.MISC_ERROR, 'gettxout "txid" n ( includemempool )'); - const data = Buffer.allocUnsafe(128); - data.fill(0); + const valid = new Validator(args); + const hash = valid.rhash(0); + const index = valid.u32(1); + const mempool = valid.bool(2, true); - const root = attempt.getRoot(n1, n2); - const head = attempt.getHeader(root, time, 0); + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.'); - head.copy(data, 0); + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.'); - data[80] = 0x80; - data.writeUInt32BE(80 * 8, data.length - 4, true); + if (!hash || index == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.'); - swap32(data); + let coin; + if (mempool) { + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + coin = this.mempool.getCoin(hash, index); + } - return { - data: data.toString('hex'), - target: attempt.target.toString('hex'), - height: attempt.height - }; -}; + if (!coin) + coin = await this.chain.getCoin(hash, index); -RPC.prototype.getWorkLongpoll = async function getWorkLongpoll(args, help) { - await this.longpoll(); - return await this.createWork(); -}; + if (!coin) + return null; -RPC.prototype.getWork = async function getWork(args, help) { - if (args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'getwork ( "data" )'); + return { + bestblock: this.chain.tip.rhash(), + confirmations: coin.getDepth(this.chain.height), + value: Amount.btc(coin.value, true), + scriptPubKey: this.scriptToJSON(coin.script, true), + version: coin.version, + coinbase: coin.coinbase + }; + } + + async getTXOutProof(args, help) { + if (help || (args.length !== 1 && args.length !== 2)) { + throw new RPCError(errs.MISC_ERROR, + 'gettxoutproof ["txid",...] ( blockhash )'); + } + + const valid = new Validator(args); + const txids = valid.array(0); + const hash = valid.rhash(1); + + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.'); + + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.'); + + if (!txids || txids.length === 0) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid TXIDs.'); + + const items = new Validator(txids); + const set = new Set(); + const hashes = []; + + let last = null; + + for (let i = 0; i < txids.length; i++) { + const hash = items.rhash(i); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + + if (set.has(hash)) + throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate txid.'); + + set.add(hash); + hashes.push(hash); + + last = hash; + } + + let block = null; + + if (hash) { + block = await this.chain.getBlock(hash); + } else if (this.chain.options.indexTX) { + const tx = await this.chain.getMeta(last); + if (tx) + block = await this.chain.getBlock(tx.block); + } else { + const coin = await this.chain.getCoin(last, 0); + if (coin) + block = await this.chain.getBlock(coin.height); + } + + if (!block) + throw new RPCError(errs.MISC_ERROR, 'Block not found.'); + + for (const hash of hashes) { + if (!block.hasTX(hash)) { + throw new RPCError(errs.VERIFY_ERROR, + 'Block does not contain all txids.'); + } + } + + block = MerkleBlock.fromHashes(block, hashes); + + return block.toRaw().toString('hex'); + } + + async verifyTXOutProof(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'verifytxoutproof "proof"'); - if (args.length === 1) { const valid = new Validator(args); const data = valid.buf(0); if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid work data.'); + throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); - return await this.submitWork(data); + const block = MerkleBlock.fromRaw(data); + + if (!block.verify()) + return []; + + const entry = await this.chain.getEntry(block.hash('hex')); + + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Block not found in chain.'); + + const tree = block.getTree(); + const out = []; + + for (const hash of tree.matches) + out.push(util.revHex(hash.toString('hex'))); + + return out; } - return await this.createWork(); -}; + async getTXOutSetInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'gettxoutsetinfo'); -RPC.prototype.submitBlock = async function submitBlock(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'submitblock "hexdata" ( "jsonparametersobject" )'); + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Chainstate not available (SPV mode).'); + + return { + height: this.chain.height, + bestblock: this.chain.tip.rhash(), + transactions: this.chain.db.state.tx, + txouts: this.chain.db.state.coin, + bytes_serialized: 0, + hash_serialized: 0, + total_amount: Amount.btc(this.chain.db.state.value, true) + }; } - const valid = new Validator(args); - const data = valid.buf(0); + async pruneBlockchain(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'pruneblockchain'); - const block = Block.fromRaw(data); + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot prune chain in SPV mode.'); - return await this.addBlock(block); -}; + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Chain is already pruned.'); -RPC.prototype.getBlockTemplate = async function getBlockTemplate(args, help) { - if (help || args.length > 1) { - throw new RPCError(errs.MISC_ERROR, - 'getblocktemplate ( "jsonrequestobject" )'); + if (this.chain.height < this.network.block.pruneAfterHeight) + throw new RPCError(errs.MISC_ERROR, 'Chain is too short for pruning.'); + + try { + await this.chain.prune(); + } catch (e) { + throw new RPCError(errs.DATABASE_ERROR, e.message); + } } - const validator = new Validator(args); - const options = validator.obj(0, {}); - const valid = new Validator(options); - const mode = valid.str('mode', 'template'); + async verifyChain(args, help) { + if (help || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'verifychain ( checklevel numblocks )'); - if (mode !== 'template' && mode !== 'proposal') - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid mode.'); + const valid = new Validator(args); + const level = valid.u32(0); + const blocks = valid.u32(1); - if (mode === 'proposal') { - const data = valid.buf('data'); + if (level == null || blocks == null) + throw new RPCError(errs.TYPE_ERROR, 'Missing parameters.'); - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Missing data parameter.'); + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain in SPV mode.'); + + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain when pruned.'); + + return null; + } + + /* + * Mining + */ + + async submitWork(data) { + const unlock = await this.locker.lock(); + try { + return await this._submitWork(data); + } finally { + unlock(); + } + } + + async _submitWork(data) { + const attempt = this.attempt; + + if (!attempt) + return false; + + if (data.length !== 128) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid work size.'); + + const raw = data.slice(0, 80); + swap32(raw); + + const header = Headers.fromHead(raw); + + if (header.prevBlock !== attempt.prevBlock + || header.bits !== attempt.bits) { + return false; + } + + if (!header.verify()) + return false; + + const nonces = this.merkleMap.get(header.merkleRoot); + + if (!nonces) + return false; + + const [n1, n2] = nonces; + const nonce = header.nonce; + const time = header.time; + + const proof = attempt.getProof(n1, n2, time, nonce); + + if (!proof.verify(attempt.target)) + return false; + + const block = attempt.commit(proof); + + let entry; + try { + entry = await this.chain.add(block); + } catch (err) { + if (err.type === 'VerifyError') { + this.logger.warning('RPC block rejected: %s (%s).', + block.rhash(), err.reason); + return false; + } + throw err; + } + + if (!entry) { + this.logger.warning('RPC block rejected: %s (bad-prevblk).', + block.rhash()); + return false; + } + + return true; + } + + async createWork(data) { + const unlock = await this.locker.lock(); + try { + return await this._createWork(data); + } finally { + unlock(); + } + } + + async _createWork() { + const attempt = await this.updateWork(); + const n1 = this.nonce1; + const n2 = this.nonce2; + const time = attempt.time; + + const data = Buffer.allocUnsafe(128); + data.fill(0); + + const root = attempt.getRoot(n1, n2); + const head = attempt.getHeader(root, time, 0); + + head.copy(data, 0); + + data[80] = 0x80; + data.writeUInt32BE(80 * 8, data.length - 4, true); + + swap32(data); + + return { + data: data.toString('hex'), + target: attempt.target.toString('hex'), + height: attempt.height + }; + } + + async getWorkLongpoll(args, help) { + await this.longpoll(); + return await this.createWork(); + } + + async getWork(args, help) { + if (args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'getwork ( "data" )'); + + if (args.length === 1) { + const valid = new Validator(args); + const data = valid.buf(0); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid work data.'); + + return await this.submitWork(data); + } + + return await this.createWork(); + } + + async submitBlock(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'submitblock "hexdata" ( "jsonparametersobject" )'); + } + + const valid = new Validator(args); + const data = valid.buf(0); const block = Block.fromRaw(data); - if (block.prevBlock !== this.chain.tip.hash) - return 'inconclusive-not-best-prevblk'; + return await this.addBlock(block); + } + + async getBlockTemplate(args, help) { + if (help || args.length > 1) { + throw new RPCError(errs.MISC_ERROR, + 'getblocktemplate ( "jsonrequestobject" )'); + } + + const validator = new Validator(args); + const options = validator.obj(0, {}); + const valid = new Validator(options); + const mode = valid.str('mode', 'template'); + + if (mode !== 'template' && mode !== 'proposal') + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid mode.'); + + if (mode === 'proposal') { + const data = valid.buf('data'); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Missing data parameter.'); + + const block = Block.fromRaw(data); + + if (block.prevBlock !== this.chain.tip.hash) + return 'inconclusive-not-best-prevblk'; + + try { + await this.chain.verifyBlock(block); + } catch (e) { + if (e.type === 'VerifyError') + return e.reason; + throw e; + } + + return null; + } + + let maxVersion = valid.u32('maxversion', -1); + let rules = valid.array('rules'); + + if (rules) + maxVersion = -1; + + const capabilities = valid.array('capabilities'); + let coinbase = false; + + if (capabilities) { + let txnCap = false; + let valueCap = false; + + for (const capability of capabilities) { + if (typeof capability !== 'string') + throw new RPCError(errs.TYPE_ERROR, 'Invalid capability.'); + + switch (capability) { + case 'coinbasetxn': + txnCap = true; + break; + case 'coinbasevalue': + // Prefer value if they support it. + valueCap = true; + break; + } + } + + // BIP22 states that we can't have coinbasetxn + // _and_ coinbasevalue in the same template. + // The problem is, many clients _say_ they + // support coinbasetxn when they don't (ckpool). + // To make matters worse, some clients will + // parse an undefined `coinbasevalue` as zero. + // Because of all of this, coinbasetxn is + // disabled for now. + valueCap = true; + + if (txnCap && !valueCap) { + if (this.miner.addresses.length === 0) { + throw new RPCError(errs.MISC_ERROR, + 'No addresses available for coinbase.'); + } + coinbase = true; + } + } + + if (!this.network.selfConnect) { + if (this.pool.peers.size() === 0) { + throw new RPCError(errs.CLIENT_NOT_CONNECTED, + 'Bitcoin is not connected!'); + } + + if (!this.chain.synced) { + throw new RPCError(errs.CLIENT_IN_INITIAL_DOWNLOAD, + 'Bitcoin is downloading blocks...'); + } + } + + const lpid = valid.str('longpollid'); + + if (lpid) + await this.handleLongpoll(lpid); + + if (!rules) + rules = []; + + return await this.createTemplate(maxVersion, coinbase, rules); + } + + async createTemplate(maxVersion, coinbase, rules) { + const unlock = await this.locker.lock(); + try { + return await this._createTemplate(maxVersion, coinbase, rules); + } finally { + unlock(); + } + } + + async _createTemplate(maxVersion, coinbase, rules) { + const attempt = await this.getTemplate(); + const scale = attempt.witness ? 1 : consensus.WITNESS_SCALE_FACTOR; + + // Default mutable fields. + const mutable = ['time', 'transactions', 'prevblock']; + + // The miner doesn't support + // versionbits. Force them to + // encode our version. + if (maxVersion >= 2) + mutable.push('version/force'); + + // Allow the miner to change + // our provided coinbase. + // Note that these are implied + // without `coinbasetxn`. + if (coinbase) { + mutable.push('coinbase'); + mutable.push('coinbase/append'); + mutable.push('generation'); + } + + // Build an index of every transaction. + const index = new Map(); + for (let i = 0; i < attempt.items.length; i++) { + const entry = attempt.items[i]; + index.set(entry.hash, i + 1); + } + + // Calculate dependencies for each transaction. + const txs = []; + for (let i = 0; i < attempt.items.length; i++) { + const entry = attempt.items[i]; + const tx = entry.tx; + const deps = []; + + for (let j = 0; j < tx.inputs.length; j++) { + const input = tx.inputs[j]; + const dep = index.get(input.prevout.hash); + + if (dep == null) + continue; + + if (deps.indexOf(dep) === -1) { + assert(dep < i + 1); + deps.push(dep); + } + } + + txs.push({ + data: tx.toRaw().toString('hex'), + txid: tx.txid(), + hash: tx.wtxid(), + depends: deps, + fee: entry.fee, + sigops: entry.sigops / scale | 0, + weight: tx.getWeight() + }); + } + + if (this.chain.options.bip91) { + rules.push('segwit'); + rules.push('segsignal'); + } + + if (this.chain.options.bip148) + rules.push('segwit'); + + // Calculate version based on given rules. + let version = attempt.version; + const vbavailable = {}; + const vbrules = []; + + for (const deploy of this.network.deploys) { + const state = await this.chain.getState(this.chain.tip, deploy); + let name = deploy.name; + + switch (state) { + case common.thresholdStates.DEFINED: + case common.thresholdStates.FAILED: + break; + case common.thresholdStates.LOCKED_IN: + version |= 1 << deploy.bit; + case common.thresholdStates.STARTED: + if (!deploy.force) { + if (rules.indexOf(name) === -1) + version &= ~(1 << deploy.bit); + if (deploy.required) + name = '!' + name; + } + vbavailable[name] = deploy.bit; + break; + case common.thresholdStates.ACTIVE: + if (!deploy.force && deploy.required) { + if (rules.indexOf(name) === -1) { + throw new RPCError(errs.INVALID_PARAMETER, + `Client must support ${name}.`); + } + name = '!' + name; + } + vbrules.push(name); + break; + default: + assert(false, 'Bad state.'); + break; + } + } + + version >>>= 0; + + const json = { + capabilities: ['proposal'], + mutable: mutable, + version: version, + rules: vbrules, + vbavailable: vbavailable, + vbrequired: 0, + height: attempt.height, + previousblockhash: util.revHex(attempt.prevBlock), + target: util.revHex(attempt.target.toString('hex')), + bits: util.hex32(attempt.bits), + noncerange: '00000000ffffffff', + curtime: attempt.time, + mintime: attempt.mtp + 1, + maxtime: attempt.time + 7200, + expires: attempt.time + 7200, + sigoplimit: consensus.MAX_BLOCK_SIGOPS_COST / scale | 0, + sizelimit: consensus.MAX_BLOCK_SIZE, + weightlimit: undefined, + longpollid: this.chain.tip.rhash() + util.pad32(this.totalTX()), + submitold: false, + coinbaseaux: { + flags: attempt.coinbaseFlags.toString('hex') + }, + coinbasevalue: undefined, + coinbasetxn: undefined, + default_witness_commitment: undefined, + transactions: txs + }; + + // See: + // bitcoin/bitcoin#9fc7f0bce94f1cea0239b1543227f22a3f3b9274 + if (attempt.witness) { + json.sizelimit = consensus.MAX_RAW_BLOCK_SIZE; + json.weightlimit = consensus.MAX_BLOCK_WEIGHT; + } + + // The client wants a coinbasetxn + // instead of a coinbasevalue. + if (coinbase) { + const tx = attempt.toCoinbase(); + const input = tx.inputs[0]; + + // Pop off the nonces. + input.script.pop(); + input.script.compile(); + + if (attempt.witness) { + // We don't include the commitment + // output (see bip145). + const output = tx.outputs.pop(); + assert(output.script.isCommitment()); + + // Also not including the witness nonce. + input.witness.clear(); + } + + tx.refresh(); + + json.coinbasetxn = { + data: tx.toRaw().toString('hex'), + txid: tx.txid(), + hash: tx.wtxid(), + depends: [], + fee: 0, + sigops: tx.getSigopsCost() / scale | 0, + weight: tx.getWeight() + }; + } else { + json.coinbasevalue = attempt.getReward(); + } + + if (rules.indexOf('segwit') !== -1) + json.default_witness_commitment = attempt.getWitnessScript().toJSON(); + + return json; + } + + async getMiningInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getmininginfo'); + + const attempt = this.attempt; + + let size = 0; + let weight = 0; + let txs = 0; + let diff = 0; + + if (attempt) { + weight = attempt.weight; + txs = attempt.items.length + 1; + diff = attempt.getDifficulty(); + size = 1000; + for (const item of attempt.items) + size += item.tx.getBaseSize(); + } + + return { + blocks: this.chain.height, + currentblocksize: size, + currentblockweight: weight, + currentblocktx: txs, + difficulty: diff, + errors: '', + genproclimit: this.procLimit, + networkhashps: await this.getHashRate(120), + pooledtx: this.totalTX(), + testnet: this.network !== Network.main, + chain: this.network.type !== 'testnet' + ? this.network.type + : 'test', + generate: this.mining + }; + } + + async getNetworkHashPS(args, help) { + if (help || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getnetworkhashps ( blocks height )'); + + const valid = new Validator(args); + const lookup = valid.u32(0, 120); + const height = valid.u32(1); + + return await this.getHashRate(lookup, height); + } + + async prioritiseTransaction(args, help) { + if (help || args.length !== 3) { + throw new RPCError(errs.MISC_ERROR, + 'prioritisetransaction '); + } + + const valid = new Validator(args); + const hash = valid.rhash(0); + const pri = valid.i64(1); + const fee = valid.i64(2); + + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID'); + + if (pri == null || fee == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid fee or priority.'); + + const entry = this.mempool.getEntry(hash); + + if (!entry) + throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); + + this.mempool.prioritise(entry, pri, fee); + + return true; + } + + async verifyBlock(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'verifyblock "block-hex"'); + + const valid = new Validator(args); + const data = valid.buf(0); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid block hex.'); + + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot verify block in SPV mode.'); + + const block = Block.fromRaw(data); try { await this.chain.verifyBlock(block); @@ -1201,1505 +1571,1133 @@ RPC.prototype.getBlockTemplate = async function getBlockTemplate(args, help) { return null; } - let maxVersion = valid.u32('maxversion', -1); - let rules = valid.array('rules'); + /* + * Coin generation + */ - if (rules) - maxVersion = -1; - - const capabilities = valid.array('capabilities'); - let coinbase = false; - - if (capabilities) { - let txnCap = false; - let valueCap = false; - - for (const capability of capabilities) { - if (typeof capability !== 'string') - throw new RPCError(errs.TYPE_ERROR, 'Invalid capability.'); - - switch (capability) { - case 'coinbasetxn': - txnCap = true; - break; - case 'coinbasevalue': - // Prefer value if they support it. - valueCap = true; - break; - } - } - - // BIP22 states that we can't have coinbasetxn - // _and_ coinbasevalue in the same template. - // The problem is, many clients _say_ they - // support coinbasetxn when they don't (ckpool). - // To make matters worse, some clients will - // parse an undefined `coinbasevalue` as zero. - // Because of all of this, coinbasetxn is - // disabled for now. - valueCap = true; - - if (txnCap && !valueCap) { - if (this.miner.addresses.length === 0) { - throw new RPCError(errs.MISC_ERROR, - 'No addresses available for coinbase.'); - } - coinbase = true; - } + async getGenerate(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getgenerate'); + return this.mining; } - if (!this.network.selfConnect) { - if (this.pool.peers.size() === 0) { - throw new RPCError(errs.CLIENT_NOT_CONNECTED, - 'Bitcoin is not connected!'); - } + async setGenerate(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'setgenerate mine ( proclimit )'); - if (!this.chain.synced) { - throw new RPCError(errs.CLIENT_IN_INITIAL_DOWNLOAD, - 'Bitcoin is downloading blocks...'); - } - } + const valid = new Validator(args); + const mine = valid.bool(0, false); + const limit = valid.u32(1, 0); - const lpid = valid.str('longpollid'); - - if (lpid) - await this.handleLongpoll(lpid); - - if (!rules) - rules = []; - - return await this.createTemplate(maxVersion, coinbase, rules); -}; - -RPC.prototype.createTemplate = async function createTemplate(maxVersion, coinbase, rules) { - const unlock = await this.locker.lock(); - try { - return await this._createTemplate(maxVersion, coinbase, rules); - } finally { - unlock(); - } -}; - -RPC.prototype._createTemplate = async function _createTemplate(maxVersion, coinbase, rules) { - const attempt = await this.getTemplate(); - const scale = attempt.witness ? 1 : consensus.WITNESS_SCALE_FACTOR; - - // Default mutable fields. - const mutable = ['time', 'transactions', 'prevblock']; - - // The miner doesn't support - // versionbits. Force them to - // encode our version. - if (maxVersion >= 2) - mutable.push('version/force'); - - // Allow the miner to change - // our provided coinbase. - // Note that these are implied - // without `coinbasetxn`. - if (coinbase) { - mutable.push('coinbase'); - mutable.push('coinbase/append'); - mutable.push('generation'); - } - - // Build an index of every transaction. - const index = new Map(); - for (let i = 0; i < attempt.items.length; i++) { - const entry = attempt.items[i]; - index.set(entry.hash, i + 1); - } - - // Calculate dependencies for each transaction. - const txs = []; - for (let i = 0; i < attempt.items.length; i++) { - const entry = attempt.items[i]; - const tx = entry.tx; - const deps = []; - - for (let j = 0; j < tx.inputs.length; j++) { - const input = tx.inputs[j]; - const dep = index.get(input.prevout.hash); - - if (dep == null) - continue; - - if (deps.indexOf(dep) === -1) { - assert(dep < i + 1); - deps.push(dep); - } - } - - txs.push({ - data: tx.toRaw().toString('hex'), - txid: tx.txid(), - hash: tx.wtxid(), - depends: deps, - fee: entry.fee, - sigops: entry.sigops / scale | 0, - weight: tx.getWeight() - }); - } - - if (this.chain.options.bip91) { - rules.push('segwit'); - rules.push('segsignal'); - } - - if (this.chain.options.bip148) - rules.push('segwit'); - - // Calculate version based on given rules. - let version = attempt.version; - const vbavailable = {}; - const vbrules = []; - - for (const deploy of this.network.deploys) { - const state = await this.chain.getState(this.chain.tip, deploy); - let name = deploy.name; - - switch (state) { - case common.thresholdStates.DEFINED: - case common.thresholdStates.FAILED: - break; - case common.thresholdStates.LOCKED_IN: - version |= 1 << deploy.bit; - case common.thresholdStates.STARTED: - if (!deploy.force) { - if (rules.indexOf(name) === -1) - version &= ~(1 << deploy.bit); - if (deploy.required) - name = '!' + name; - } - vbavailable[name] = deploy.bit; - break; - case common.thresholdStates.ACTIVE: - if (!deploy.force && deploy.required) { - if (rules.indexOf(name) === -1) { - throw new RPCError(errs.INVALID_PARAMETER, - `Client must support ${name}.`); - } - name = '!' + name; - } - vbrules.push(name); - break; - default: - assert(false, 'Bad state.'); - break; - } - } - - version >>>= 0; - - const json = { - capabilities: ['proposal'], - mutable: mutable, - version: version, - rules: vbrules, - vbavailable: vbavailable, - vbrequired: 0, - height: attempt.height, - previousblockhash: util.revHex(attempt.prevBlock), - target: util.revHex(attempt.target.toString('hex')), - bits: util.hex32(attempt.bits), - noncerange: '00000000ffffffff', - curtime: attempt.time, - mintime: attempt.mtp + 1, - maxtime: attempt.time + 7200, - expires: attempt.time + 7200, - sigoplimit: consensus.MAX_BLOCK_SIGOPS_COST / scale | 0, - sizelimit: consensus.MAX_BLOCK_SIZE, - weightlimit: undefined, - longpollid: this.chain.tip.rhash() + util.pad32(this.totalTX()), - submitold: false, - coinbaseaux: { - flags: attempt.coinbaseFlags.toString('hex') - }, - coinbasevalue: undefined, - coinbasetxn: undefined, - default_witness_commitment: undefined, - transactions: txs - }; - - // See: - // bitcoin/bitcoin#9fc7f0bce94f1cea0239b1543227f22a3f3b9274 - if (attempt.witness) { - json.sizelimit = consensus.MAX_RAW_BLOCK_SIZE; - json.weightlimit = consensus.MAX_BLOCK_WEIGHT; - } - - // The client wants a coinbasetxn - // instead of a coinbasevalue. - if (coinbase) { - const tx = attempt.toCoinbase(); - const input = tx.inputs[0]; - - // Pop off the nonces. - input.script.pop(); - input.script.compile(); - - if (attempt.witness) { - // We don't include the commitment - // output (see bip145). - const output = tx.outputs.pop(); - assert(output.script.isCommitment()); - - // Also not including the witness nonce. - input.witness.clear(); - } - - tx.refresh(); - - json.coinbasetxn = { - data: tx.toRaw().toString('hex'), - txid: tx.txid(), - hash: tx.wtxid(), - depends: [], - fee: 0, - sigops: tx.getSigopsCost() / scale | 0, - weight: tx.getWeight() - }; - } else { - json.coinbasevalue = attempt.getReward(); - } - - if (rules.indexOf('segwit') !== -1) - json.default_witness_commitment = attempt.getWitnessScript().toJSON(); - - return json; -}; - -RPC.prototype.getMiningInfo = async function getMiningInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getmininginfo'); - - const attempt = this.attempt; - - let size = 0; - let weight = 0; - let txs = 0; - let diff = 0; - - if (attempt) { - weight = attempt.weight; - txs = attempt.items.length + 1; - diff = attempt.getDifficulty(); - size = 1000; - for (const item of attempt.items) - size += item.tx.getBaseSize(); - } - - return { - blocks: this.chain.height, - currentblocksize: size, - currentblockweight: weight, - currentblocktx: txs, - difficulty: diff, - errors: '', - genproclimit: this.procLimit, - networkhashps: await this.getHashRate(120), - pooledtx: this.totalTX(), - testnet: this.network !== Network.main, - chain: this.network.type !== 'testnet' - ? this.network.type - : 'test', - generate: this.mining - }; -}; - -RPC.prototype.getNetworkHashPS = async function getNetworkHashPS(args, help) { - if (help || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'getnetworkhashps ( blocks height )'); - - const valid = new Validator(args); - const lookup = valid.u32(0, 120); - const height = valid.u32(1); - - return await this.getHashRate(lookup, height); -}; - -RPC.prototype.prioritiseTransaction = async function prioritiseTransaction(args, help) { - if (help || args.length !== 3) { - throw new RPCError(errs.MISC_ERROR, - 'prioritisetransaction '); - } - - const valid = new Validator(args); - const hash = valid.rhash(0); - const pri = valid.i64(1); - const fee = valid.i64(2); - - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID'); - - if (pri == null || fee == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid fee or priority.'); - - const entry = this.mempool.getEntry(hash); - - if (!entry) - throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.'); - - this.mempool.prioritise(entry, pri, fee); - - return true; -}; - -RPC.prototype.verifyBlock = async function verifyBlock(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'verifyblock "block-hex"'); - - const valid = new Validator(args); - const data = valid.buf(0); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid block hex.'); - - if (this.chain.options.spv) - throw new RPCError(errs.MISC_ERROR, 'Cannot verify block in SPV mode.'); - - const block = Block.fromRaw(data); - - try { - await this.chain.verifyBlock(block); - } catch (e) { - if (e.type === 'VerifyError') - return e.reason; - throw e; - } - - return null; -}; - -/* - * Coin generation - */ - -RPC.prototype.getGenerate = async function getGenerate(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getgenerate'); - return this.mining; -}; - -RPC.prototype.setGenerate = async function setGenerate(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'setgenerate mine ( proclimit )'); - - const valid = new Validator(args); - const mine = valid.bool(0, false); - const limit = valid.u32(1, 0); - - if (mine && this.miner.addresses.length === 0) { - throw new RPCError(errs.MISC_ERROR, - 'No addresses available for coinbase.'); - } - - this.mining = mine; - this.procLimit = limit; - - if (mine) { - this.miner.cpu.start(); - return true; - } - - await this.miner.cpu.stop(); - - return false; -}; - -RPC.prototype.generate = async function generate(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'generate numblocks ( maxtries )'); - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - const tries = valid.u32(1); - - if (this.miner.addresses.length === 0) { - throw new RPCError(errs.MISC_ERROR, - 'No addresses available for coinbase.'); - } - - return await this.mineBlocks(blocks, null, tries); -}; - -RPC.prototype.generateToAddress = async function generateToAddress(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'generatetoaddress numblocks address ( maxtries )'); - } - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - const str = valid.str(1, ''); - const tries = valid.u32(2); - - const addr = parseAddress(str, this.network); - - return await this.mineBlocks(blocks, addr, tries); -}; - -/* - * Raw transactions - */ - -RPC.prototype.createRawTransaction = async function createRawTransaction(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'createrawtransaction' - + ' [{"txid":"id","vout":n},...]' - + ' {"address":amount,"data":"hex",...}' - + ' ( locktime )'); - } - - const valid = new Validator(args); - const inputs = valid.array(0); - const sendTo = valid.obj(1); - const locktime = valid.u32(2); - - if (!inputs || !sendTo) { - throw new RPCError(errs.TYPE_ERROR, - 'Invalid parameters (inputs and sendTo).'); - } - - const tx = new MTX(); - - if (locktime != null) - tx.locktime = locktime; - - for (const obj of inputs) { - const valid = new Validator(obj); - const hash = valid.rhash('txid'); - const index = valid.u32('vout'); - let sequence = valid.u32('sequence', 0xffffffff); - - if (tx.locktime) - sequence--; - - if (!hash || index == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.'); - - const input = new Input(); - input.prevout.hash = hash; - input.prevout.index = index; - input.sequence = sequence; - - tx.inputs.push(input); - } - - const sends = new Validator(sendTo); - const uniq = new Set(); - - for (const key of Object.keys(sendTo)) { - if (key === 'data') { - const value = sends.buf(key); - - if (!value) - throw new RPCError(errs.TYPE_ERROR, 'Invalid nulldata..'); - - const output = new Output(); - output.value = 0; - output.script.fromNulldata(value); - tx.outputs.push(output); - - continue; - } - - const addr = parseAddress(key, this.network); - const b58 = addr.toString(this.network); - - if (uniq.has(b58)) - throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address'); - - uniq.add(b58); - - const value = sends.ufixed(key, 8); - - if (value == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid output value.'); - - const output = new Output(); - output.value = value; - output.script.fromAddress(addr); - - tx.outputs.push(output); - } - - return tx.toRaw().toString('hex'); -}; - -RPC.prototype.decodeRawTransaction = async function decodeRawTransaction(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'decoderawtransaction "hexstring"'); - - const valid = new Validator(args); - const data = valid.buf(0); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); - - const tx = TX.fromRaw(data); - - return this.txToJSON(tx); -}; - -RPC.prototype.decodeScript = async function decodeScript(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'decodescript "hex"'); - - const valid = new Validator(args); - const data = valid.buf(0); - - let script = new Script(); - - if (data) - script = Script.fromRaw(data); - - const addr = Address.fromScripthash(script.hash160()); - - const json = this.scriptToJSON(script); - json.p2sh = addr.toString(this.network); - - return json; -}; - -RPC.prototype.getRawTransaction = async function getRawTransaction(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'getrawtransaction "txid" ( verbose )'); - - const valid = new Validator(args); - const hash = valid.rhash(0); - const verbose = valid.bool(1, false); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); - - const meta = await this.node.getMeta(hash); - - if (!meta) - throw new RPCError(errs.MISC_ERROR, 'Transaction not found.'); - - const tx = meta.tx; - - if (!verbose) - return tx.toRaw().toString('hex'); - - let entry; - if (meta.block) - entry = await this.chain.getEntry(meta.block); - - const json = this.txToJSON(tx, entry); - json.time = meta.mtime; - json.hex = tx.toRaw().toString('hex'); - - return json; -}; - -RPC.prototype.sendRawTransaction = async function sendRawTransaction(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'sendrawtransaction "hexstring" ( allowhighfees )'); - } - - const valid = new Validator(args); - const data = valid.buf(0); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); - - const tx = TX.fromRaw(data); - - this.node.relay(tx); - - return tx.txid(); -}; - -RPC.prototype.signRawTransaction = async function signRawTransaction(args, help) { - if (help || args.length < 1 || args.length > 4) { - throw new RPCError(errs.MISC_ERROR, - 'signrawtransaction' - + ' "hexstring" (' - + ' [{"txid":"id","vout":n,"scriptPubKey":"hex",' - + 'redeemScript":"hex"},...] ["privatekey1",...]' - + ' sighashtype )'); - } - - const valid = new Validator(args); - const data = valid.buf(0); - const prevout = valid.array(1); - const secrets = valid.array(2); - const sighash = valid.str(3); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); - - if (!this.mempool) - throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); - - const tx = MTX.fromRaw(data); - tx.view = await this.mempool.getSpentView(tx); - - const map = new Map(); - const keys = []; - - if (secrets) { - const valid = new Validator(secrets); - for (let i = 0; i < secrets.length; i++) { - const secret = valid.str(i, ''); - const key = parseSecret(secret, this.network); - map.set(key.getPublicKey('hex'), key); - keys.push(key); - } - } - - if (prevout) { - for (const prev of prevout) { - const valid = new Validator(prev); - const hash = valid.rhash('txid'); - const index = valid.u32('vout'); - const scriptRaw = valid.buf('scriptPubKey'); - const value = valid.ufixed('amount', 8); - const redeemRaw = valid.buf('redeemScript'); - - if (!hash || index == null || !scriptRaw || value == null) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid UTXO.'); - - const outpoint = new Outpoint(hash, index); - - const script = Script.fromRaw(scriptRaw); - const coin = Output.fromScript(script, value); - - tx.view.addOutput(outpoint, coin); - - if (keys.length === 0 || !redeemRaw) - continue; - - if (!script.isScripthash() && !script.isWitnessScripthash()) - continue; - - if (!redeemRaw) { - throw new RPCError(errs.INVALID_PARAMETER, - 'P2SH requires redeem script.'); - } - - const redeem = Script.fromRaw(redeemRaw); - - for (const op of redeem.code) { - if (!op.data) - continue; - - const key = map.get(op.data.toString('hex')); - - if (key) { - key.script = redeem; - key.witness = script.isWitnessScripthash(); - key.refresh(); - break; - } - } - } - } - - let type = Script.hashType.ALL; - if (sighash) { - const parts = sighash.split('|'); - - if (parts.length < 1 || parts.length > 2) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); - - type = Script.hashType[parts[0]]; - - if (type == null) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); - - if (parts.length === 2) { - if (parts[1] !== 'ANYONECANPAY') - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); - type |= Script.hashType.ANYONECANPAY; - } - } - - await tx.signAsync(keys, type, this.workers); - - return { - hex: tx.toRaw().toString('hex'), - complete: tx.isSigned() - }; -}; - -/* - * Utility Functions - */ - -RPC.prototype.createMultisig = async function createMultisig(args, help) { - if (help || args.length < 2 || args.length > 2) - throw new RPCError(errs.MISC_ERROR, 'createmultisig nrequired ["key",...]'); - - const valid = new Validator(args); - const keys = valid.array(1, []); - const m = valid.u32(0, 0); - const n = keys.length; - - if (m < 1 || n < m || n > 16) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid m and n values.'); - - const items = new Validator(keys); - - for (let i = 0; i < keys.length; i++) { - const key = items.buf(i); - - if (!key) - throw new RPCError(errs.TYPE_ERROR, 'Invalid key.'); - - if (!secp256k1.publicKeyVerify(key)) - throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.'); - - keys[i] = key; - } - - const script = Script.fromMultisig(m, n, keys); - - if (script.getSize() > consensus.MAX_SCRIPT_PUSH) - throw new RPCError(errs.VERIFY_ERROR, 'Redeem script exceeds size limit.'); - - const addr = script.getAddress(); - - return { - address: addr.toString(this.network), - redeemScript: script.toJSON() - }; -}; - -RPC.prototype.createWitnessAddress = async function createWitnessAddress(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'createwitnessaddress "script"'); - - const valid = new Validator(args); - const raw = valid.buf(0); - - if (!raw) - throw new RPCError(errs.TYPE_ERROR, 'Invalid script hex.'); - - const script = Script.fromRaw(raw); - const program = script.forWitness(); - const addr = program.getAddress(); - - return { - address: addr.toString(this.network), - witnessScript: program.toJSON() - }; -}; - -RPC.prototype.validateAddress = async function validateAddress(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'validateaddress "bitcoinaddress"'); - - const valid = new Validator(args); - const str = valid.str(0, ''); - - let addr; - try { - addr = Address.fromString(str, this.network); - } catch (e) { - return { - isvalid: false - }; - } - - const script = Script.fromAddress(addr); - - return { - isvalid: true, - address: addr.toString(this.network), - scriptPubKey: script.toJSON(), - ismine: false, - iswatchonly: false - }; -}; - -RPC.prototype.verifyMessage = async function verifyMessage(args, help) { - if (help || args.length !== 3) { - throw new RPCError(errs.MISC_ERROR, - 'verifymessage "bitcoinaddress" "signature" "message"'); - } - - const valid = new Validator(args); - const b58 = valid.str(0, ''); - const sig = valid.buf(1, null, 'base64'); - const str = valid.str(2); - - if (!sig || !str) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.'); - - const addr = parseAddress(b58, this.network); - const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); - const hash = digest.hash256(msg); - - const key = secp256k1.recover(hash, sig, 0, true); - - if (!key) - return false; - - return ccmp(digest.hash160(key), addr.hash); -}; - -RPC.prototype.signMessageWithPrivkey = async function signMessageWithPrivkey(args, help) { - if (help || args.length !== 2) { - throw new RPCError(errs.MISC_ERROR, - 'signmessagewithprivkey "privkey" "message"'); - } - - const valid = new Validator(args); - const wif = valid.str(0, ''); - const str = valid.str(1, ''); - - const key = parseSecret(wif, this.network); - const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); - const hash = digest.hash256(msg); - const sig = key.sign(hash); - - return sig.toString('base64'); -}; - -RPC.prototype.estimateFee = async function estimateFee(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'estimatefee nblocks'); - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - - if (!this.fees) - throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.'); - - const fee = this.fees.estimateFee(blocks, false); - - if (fee === 0) - return -1; - - return Amount.btc(fee, true); -}; - -RPC.prototype.estimatePriority = async function estimatePriority(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'estimatepriority nblocks'); - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - - if (!this.fees) - throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.'); - - return this.fees.estimatePriority(blocks, false); -}; - -RPC.prototype.estimateSmartFee = async function estimateSmartFee(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'estimatesmartfee nblocks'); - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - - if (!this.fees) - throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.'); - - let fee = this.fees.estimateFee(blocks, true); - - if (fee === 0) - fee = -1; - else - fee = Amount.btc(fee, true); - - return { - fee: fee, - blocks: blocks - }; -}; - -RPC.prototype.estimateSmartPriority = async function estimateSmartPriority(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'estimatesmartpriority nblocks'); - - const valid = new Validator(args); - const blocks = valid.u32(0, 1); - - if (!this.fees) - throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.'); - - const pri = this.fees.estimatePriority(blocks, true); - - return { - priority: pri, - blocks: blocks - }; -}; - -RPC.prototype.invalidateBlock = async function invalidateBlock(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'invalidateblock "hash"'); - - const valid = new Validator(args); - const hash = valid.rhash(0); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); - - await this.chain.invalidate(hash); - - return null; -}; - -RPC.prototype.reconsiderBlock = async function reconsiderBlock(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'reconsiderblock "hash"'); - - const valid = new Validator(args); - const hash = valid.rhash(0); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); - - this.chain.removeInvalid(hash); - - return null; -}; - -RPC.prototype.setMockTime = async function setMockTime(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'setmocktime timestamp'); - - const valid = new Validator(args); - const time = valid.u32(0); - - if (time == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid timestamp.'); - - this.network.time.offset = 0; - - const delta = this.network.now() - time; - - this.network.time.offset = -delta; - - return null; -}; - -RPC.prototype.getMemoryInfo = async function getMemoryInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo'); - - return util.memoryUsage(); -}; - -RPC.prototype.setLogLevel = async function setLogLevel(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"'); - - const valid = new Validator(args); - const level = valid.str(0, ''); - - this.logger.setLevel(level); - - return null; -}; - -/* - * Helpers - */ - -RPC.prototype.handleLongpoll = async function handleLongpoll(lpid) { - if (lpid.length !== 74) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.'); - - const watched = lpid.slice(0, 64); - const lastTX = parseInt(lpid.slice(64, 74), 10); - - if (!util.isHex(watched) || !util.isU32(lastTX)) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.'); - - const hash = util.revHex(watched); - - if (this.chain.tip.hash !== hash) - return; - - await this.longpoll(); -}; - -RPC.prototype.longpoll = function longpoll() { - return new Promise((resolve, reject) => { - this.pollers.push(co.job(resolve, reject)); - }); -}; - -RPC.prototype.refreshBlock = function refreshBlock() { - const pollers = this.pollers; - - this.attempt = null; - this.lastActivity = 0; - this.merkleMap.clear(); - this.nonce1 = 0; - this.nonce2 = 0; - this.pollers = []; - - for (const job of pollers) - job.resolve(); -}; - -RPC.prototype.bindChain = function bindChain() { - if (this.boundChain) - return; - - this.boundChain = true; - - this.node.on('connect', () => { - if (!this.attempt) - return; - - this.refreshBlock(); - }); - - if (!this.mempool) - return; - - this.node.on('tx', () => { - if (!this.attempt) - return; - - if (util.now() - this.lastActivity > 10) - this.refreshBlock(); - }); -}; - -RPC.prototype.getTemplate = async function getTemplate() { - this.bindChain(); - - let attempt = this.attempt; - - if (attempt) { - this.miner.updateTime(attempt); - } else { - attempt = await this.miner.createBlock(); - this.attempt = attempt; - this.lastActivity = util.now(); - } - - return attempt; -}; - -RPC.prototype.updateWork = async function updateWork() { - this.bindChain(); - - let attempt = this.attempt; - - if (attempt) { - if (attempt.address.isNull()) { + if (mine && this.miner.addresses.length === 0) { throw new RPCError(errs.MISC_ERROR, 'No addresses available for coinbase.'); } - this.miner.updateTime(attempt); + this.mining = mine; + this.procLimit = limit; - if (++this.nonce2 === 0x100000000) { - this.nonce2 = 0; - this.nonce1++; + if (mine) { + this.miner.cpu.start(); + return true; } + await this.miner.cpu.stop(); + + return false; + } + + async generate(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'generate numblocks ( maxtries )'); + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + const tries = valid.u32(1); + + if (this.miner.addresses.length === 0) { + throw new RPCError(errs.MISC_ERROR, + 'No addresses available for coinbase.'); + } + + return await this.mineBlocks(blocks, null, tries); + } + + async generateToAddress(args, help) { + if (help || args.length < 2 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'generatetoaddress numblocks address ( maxtries )'); + } + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + const str = valid.str(1, ''); + const tries = valid.u32(2); + + const addr = parseAddress(str, this.network); + + return await this.mineBlocks(blocks, addr, tries); + } + + /* + * Raw transactions + */ + + async createRawTransaction(args, help) { + if (help || args.length < 2 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'createrawtransaction' + + ' [{"txid":"id","vout":n},...]' + + ' {"address":amount,"data":"hex",...}' + + ' ( locktime )'); + } + + const valid = new Validator(args); + const inputs = valid.array(0); + const sendTo = valid.obj(1); + const locktime = valid.u32(2); + + if (!inputs || !sendTo) { + throw new RPCError(errs.TYPE_ERROR, + 'Invalid parameters (inputs and sendTo).'); + } + + const tx = new MTX(); + + if (locktime != null) + tx.locktime = locktime; + + for (const obj of inputs) { + const valid = new Validator(obj); + const hash = valid.rhash('txid'); + const index = valid.u32('vout'); + let sequence = valid.u32('sequence', 0xffffffff); + + if (tx.locktime) + sequence--; + + if (!hash || index == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.'); + + const input = new Input(); + input.prevout.hash = hash; + input.prevout.index = index; + input.sequence = sequence; + + tx.inputs.push(input); + } + + const sends = new Validator(sendTo); + const uniq = new Set(); + + for (const key of Object.keys(sendTo)) { + if (key === 'data') { + const value = sends.buf(key); + + if (!value) + throw new RPCError(errs.TYPE_ERROR, 'Invalid nulldata..'); + + const output = new Output(); + output.value = 0; + output.script.fromNulldata(value); + tx.outputs.push(output); + + continue; + } + + const addr = parseAddress(key, this.network); + const b58 = addr.toString(this.network); + + if (uniq.has(b58)) + throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address'); + + uniq.add(b58); + + const value = sends.ufixed(key, 8); + + if (value == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid output value.'); + + const output = new Output(); + output.value = value; + output.script.fromAddress(addr); + + tx.outputs.push(output); + } + + return tx.toRaw().toString('hex'); + } + + async decodeRawTransaction(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'decoderawtransaction "hexstring"'); + + const valid = new Validator(args); + const data = valid.buf(0); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); + + const tx = TX.fromRaw(data); + + return this.txToJSON(tx); + } + + async decodeScript(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'decodescript "hex"'); + + const valid = new Validator(args); + const data = valid.buf(0); + + let script = new Script(); + + if (data) + script = Script.fromRaw(data); + + const addr = Address.fromScripthash(script.hash160()); + + const json = this.scriptToJSON(script); + json.p2sh = addr.toString(this.network); + + return json; + } + + async getRawTransaction(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getrawtransaction "txid" ( verbose )'); + + const valid = new Validator(args); + const hash = valid.rhash(0); + const verbose = valid.bool(1, false); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.'); + + const meta = await this.node.getMeta(hash); + + if (!meta) + throw new RPCError(errs.MISC_ERROR, 'Transaction not found.'); + + const tx = meta.tx; + + if (!verbose) + return tx.toRaw().toString('hex'); + + let entry; + if (meta.block) + entry = await this.chain.getEntry(meta.block); + + const json = this.txToJSON(tx, entry); + json.time = meta.mtime; + json.hex = tx.toRaw().toString('hex'); + + return json; + } + + async sendRawTransaction(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'sendrawtransaction "hexstring" ( allowhighfees )'); + } + + const valid = new Validator(args); + const data = valid.buf(0); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); + + const tx = TX.fromRaw(data); + + this.node.relay(tx); + + return tx.txid(); + } + + async signRawTransaction(args, help) { + if (help || args.length < 1 || args.length > 4) { + throw new RPCError(errs.MISC_ERROR, + 'signrawtransaction' + + ' "hexstring" (' + + ' [{"txid":"id","vout":n,"scriptPubKey":"hex",' + + 'redeemScript":"hex"},...] ["privatekey1",...]' + + ' sighashtype )'); + } + + const valid = new Validator(args); + const data = valid.buf(0); + const prevout = valid.array(1); + const secrets = valid.array(2); + const sighash = valid.str(3); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); + + if (!this.mempool) + throw new RPCError(errs.MISC_ERROR, 'No mempool available.'); + + const tx = MTX.fromRaw(data); + tx.view = await this.mempool.getSpentView(tx); + + const map = new Map(); + const keys = []; + + if (secrets) { + const valid = new Validator(secrets); + for (let i = 0; i < secrets.length; i++) { + const secret = valid.str(i, ''); + const key = parseSecret(secret, this.network); + map.set(key.getPublicKey('hex'), key); + keys.push(key); + } + } + + if (prevout) { + for (const prev of prevout) { + const valid = new Validator(prev); + const hash = valid.rhash('txid'); + const index = valid.u32('vout'); + const scriptRaw = valid.buf('scriptPubKey'); + const value = valid.ufixed('amount', 8); + const redeemRaw = valid.buf('redeemScript'); + + if (!hash || index == null || !scriptRaw || value == null) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid UTXO.'); + + const outpoint = new Outpoint(hash, index); + + const script = Script.fromRaw(scriptRaw); + const coin = Output.fromScript(script, value); + + tx.view.addOutput(outpoint, coin); + + if (keys.length === 0 || !redeemRaw) + continue; + + if (!script.isScripthash() && !script.isWitnessScripthash()) + continue; + + if (!redeemRaw) { + throw new RPCError(errs.INVALID_PARAMETER, + 'P2SH requires redeem script.'); + } + + const redeem = Script.fromRaw(redeemRaw); + + for (const op of redeem.code) { + if (!op.data) + continue; + + const key = map.get(op.data.toString('hex')); + + if (key) { + key.script = redeem; + key.witness = script.isWitnessScripthash(); + key.refresh(); + break; + } + } + } + } + + let type = Script.hashType.ALL; + if (sighash) { + const parts = sighash.split('|'); + + if (parts.length < 1 || parts.length > 2) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); + + type = Script.hashType[parts[0]]; + + if (type == null) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); + + if (parts.length === 2) { + if (parts[1] !== 'ANYONECANPAY') + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.'); + type |= Script.hashType.ANYONECANPAY; + } + } + + await tx.signAsync(keys, type, this.workers); + + return { + hex: tx.toRaw().toString('hex'), + complete: tx.isSigned() + }; + } + + /* + * Utility Functions + */ + + async createMultisig(args, help) { + if (help || args.length < 2 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'createmultisig nrequired ["key",...]'); + + const valid = new Validator(args); + const keys = valid.array(1, []); + const m = valid.u32(0, 0); + const n = keys.length; + + if (m < 1 || n < m || n > 16) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid m and n values.'); + + const items = new Validator(keys); + + for (let i = 0; i < keys.length; i++) { + const key = items.buf(i); + + if (!key) + throw new RPCError(errs.TYPE_ERROR, 'Invalid key.'); + + if (!secp256k1.publicKeyVerify(key)) + throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.'); + + keys[i] = key; + } + + const script = Script.fromMultisig(m, n, keys); + + if (script.getSize() > consensus.MAX_SCRIPT_PUSH) + throw new RPCError(errs.VERIFY_ERROR, 'Redeem script exceeds size limit.'); + + const addr = script.getAddress(); + + return { + address: addr.toString(this.network), + redeemScript: script.toJSON() + }; + } + + async createWitnessAddress(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'createwitnessaddress "script"'); + + const valid = new Validator(args); + const raw = valid.buf(0); + + if (!raw) + throw new RPCError(errs.TYPE_ERROR, 'Invalid script hex.'); + + const script = Script.fromRaw(raw); + const program = script.forWitness(); + const addr = program.getAddress(); + + return { + address: addr.toString(this.network), + witnessScript: program.toJSON() + }; + } + + async validateAddress(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'validateaddress "bitcoinaddress"'); + + const valid = new Validator(args); + const str = valid.str(0, ''); + + let addr; + try { + addr = Address.fromString(str, this.network); + } catch (e) { + return { + isvalid: false + }; + } + + const script = Script.fromAddress(addr); + + return { + isvalid: true, + address: addr.toString(this.network), + scriptPubKey: script.toJSON(), + ismine: false, + iswatchonly: false + }; + } + + async verifyMessage(args, help) { + if (help || args.length !== 3) { + throw new RPCError(errs.MISC_ERROR, + 'verifymessage "bitcoinaddress" "signature" "message"'); + } + + const valid = new Validator(args); + const b58 = valid.str(0, ''); + const sig = valid.buf(1, null, 'base64'); + const str = valid.str(2); + + if (!sig || !str) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.'); + + const addr = parseAddress(b58, this.network); + const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); + const hash = digest.hash256(msg); + + const key = secp256k1.recover(hash, sig, 0, true); + + if (!key) + return false; + + return ccmp(digest.hash160(key), addr.hash); + } + + async signMessageWithPrivkey(args, help) { + if (help || args.length !== 2) { + throw new RPCError(errs.MISC_ERROR, + 'signmessagewithprivkey "privkey" "message"'); + } + + const valid = new Validator(args); + const wif = valid.str(0, ''); + const str = valid.str(1, ''); + + const key = parseSecret(wif, this.network); + const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); + const hash = digest.hash256(msg); + const sig = key.sign(hash); + + return sig.toString('base64'); + } + + async estimateFee(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'estimatefee nblocks'); + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + + if (!this.fees) + throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.'); + + const fee = this.fees.estimateFee(blocks, false); + + if (fee === 0) + return -1; + + return Amount.btc(fee, true); + } + + async estimatePriority(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'estimatepriority nblocks'); + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + + if (!this.fees) + throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.'); + + return this.fees.estimatePriority(blocks, false); + } + + async estimateSmartFee(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'estimatesmartfee nblocks'); + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + + if (!this.fees) + throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.'); + + let fee = this.fees.estimateFee(blocks, true); + + if (fee === 0) + fee = -1; + else + fee = Amount.btc(fee, true); + + return { + fee: fee, + blocks: blocks + }; + } + + async estimateSmartPriority(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'estimatesmartpriority nblocks'); + + const valid = new Validator(args); + const blocks = valid.u32(0, 1); + + if (!this.fees) + throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.'); + + const pri = this.fees.estimatePriority(blocks, true); + + return { + priority: pri, + blocks: blocks + }; + } + + async invalidateBlock(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'invalidateblock "hash"'); + + const valid = new Validator(args); + const hash = valid.rhash(0); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); + + await this.chain.invalidate(hash); + + return null; + } + + async reconsiderBlock(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'reconsiderblock "hash"'); + + const valid = new Validator(args); + const hash = valid.rhash(0); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.'); + + this.chain.removeInvalid(hash); + + return null; + } + + async setMockTime(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'setmocktime timestamp'); + + const valid = new Validator(args); + const time = valid.u32(0); + + if (time == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid timestamp.'); + + this.network.time.offset = 0; + + const delta = this.network.now() - time; + + this.network.time.offset = -delta; + + return null; + } + + async getMemoryInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo'); + + return util.memoryUsage(); + } + + async setLogLevel(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"'); + + const valid = new Validator(args); + const level = valid.str(0, ''); + + this.logger.setLevel(level); + + return null; + } + + /* + * Helpers + */ + + async handleLongpoll(lpid) { + if (lpid.length !== 74) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.'); + + const watched = lpid.slice(0, 64); + const lastTX = parseInt(lpid.slice(64, 74), 10); + + if (!util.isHex(watched) || !util.isU32(lastTX)) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.'); + + const hash = util.revHex(watched); + + if (this.chain.tip.hash !== hash) + return; + + await this.longpoll(); + } + + longpoll() { + return new Promise((resolve, reject) => { + this.pollers.push(co.job(resolve, reject)); + }); + } + + refreshBlock() { + const pollers = this.pollers; + + this.attempt = null; + this.lastActivity = 0; + this.merkleMap.clear(); + this.nonce1 = 0; + this.nonce2 = 0; + this.pollers = []; + + for (const job of pollers) + job.resolve(); + } + + bindChain() { + if (this.boundChain) + return; + + this.boundChain = true; + + this.node.on('connect', () => { + if (!this.attempt) + return; + + this.refreshBlock(); + }); + + if (!this.mempool) + return; + + this.node.on('tx', () => { + if (!this.attempt) + return; + + if (util.now() - this.lastActivity > 10) + this.refreshBlock(); + }); + } + + async getTemplate() { + this.bindChain(); + + let attempt = this.attempt; + + if (attempt) { + this.miner.updateTime(attempt); + } else { + attempt = await this.miner.createBlock(); + this.attempt = attempt; + this.lastActivity = util.now(); + } + + return attempt; + } + + async updateWork() { + this.bindChain(); + + let attempt = this.attempt; + + if (attempt) { + if (attempt.address.isNull()) { + throw new RPCError(errs.MISC_ERROR, + 'No addresses available for coinbase.'); + } + + this.miner.updateTime(attempt); + + if (++this.nonce2 === 0x100000000) { + this.nonce2 = 0; + this.nonce1++; + } + + const n1 = this.nonce1; + const n2 = this.nonce2; + + const root = attempt.getRoot(n1, n2); + const hash = root.toString('hex'); + + this.merkleMap.set(hash, [n1, n2]); + + return attempt; + } + + if (this.miner.addresses.length === 0) { + throw new RPCError(errs.MISC_ERROR, + 'No addresses available for coinbase.'); + } + + attempt = await this.miner.createBlock(); + const n1 = this.nonce1; const n2 = this.nonce2; const root = attempt.getRoot(n1, n2); const hash = root.toString('hex'); + this.attempt = attempt; + this.lastActivity = util.now(); this.merkleMap.set(hash, [n1, n2]); return attempt; } - if (this.miner.addresses.length === 0) { - throw new RPCError(errs.MISC_ERROR, - 'No addresses available for coinbase.'); + async addBlock(block) { + const unlock1 = await this.locker.lock(); + const unlock2 = await this.chain.locker.lock(); + try { + return await this._addBlock(block); + } finally { + unlock2(); + unlock1(); + } } - attempt = await this.miner.createBlock(); + async _addBlock(block) { + this.logger.info('Handling submitted block: %s.', block.rhash()); - const n1 = this.nonce1; - const n2 = this.nonce2; + const prev = await this.chain.getEntry(block.prevBlock); - const root = attempt.getRoot(n1, n2); - const hash = root.toString('hex'); + if (prev) { + const state = await this.chain.getDeployments(block.time, prev); - this.attempt = attempt; - this.lastActivity = util.now(); - this.merkleMap.set(hash, [n1, n2]); + // Fix eloipool bug (witness nonce is not present). + if (state.hasWitness() && block.getCommitmentHash()) { + const tx = block.txs[0]; + const input = tx.inputs[0]; + if (!tx.hasWitness()) { + this.logger.warning('Submitted block had no witness nonce.'); + this.logger.debug(tx); - return attempt; -}; + // Recreate witness nonce (all zeroes). + input.witness.push(encoding.ZERO_HASH); + input.witness.compile(); -RPC.prototype.addBlock = async function addBlock(block) { - const unlock1 = await this.locker.lock(); - const unlock2 = await this.chain.locker.lock(); - try { - return await this._addBlock(block); - } finally { - unlock2(); - unlock1(); - } -}; - -RPC.prototype._addBlock = async function _addBlock(block) { - this.logger.info('Handling submitted block: %s.', block.rhash()); - - const prev = await this.chain.getEntry(block.prevBlock); - - if (prev) { - const state = await this.chain.getDeployments(block.time, prev); - - // Fix eloipool bug (witness nonce is not present). - if (state.hasWitness() && block.getCommitmentHash()) { - const tx = block.txs[0]; - const input = tx.inputs[0]; - if (!tx.hasWitness()) { - this.logger.warning('Submitted block had no witness nonce.'); - this.logger.debug(tx); - - // Recreate witness nonce (all zeroes). - input.witness.push(encoding.ZERO_HASH); - input.witness.compile(); - - tx.refresh(); - block.refresh(); + tx.refresh(); + block.refresh(); + } } } - } - let entry; - try { - entry = await this.chain._add(block); - } catch (err) { - if (err.type === 'VerifyError') { - this.logger.warning('RPC block rejected: %s (%s).', - block.rhash(), err.reason); - return `rejected: ${err.reason}`; - } - throw err; - } - - if (!entry) { - this.logger.warning('RPC block rejected: %s (bad-prevblk).', - block.rhash()); - return 'rejected: bad-prevblk'; - } - - return null; -}; - -RPC.prototype.totalTX = function totalTX() { - return this.mempool ? this.mempool.map.size : 0; -}; - -RPC.prototype.getSoftforks = function getSoftforks() { - return [ - toDeployment('bip34', 2, this.chain.state.hasBIP34()), - toDeployment('bip66', 3, this.chain.state.hasBIP66()), - toDeployment('bip65', 4, this.chain.state.hasCLTV()) - ]; -}; - -RPC.prototype.getBIP9Softforks = async function getBIP9Softforks() { - const tip = this.chain.tip; - const forks = {}; - - for (const deployment of this.network.deploys) { - const state = await this.chain.getState(tip, deployment); - let status; - - switch (state) { - case common.thresholdStates.DEFINED: - status = 'defined'; - break; - case common.thresholdStates.STARTED: - status = 'started'; - break; - case common.thresholdStates.LOCKED_IN: - status = 'locked_in'; - break; - case common.thresholdStates.ACTIVE: - status = 'active'; - break; - case common.thresholdStates.FAILED: - status = 'failed'; - break; - default: - assert(false, 'Bad state.'); - break; + let entry; + try { + entry = await this.chain._add(block); + } catch (err) { + if (err.type === 'VerifyError') { + this.logger.warning('RPC block rejected: %s (%s).', + block.rhash(), err.reason); + return `rejected: ${err.reason}`; + } + throw err; } - forks[deployment.name] = { - status: status, - bit: deployment.bit, - startTime: deployment.startTime, - timeout: deployment.timeout - }; + if (!entry) { + this.logger.warning('RPC block rejected: %s (bad-prevblk).', + block.rhash()); + return 'rejected: bad-prevblk'; + } + + return null; } - return forks; -}; - -RPC.prototype.getHashRate = async function getHashRate(lookup, height) { - let tip = this.chain.tip; - - if (height != null) - tip = await this.chain.getEntry(height); - - if (!tip) - return 0; - - assert(typeof lookup === 'number'); - assert(lookup >= 0); - - if (lookup === 0) - lookup = tip.height % this.network.pow.retargetInterval + 1; - - if (lookup > tip.height) - lookup = tip.height; - - let min = tip.time; - let max = min; - let entry = tip; - - for (let i = 0; i < lookup; i++) { - entry = await this.chain.getPrevious(entry); - - if (!entry) - throw new RPCError(errs.DATABASE_ERROR, 'Not found.'); - - min = Math.min(entry.time, min); - max = Math.max(entry.time, max); + totalTX() { + return this.mempool ? this.mempool.map.size : 0; } - const diff = max - min; - - if (diff === 0) - return 0; - - const work = tip.chainwork.sub(entry.chainwork); - - return Number(work.toString()) / diff; -}; - -RPC.prototype.mineBlocks = async function mineBlocks(blocks, addr, tries) { - const unlock = await this.locker.lock(); - try { - return await this._mineBlocks(blocks, addr, tries); - } finally { - unlock(); - } -}; - -RPC.prototype._mineBlocks = async function _mineBlocks(blocks, addr, tries) { - const hashes = []; - - for (let i = 0; i < blocks; i++) { - const block = await this.miner.mineBlock(null, addr); - const entry = await this.chain.add(block); - assert(entry); - hashes.push(entry.rhash()); + getSoftforks() { + return [ + toDeployment('bip34', 2, this.chain.state.hasBIP34()), + toDeployment('bip66', 3, this.chain.state.hasBIP66()), + toDeployment('bip65', 4, this.chain.state.hasCLTV()) + ]; } - return hashes; -}; + async getBIP9Softforks() { + const tip = this.chain.tip; + const forks = {}; -RPC.prototype.findFork = async function findFork(entry) { - while (entry) { - if (await this.chain.isMainChain(entry)) - return entry; - entry = await this.chain.getPrevious(entry); - } - throw new Error('Fork not found.'); -}; + for (const deployment of this.network.deploys) { + const state = await this.chain.getState(tip, deployment); + let status; -RPC.prototype.txToJSON = function txToJSON(tx, entry) { - let height = -1; - let time = 0; - let hash = null; - let conf = 0; + switch (state) { + case common.thresholdStates.DEFINED: + status = 'defined'; + break; + case common.thresholdStates.STARTED: + status = 'started'; + break; + case common.thresholdStates.LOCKED_IN: + status = 'locked_in'; + break; + case common.thresholdStates.ACTIVE: + status = 'active'; + break; + case common.thresholdStates.FAILED: + status = 'failed'; + break; + default: + assert(false, 'Bad state.'); + break; + } - if (entry) { - height = entry.height; - time = entry.time; - hash = entry.rhash(); - conf = this.chain.height - height + 1; - } - - const vin = []; - - for (const input of tx.inputs) { - const json = { - coinbase: undefined, - txid: undefined, - scriptSig: undefined, - txinwitness: undefined, - sequence: input.sequence - }; - - if (tx.isCoinbase()) { - json.coinbase = input.script.toJSON(); - } else { - json.txid = input.prevout.txid(); - json.vout = input.prevout.index; - json.scriptSig = { - asm: input.script.toASM(), - hex: input.script.toJSON() + forks[deployment.name] = { + status: status, + bit: deployment.bit, + startTime: deployment.startTime, + timeout: deployment.timeout }; } - if (input.witness.items.length > 0) { - json.txinwitness = input.witness.items.map((item) => { - return item.toString('hex'); + return forks; + } + + async getHashRate(lookup, height) { + let tip = this.chain.tip; + + if (height != null) + tip = await this.chain.getEntry(height); + + if (!tip) + return 0; + + assert(typeof lookup === 'number'); + assert(lookup >= 0); + + if (lookup === 0) + lookup = tip.height % this.network.pow.retargetInterval + 1; + + if (lookup > tip.height) + lookup = tip.height; + + let min = tip.time; + let max = min; + let entry = tip; + + for (let i = 0; i < lookup; i++) { + entry = await this.chain.getPrevious(entry); + + if (!entry) + throw new RPCError(errs.DATABASE_ERROR, 'Not found.'); + + min = Math.min(entry.time, min); + max = Math.max(entry.time, max); + } + + const diff = max - min; + + if (diff === 0) + return 0; + + const work = tip.chainwork.sub(entry.chainwork); + + return Number(work.toString()) / diff; + } + + async mineBlocks(blocks, addr, tries) { + const unlock = await this.locker.lock(); + try { + return await this._mineBlocks(blocks, addr, tries); + } finally { + unlock(); + } + } + + async _mineBlocks(blocks, addr, tries) { + const hashes = []; + + for (let i = 0; i < blocks; i++) { + const block = await this.miner.mineBlock(null, addr); + const entry = await this.chain.add(block); + assert(entry); + hashes.push(entry.rhash()); + } + + return hashes; + } + + async findFork(entry) { + while (entry) { + if (await this.chain.isMainChain(entry)) + return entry; + entry = await this.chain.getPrevious(entry); + } + throw new Error('Fork not found.'); + } + + txToJSON(tx, entry) { + let height = -1; + let time = 0; + let hash = null; + let conf = 0; + + if (entry) { + height = entry.height; + time = entry.time; + hash = entry.rhash(); + conf = this.chain.height - height + 1; + } + + const vin = []; + + for (const input of tx.inputs) { + const json = { + coinbase: undefined, + txid: undefined, + scriptSig: undefined, + txinwitness: undefined, + sequence: input.sequence + }; + + if (tx.isCoinbase()) { + json.coinbase = input.script.toJSON(); + } else { + json.txid = input.prevout.txid(); + json.vout = input.prevout.index; + json.scriptSig = { + asm: input.script.toASM(), + hex: input.script.toJSON() + }; + } + + if (input.witness.items.length > 0) { + json.txinwitness = input.witness.items.map((item) => { + return item.toString('hex'); + }); + } + + vin.push(json); + } + + const vout = []; + + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + vout.push({ + value: Amount.btc(output.value, true), + n: i, + scriptPubKey: this.scriptToJSON(output.script, true) }); } - vin.push(json); + return { + txid: tx.txid(), + hash: tx.wtxid(), + size: tx.getSize(), + vsize: tx.getVirtualSize(), + version: tx.version, + locktime: tx.locktime, + vin: vin, + vout: vout, + blockhash: hash, + confirmations: conf, + time: time, + blocktime: time, + hex: undefined + }; } - const vout = []; + scriptToJSON(script, hex) { + const type = script.getType(); - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - vout.push({ - value: Amount.btc(output.value, true), - n: i, - scriptPubKey: this.scriptToJSON(output.script, true) - }); - } + const json = { + asm: script.toASM(), + hex: undefined, + type: Script.typesByVal[type], + reqSigs: 1, + addresses: [], + p2sh: undefined + }; - return { - txid: tx.txid(), - hash: tx.wtxid(), - size: tx.getSize(), - vsize: tx.getVirtualSize(), - version: tx.version, - locktime: tx.locktime, - vin: vin, - vout: vout, - blockhash: hash, - confirmations: conf, - time: time, - blocktime: time, - hex: undefined - }; -}; + if (hex) + json.hex = script.toJSON(); -RPC.prototype.scriptToJSON = function scriptToJSON(script, hex) { - const type = script.getType(); + const [m] = script.getMultisig(); - const json = { - asm: script.toASM(), - hex: undefined, - type: Script.typesByVal[type], - reqSigs: 1, - addresses: [], - p2sh: undefined - }; + if (m !== -1) + json.reqSigs = m; - if (hex) - json.hex = script.toJSON(); + const addr = script.getAddress(); - const [m] = script.getMultisig(); - - if (m !== -1) - json.reqSigs = m; - - const addr = script.getAddress(); - - if (addr) { - const str = addr.toString(this.network); - json.addresses.push(str); - } - - return json; -}; - -RPC.prototype.headerToJSON = async function headerToJSON(entry) { - const mtp = await this.chain.getMedianTime(entry); - const next = await this.chain.getNextHash(entry.hash); - - return { - hash: entry.rhash(), - confirmations: this.chain.height - entry.height + 1, - height: entry.height, - version: entry.version, - versionHex: util.hex32(entry.version), - merkleroot: util.revHex(entry.merkleRoot), - time: entry.time, - mediantime: mtp, - bits: entry.bits, - difficulty: toDifficulty(entry.bits), - chainwork: entry.chainwork.toString('hex', 64), - previousblockhash: entry.prevBlock !== encoding.NULL_HASH - ? util.revHex(entry.prevBlock) - : null, - nextblockhash: next ? util.revHex(next) : null - }; -}; - -RPC.prototype.blockToJSON = async function blockToJSON(entry, block, details) { - const mtp = await this.chain.getMedianTime(entry); - const next = await this.chain.getNextHash(entry.hash); - const txs = []; - - for (const tx of block.txs) { - if (details) { - const json = this.txToJSON(tx, entry); - txs.push(json); - continue; + if (addr) { + const str = addr.toString(this.network); + json.addresses.push(str); } - txs.push(tx.txid()); + + return json; } - return { - hash: entry.rhash(), - confirmations: this.chain.height - entry.height + 1, - strippedsize: block.getBaseSize(), - size: block.getSize(), - weight: block.getWeight(), - height: entry.height, - version: entry.version, - versionHex: util.hex32(entry.version), - merkleroot: util.revHex(entry.merkleRoot), - coinbase: block.txs[0].inputs[0].script.toJSON(), - tx: txs, - time: entry.time, - mediantime: mtp, - bits: entry.bits, - difficulty: toDifficulty(entry.bits), - chainwork: entry.chainwork.toString('hex', 64), - previousblockhash: entry.prevBlock !== encoding.NULL_HASH - ? util.revHex(entry.prevBlock) - : null, - nextblockhash: next ? util.revHex(next) : null - }; -}; + async headerToJSON(entry) { + const mtp = await this.chain.getMedianTime(entry); + const next = await this.chain.getNextHash(entry.hash); -RPC.prototype.entryToJSON = function entryToJSON(entry) { - return { - size: entry.size, - fee: Amount.btc(entry.deltaFee, true), - modifiedfee: 0, - time: entry.time, - height: entry.height, - startingpriority: entry.priority, - currentpriority: entry.getPriority(this.chain.height), - descendantcount: this.mempool.countDescendants(entry), - descendantsize: entry.descSize, - descendantfees: entry.descFee, - ancestorcount: this.mempool.countAncestors(entry), - ancestorsize: 0, - ancestorfees: 0, - depends: this.mempool.getDepends(entry.tx).map(util.revHex) - }; -}; + return { + hash: entry.rhash(), + confirmations: this.chain.height - entry.height + 1, + height: entry.height, + version: entry.version, + versionHex: util.hex32(entry.version), + merkleroot: util.revHex(entry.merkleRoot), + time: entry.time, + mediantime: mtp, + bits: entry.bits, + difficulty: toDifficulty(entry.bits), + chainwork: entry.chainwork.toString('hex', 64), + previousblockhash: entry.prevBlock !== encoding.NULL_HASH + ? util.revHex(entry.prevBlock) + : null, + nextblockhash: next ? util.revHex(next) : null + }; + } + + async blockToJSON(entry, block, details) { + const mtp = await this.chain.getMedianTime(entry); + const next = await this.chain.getNextHash(entry.hash); + const txs = []; + + for (const tx of block.txs) { + if (details) { + const json = this.txToJSON(tx, entry); + txs.push(json); + continue; + } + txs.push(tx.txid()); + } + + return { + hash: entry.rhash(), + confirmations: this.chain.height - entry.height + 1, + strippedsize: block.getBaseSize(), + size: block.getSize(), + weight: block.getWeight(), + height: entry.height, + version: entry.version, + versionHex: util.hex32(entry.version), + merkleroot: util.revHex(entry.merkleRoot), + coinbase: block.txs[0].inputs[0].script.toJSON(), + tx: txs, + time: entry.time, + mediantime: mtp, + bits: entry.bits, + difficulty: toDifficulty(entry.bits), + chainwork: entry.chainwork.toString('hex', 64), + previousblockhash: entry.prevBlock !== encoding.NULL_HASH + ? util.revHex(entry.prevBlock) + : null, + nextblockhash: next ? util.revHex(next) : null + }; + } + + entryToJSON(entry) { + return { + size: entry.size, + fee: Amount.btc(entry.deltaFee, true), + modifiedfee: 0, + time: entry.time, + height: entry.height, + startingpriority: entry.priority, + currentpriority: entry.getPriority(this.chain.height), + descendantcount: this.mempool.countDescendants(entry), + descendantsize: entry.descSize, + descendantfees: entry.descFee, + ancestorcount: this.mempool.countAncestors(entry), + ancestorsize: 0, + ancestorfees: 0, + depends: this.mempool.getDepends(entry.tx).map(util.revHex) + }; + } +} /* * Helpers diff --git a/lib/http/rpcbase.js b/lib/http/rpcbase.js deleted file mode 100644 index 98009e9d..00000000 --- a/lib/http/rpcbase.js +++ /dev/null @@ -1,308 +0,0 @@ -/*! - * rpcbase.js - json rpc for bcoin. - * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const assert = require('assert'); -const EventEmitter = require('events'); - -/** - * JSON RPC - * @alias module:http.RPCBase - * @constructor - */ - -function RPCBase() { - if (!(this instanceof RPCBase)) - return new RPCBase(); - - EventEmitter.call(this); - - this.calls = Object.create(null); - this.mounts = []; -} - -Object.setPrototypeOf(RPCBase.prototype, EventEmitter.prototype); - -/** - * RPC errors. - * @enum {Number} - * @default - */ - -RPCBase.errors = { - // Standard JSON-RPC 2.0 errors - INVALID_REQUEST: -32600, - METHOD_NOT_FOUND: -32601, - INVALID_PARAMS: -32602, - INTERNAL_ERROR: -32603, - PARSE_ERROR: -32700, - - // General application defined errors - MISC_ERROR: -1, - FORBIDDEN_BY_SAFE_MODE: -2, - TYPE_ERROR: -3, - INVALID_ADDRESS_OR_KEY: -5, - OUT_OF_MEMORY: -7, - INVALID_PARAMETER: -8, - DATABASE_ERROR: -20, - DESERIALIZATION_ERROR: -22, - VERIFY_ERROR: -25, - VERIFY_REJECTED: -26, - VERIFY_ALREADY_IN_CHAIN: -27, - IN_WARMUP: -28, - - // Aliases for backward compatibility - TRANSACTION_ERROR: -25, - TRANSACTION_REJECTED: -26, - TRANSACTION_ALREADY_IN_CHAIN: -27, - - // P2P client errors - CLIENT_NOT_CONNECTED: -9, - CLIENT_IN_INITIAL_DOWNLOAD: -10, - CLIENT_NODE_ALREADY_ADDED: -23, - CLIENT_NODE_NOT_ADDED: -24, - CLIENT_NODE_NOT_CONNECTED: -29, - CLIENT_INVALID_IP_OR_SUBNET: -30, - CLIENT_P2P_DISABLED: -31, - - // Wallet errors - WALLET_ERROR: -4, - WALLET_INSUFFICIENT_FUNDS: -6, - WALLET_INVALID_ACCOUNT_NAME: -11, - WALLET_KEYPOOL_RAN_OUT: -12, - WALLET_UNLOCK_NEEDED: -13, - WALLET_PASSPHRASE_INCORRECT: -14, - WALLET_WRONG_ENC_STATE: -15, - WALLET_ENCRYPTION_FAILED: -16, - WALLET_ALREADY_UNLOCKED: -17 -}; - -/** - * Magic string for signing. - * @const {String} - * @default - */ - -RPCBase.MAGIC_STRING = 'Bitcoin Signed Message:\n'; - -/** - * Execute batched RPC calls. - * @param {Object|Object[]} body - * @param {Object} query - * @returns {Promise} - */ - -RPCBase.prototype.call = async function call(body, query) { - let cmds = body; - let out = []; - let array = true; - - if (!query) - query = {}; - - if (!Array.isArray(cmds)) { - cmds = [cmds]; - array = false; - } - - for (const cmd of cmds) { - if (!cmd || typeof cmd !== 'object') { - out.push({ - result: null, - error: { - message: 'Invalid request.', - code: RPCBase.errors.INVALID_REQUEST - }, - id: null - }); - continue; - } - - if (cmd.id && typeof cmd.id === 'object') { - out.push({ - result: null, - error: { - message: 'Invalid ID.', - code: RPCBase.errors.INVALID_REQUEST - }, - id: null - }); - continue; - } - - if (cmd.id == null) - cmd.id = null; - - if (!cmd.params) - cmd.params = []; - - if (typeof cmd.method !== 'string') { - out.push({ - result: null, - error: { - message: 'Method not found.', - code: RPCBase.errors.METHOD_NOT_FOUND - }, - id: cmd.id - }); - continue; - } - - if (!Array.isArray(cmd.params)) { - out.push({ - result: null, - error: { - message: 'Invalid params.', - code: RPCBase.errors.INVALID_PARAMS - }, - id: cmd.id - }); - continue; - } - - this.emit('call', cmd, query); - - let result; - try { - result = await this.execute(cmd); - } catch (err) { - let code; - - switch (err.type) { - case 'RPCError': - code = err.code; - break; - case 'ValidationError': - code = RPCBase.errors.TYPE_ERROR; - break; - case 'EncodingError': - code = RPCBase.errors.DESERIALIZATION_ERROR; - break; - case 'FundingError': - code = RPCBase.errors.WALLET_INSUFFICIENT_FUNDS; - break; - default: - code = RPCBase.errors.INTERNAL_ERROR; - this.emit('error', err); - break; - } - - out.push({ - result: null, - error: { - message: err.message, - code: code - }, - id: cmd.id - }); - - continue; - } - - if (result === undefined) - result = null; - - out.push({ - result: result, - error: null, - id: cmd.id - }); - } - - if (!array) - out = out[0]; - - return out; -}; - -/** - * Execute an RPC call. - * @private - * @param {Object} json - * @param {Boolean} help - * @returns {Promise} - */ - -RPCBase.prototype.execute = async function execute(json, help) { - const func = this.calls[json.method]; - - if (!func) { - for (const mount of this.mounts) { - if (mount.calls[json.method]) - return await mount.execute(json, help); - } - throw new RPCError(RPCBase.errors.METHOD_NOT_FOUND, - `Method not found: ${json.method}.`); - } - - return func.call(this, json.params, help); -}; - -/** - * Add a custom RPC call. - * @param {String} name - * @param {Function} func - */ - -RPCBase.prototype.add = function add(name, func) { - assert(typeof func === 'function', 'Handler must be a function.'); - assert(!this.calls[name], 'Duplicate RPC call.'); - this.calls[name] = func; -}; - -/** - * Mount another RPC object. - * @param {Object} rpc - */ - -RPCBase.prototype.mount = function mount(rpc) { - assert(rpc, 'RPC must be an object.'); - assert(typeof rpc.execute === 'function', 'Execute must be a method.'); - this.mounts.push(rpc); -}; - -/** - * Attach to another RPC object. - * @param {Object} rpc - */ - -RPCBase.prototype.attach = function attach(rpc) { - assert(rpc, 'RPC must be an object.'); - assert(typeof rpc.execute === 'function', 'Execute must be a method.'); - rpc.mount(this); -}; - -/** - * RPC Error - * @constructor - * @ignore - */ - -function RPCError(code, msg) { - Error.call(this); - - assert(typeof code === 'number'); - assert(typeof msg === 'string'); - - this.type = 'RPCError'; - this.message = msg; - this.code = code; - - if (Error.captureStackTrace) - Error.captureStackTrace(this, RPCError); -} - -Object.setPrototypeOf(RPCError.prototype, Error.prototype); - -/* - * Expose - */ - -exports = RPCBase; -exports.RPCError = RPCError; - -module.exports = exports; diff --git a/lib/http/rpcclient-browser.js b/lib/http/rpcclient-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/http/rpcclient-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/http/rpcclient.js b/lib/http/rpcclient.js deleted file mode 100644 index 0e44e6ae..00000000 --- a/lib/http/rpcclient.js +++ /dev/null @@ -1,101 +0,0 @@ -/*! - * rpcclient.js - json rpc client for bcoin - * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const Network = require('../protocol/network'); -const request = require('./request'); - -/** - * Bcoin RPC client. - * @alias module:http.RPCClient - * @constructor - * @param {String} uri - * @param {Object?} options - */ - -function RPCClient(options) { - if (!(this instanceof RPCClient)) - return new RPCClient(options); - - if (!options) - options = {}; - - if (typeof options === 'string') - options = { uri: options }; - - this.options = options; - this.network = Network.get(options.network); - - this.uri = options.uri || `http://localhost:${this.network.rpcPort}`; - this.apiKey = options.apiKey; - this.id = 0; -} - -/** - * Make a json rpc request. - * @private - * @param {String} method - RPC method name. - * @param {Array} params - RPC parameters. - * @returns {Promise} - Returns Object?. - */ - -RPCClient.prototype.execute = async function execute(method, params) { - const res = await request({ - method: 'POST', - uri: this.uri, - pool: true, - json: { - method: method, - params: params, - id: this.id++ - }, - auth: { - username: 'bitcoinrpc', - password: this.apiKey || '' - } - }); - - if (res.statusCode === 401) - throw new RPCError('Unauthorized (bad API key).', -1); - - if (res.type !== 'json') - throw new Error('Bad response (wrong content-type).'); - - if (!res.body) - throw new Error('No body for JSON-RPC response.'); - - if (res.body.error) - throw new RPCError(res.body.error.message, res.body.error.code); - - if (res.statusCode !== 200) - throw new Error(`Status code: ${res.statusCode}.`); - - return res.body.result; -}; - -/* - * Helpers - */ - -function RPCError(msg, code) { - Error.call(this); - - this.type = 'RPCError'; - this.message = String(msg); - this.code = code >>> 0; - - if (Error.captureStackTrace) - Error.captureStackTrace(this, RPCError); -} - -Object.setPrototypeOf(RPCError.prototype, Error.prototype); - -/* - * Expose - */ - -module.exports = RPCClient; diff --git a/lib/http/server-browser.js b/lib/http/server-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/http/server-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/http/server.js b/lib/http/server.js index 25913833..194177cf 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -9,7 +9,7 @@ const assert = require('assert'); const path = require('path'); -const HTTPBase = require('./base'); +const {Server} = require('bweb'); const util = require('../utils/util'); const base58 = require('../utils/base58'); const Bloom = require('../utils/bloom'); @@ -22,791 +22,751 @@ const Network = require('../protocol/network'); const Validator = require('../utils/validator'); const pkg = require('../pkg'); -/** - * HTTPServer - * @alias module:http.Server - * @constructor - * @param {Object} options - * @param {Fullnode} options.node - * @see HTTPBase - * @emits HTTPServer#socket - */ +class HTTPServer extends Server { + /** + * HTTPServer + * @alias module:http.Server + * @constructor + * @param {Object} options + * @param {Fullnode} options.node + * @see HTTPBase + * @emits HTTPServer#socket + */ -function HTTPServer(options) { - if (!(this instanceof HTTPServer)) - return new HTTPServer(options); + constructor(options) { + super(new HTTPOptions(options)); - options = new HTTPOptions(options); + this.network = this.options.network; + this.logger = this.options.logger.context('http'); + this.node = this.options.node; - HTTPBase.call(this, options); + this.chain = this.node.chain; + this.mempool = this.node.mempool; + this.pool = this.node.pool; + this.fees = this.node.fees; + this.miner = this.node.miner; + this.rpc = this.node.rpc; - this.options = options; - this.network = this.options.network; - this.logger = this.options.logger.context('http'); - this.node = this.options.node; - - this.chain = this.node.chain; - this.mempool = this.node.mempool; - this.pool = this.node.pool; - this.fees = this.node.fees; - this.miner = this.node.miner; - this.rpc = this.node.rpc; - - this.init(); -} - -Object.setPrototypeOf(HTTPServer.prototype, HTTPBase.prototype); - -/** - * Initialize routes. - * @private - */ - -HTTPServer.prototype.init = function init() { - this.on('request', (req, res) => { - if (req.method === 'POST' && req.pathname === '/') - return; - - this.logger.debug('Request for method=%s path=%s (%s).', - req.method, req.pathname, req.socket.remoteAddress); - }); - - this.on('listening', (address) => { - this.logger.info('Node HTTP server listening on %s (port=%d).', - address.address, address.port); - }); - - this.initRouter(); - this.initSockets(); -}; - -/** - * Initialize routes. - * @private - */ - -HTTPServer.prototype.initRouter = function initRouter() { - this.use(this.cors()); - - if (!this.options.noAuth) { - this.use(this.basicAuth({ - password: this.options.apiKey, - realm: 'node' - })); + this.init(); } - this.use(this.bodyParser({ - contentType: 'json' - })); + /** + * Initialize routes. + * @private + */ - this.use(this.jsonRPC(this.rpc)); + init() { + this.on('request', (req, res) => { + if (req.method === 'POST' && req.pathname === '/') + return; - this.get('/', async (req, res) => { - const totalTX = this.mempool ? this.mempool.map.size : 0; - const size = this.mempool ? this.mempool.getSize() : 0; - let addr = this.pool.hosts.getLocal(); - - if (!addr) - addr = this.pool.hosts.address; - - res.send(200, { - version: pkg.version, - network: this.network.type, - chain: { - height: this.chain.height, - tip: this.chain.tip.rhash(), - progress: this.chain.getProgress() - }, - pool: { - host: addr.host, - port: addr.port, - agent: this.pool.options.agent, - services: this.pool.options.services.toString(2), - outbound: this.pool.peers.outbound, - inbound: this.pool.peers.inbound - }, - mempool: { - tx: totalTX, - size: size - }, - time: { - uptime: this.node.uptime(), - system: util.now(), - adjusted: this.network.now(), - offset: this.network.time.offset - }, - memory: util.memoryUsage() + this.logger.debug('Request for method=%s path=%s (%s).', + req.method, req.pathname, req.socket.remoteAddress); }); - }); - // UTXO by address - this.get('/coin/address/:address', async (req, res) => { - const valid = Validator.fromRequest(req); - const address = valid.str('address'); + this.on('listening', (address) => { + this.logger.info('Node HTTP server listening on %s (port=%d).', + address.address, address.port); + }); - enforce(address, 'Address is required.'); - enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); + this.initRouter(); + this.initSockets(); + } - const coins = await this.node.getCoinsByAddress(address); - const result = []; + /** + * Initialize routes. + * @private + */ - for (const coin of coins) - result.push(coin.getJSON(this.network)); - - res.send(200, result); - }); - - // UTXO by id - this.get('/coin/:hash/:index', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - const index = valid.u32('index'); - - enforce(hash, 'Hash is required.'); - enforce(index != null, 'Index is required.'); - enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); - - const coin = await this.node.getCoin(hash, index); - - if (!coin) { - res.send(404); - return; - } - - res.send(200, coin.getJSON(this.network)); - }); - - // Bulk read UTXOs - this.post('/coin/address', async (req, res) => { - const valid = Validator.fromRequest(req); - const address = valid.array('addresses'); - - enforce(address, 'Address is required.'); - enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); - - const coins = await this.node.getCoinsByAddress(address); - const result = []; - - for (const coin of coins) - result.push(coin.getJSON(this.network)); - - res.send(200, result); - }); - - // TX by hash - this.get('/tx/:hash', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - - enforce(hash, 'Hash is required.'); - enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); - - const meta = await this.node.getMeta(hash); - - if (!meta) { - res.send(404); - return; - } - - const view = await this.node.getMetaView(meta); - - res.send(200, meta.getJSON(this.network, view, this.chain.height)); - }); - - // TX by address - this.get('/tx/address/:address', async (req, res) => { - const valid = Validator.fromRequest(req); - const address = valid.str('address'); - - enforce(address, 'Address is required.'); - enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); - - const metas = await this.node.getMetaByAddress(address); - const result = []; - - for (const meta of metas) { - const view = await this.node.getMetaView(meta); - result.push(meta.getJSON(this.network, view, this.chain.height)); - } - - res.send(200, result); - }); - - // Bulk read TXs - this.post('/tx/address', async (req, res) => { - const valid = Validator.fromRequest(req); - const address = valid.array('addresses'); - - enforce(address, 'Address is required.'); - enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); - - const metas = await this.node.getMetaByAddress(address); - const result = []; - - for (const meta of metas) { - const view = await this.node.getMetaView(meta); - result.push(meta.getJSON(this.network, view, this.chain.height)); - } - - res.send(200, result); - }); - - // Block by hash/height - this.get('/block/:block', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.uintrhash('block'); - - enforce(hash != null, 'Hash or height required.'); - enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.'); - - const block = await this.chain.getBlock(hash); - - if (!block) { - res.send(404); - return; - } - - const view = await this.chain.getBlockView(block); - - if (!view) { - res.send(404); - return; - } - - const height = await this.chain.getHeight(hash); - - res.send(200, block.getJSON(this.network, view, height, confirmations)); - }); - - // Mempool snapshot - this.get('/mempool', async (req, res) => { - enforce(this.mempool, 'No mempool available.'); - - const hashes = this.mempool.getSnapshot(); - const result = []; - - for (const hash of hashes) - result.push(util.revHex(hash)); - - res.send(200, result); - }); - - // Broadcast TX - this.post('/broadcast', async (req, res) => { - const valid = Validator.fromRequest(req); - const raw = valid.buf('tx'); - - enforce(raw, 'TX is required.'); - - const tx = TX.fromRaw(raw); - - await this.node.sendTX(tx); - - res.send(200, { success: true }); - }); - - // Estimate fee - this.get('/fee', async (req, res) => { - const valid = Validator.fromRequest(req); - const blocks = valid.u32('blocks'); - - if (!this.fees) { - res.send(200, { rate: this.network.feeRate }); - return; - } - - const fee = this.fees.estimateFee(blocks); - - res.send(200, { rate: fee }); - }); - - // Reset chain - this.post('/reset', async (req, res) => { - const valid = Validator.fromRequest(req); - const height = valid.u32('height'); - - enforce(height != null, 'Height is required.'); - - await this.chain.reset(height); - - res.send(200, { success: true }); - }); -}; - -/** - * Initialize websockets. - * @private - */ - -HTTPServer.prototype.initSockets = function initSockets() { - if (!this.io) - return; - - this.on('socket', (socket) => { - this.handleSocket(socket); - }); -}; - -/** - * Handle new websocket. - * @private - * @param {WebSocket} socket - */ - -HTTPServer.prototype.handleSocket = function handleSocket(socket) { - socket.hook('auth', (args) => { - if (socket.auth) - throw new Error('Already authed.'); + initRouter() { + this.use(this.cors()); if (!this.options.noAuth) { - const valid = new Validator(args); - const key = valid.str(0, ''); - - if (key.length > 255) - throw new Error('Invalid API key.'); - - const data = Buffer.from(key, 'ascii'); - const hash = digest.hash256(data); - - if (!ccmp(hash, this.options.apiHash)) - throw new Error('Invalid API key.'); + this.use(this.basicAuth({ + password: this.options.apiKey, + realm: 'node' + })); } - socket.auth = true; + this.use(this.bodyParser({ + type: 'json' + })); - this.logger.info('Successful auth from %s.', socket.remoteAddress); - this.handleAuth(socket); + this.use(this.jsonRPC()); + this.use(this.router()); - return null; - }); + this.get('/', async (req, res) => { + const totalTX = this.mempool ? this.mempool.map.size : 0; + const size = this.mempool ? this.mempool.getSize() : 0; + let addr = this.pool.hosts.getLocal(); - socket.emit('version', { - version: pkg.version, - network: this.network.type - }); -}; + if (!addr) + addr = this.pool.hosts.address; -/** - * Handle new auth'd websocket. - * @private - * @param {WebSocket} socket - */ + res.json(200, { + version: pkg.version, + network: this.network.type, + chain: { + height: this.chain.height, + tip: this.chain.tip.rhash(), + progress: this.chain.getProgress() + }, + pool: { + host: addr.host, + port: addr.port, + agent: this.pool.options.agent, + services: this.pool.options.services.toString(2), + outbound: this.pool.peers.outbound, + inbound: this.pool.peers.inbound + }, + mempool: { + tx: totalTX, + size: size + }, + time: { + uptime: this.node.uptime(), + system: util.now(), + adjusted: this.network.now(), + offset: this.network.time.offset + }, + memory: util.memoryUsage() + }); + }); -HTTPServer.prototype.handleAuth = function handleAuth(socket) { - socket.hook('watch chain', (args) => { - socket.join('chain'); - return null; - }); + // UTXO by address + this.get('/coin/address/:address', async (req, res) => { + const valid = Validator.fromRequest(req); + const address = valid.str('address'); - socket.hook('unwatch chain', (args) => { - socket.leave('chain'); - return null; - }); + enforce(address, 'Address is required.'); + enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); - socket.hook('watch mempool', (args) => { - socket.join('mempool'); - return null; - }); + const coins = await this.node.getCoinsByAddress(address); + const result = []; - socket.hook('unwatch mempool', (args) => { - socket.leave('mempool'); - return null; - }); + for (const coin of coins) + result.push(coin.getJSON(this.network)); - socket.hook('set filter', (args) => { - const valid = new Validator(args); - const data = valid.buf(0); + res.json(200, result); + }); - if (!data) - throw new Error('Invalid parameter.'); + // UTXO by id + this.get('/coin/:hash/:index', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + const index = valid.u32('index'); - socket.filter = Bloom.fromRaw(data); + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); + enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); - return null; - }); + const coin = await this.node.getCoin(hash, index); - socket.hook('get tip', (args) => { - return this.chain.tip.toRaw(); - }); + if (!coin) { + res.send(404); + return; + } - socket.hook('get entry', async (args) => { - const valid = new Validator(args); - const block = valid.uintrhash(0); + res.json(200, coin.getJSON(this.network)); + }); - if (block == null) - throw new Error('Invalid parameter.'); + // Bulk read UTXOs + this.post('/coin/address', async (req, res) => { + const valid = Validator.fromRequest(req); + const address = valid.array('addresses'); - const entry = await this.chain.getEntry(block); + enforce(address, 'Address is required.'); + enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); + + const coins = await this.node.getCoinsByAddress(address); + const result = []; + + for (const coin of coins) + result.push(coin.getJSON(this.network)); + + res.json(200, result); + }); + + // TX by hash + this.get('/tx/:hash', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + + enforce(hash, 'Hash is required.'); + enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); + + const meta = await this.node.getMeta(hash); + + if (!meta) { + res.send(404); + return; + } + + const view = await this.node.getMetaView(meta); + + res.json(200, meta.getJSON(this.network, view, this.chain.height)); + }); + + // TX by address + this.get('/tx/address/:address', async (req, res) => { + const valid = Validator.fromRequest(req); + const address = valid.str('address'); + + enforce(address, 'Address is required.'); + enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); + + const metas = await this.node.getMetaByAddress(address); + const result = []; + + for (const meta of metas) { + const view = await this.node.getMetaView(meta); + result.push(meta.getJSON(this.network, view, this.chain.height)); + } + + res.json(200, result); + }); + + // Bulk read TXs + this.post('/tx/address', async (req, res) => { + const valid = Validator.fromRequest(req); + const address = valid.array('addresses'); + + enforce(address, 'Address is required.'); + enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); + + const metas = await this.node.getMetaByAddress(address); + const result = []; + + for (const meta of metas) { + const view = await this.node.getMetaView(meta); + result.push(meta.getJSON(this.network, view, this.chain.height)); + } + + res.json(200, result); + }); + + // Block by hash/height + this.get('/block/:block', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.uintrhash('block'); + + enforce(hash != null, 'Hash or height required.'); + enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.'); + + const block = await this.chain.getBlock(hash); + + if (!block) { + res.send(404); + return; + } + + const view = await this.chain.getBlockView(block); + + if (!view) { + res.send(404); + return; + } + + const height = await this.chain.getHeight(hash); + const depth = this.chain.height - height + 1; + + res.json(200, block.getJSON(this.network, view, height, depth)); + }); + + // Mempool snapshot + this.get('/mempool', async (req, res) => { + enforce(this.mempool, 'No mempool available.'); + + const hashes = this.mempool.getSnapshot(); + const result = []; + + for (const hash of hashes) + result.push(util.revHex(hash)); + + res.json(200, result); + }); + + // Broadcast TX + this.post('/broadcast', async (req, res) => { + const valid = Validator.fromRequest(req); + const raw = valid.buf('tx'); + + enforce(raw, 'TX is required.'); + + const tx = TX.fromRaw(raw); + + await this.node.sendTX(tx); + + res.json(200, { success: true }); + }); + + // Estimate fee + this.get('/fee', async (req, res) => { + const valid = Validator.fromRequest(req); + const blocks = valid.u32('blocks'); + + if (!this.fees) { + res.send(200, { rate: this.network.feeRate }); + return; + } + + const fee = this.fees.estimateFee(blocks); + + res.json(200, { rate: fee }); + }); + + // Reset chain + this.post('/reset', async (req, res) => { + const valid = Validator.fromRequest(req); + const height = valid.u32('height'); + + enforce(height != null, 'Height is required.'); + + await this.chain.reset(height); + + res.json(200, { success: true }); + }); + } + + /** + * Handle new websocket. + * @private + * @param {WebSocket} socket + */ + + handleSocket(socket) { + socket.hook('auth', (...args) => { + if (socket.auth) + throw new Error('Already authed.'); + + if (!this.options.noAuth) { + const valid = new Validator(args); + const key = valid.str(0, ''); + + if (key.length > 255) + throw new Error('Invalid API key.'); + + const data = Buffer.from(key, 'ascii'); + const hash = digest.hash256(data); + + if (!ccmp(hash, this.options.apiHash)) + throw new Error('Invalid API key.'); + } + + socket.auth = true; + + this.logger.info('Successful auth from %s.', socket.remoteAddress); + this.handleAuth(socket); - if (!entry) return null; + }); - if (!await this.chain.isMainChain(entry)) + socket.fire('version', { + version: pkg.version, + network: this.network.type + }); + } + + /** + * Handle new auth'd websocket. + * @private + * @param {WebSocket} socket + */ + + handleAuth(socket) { + socket.hook('watch chain', () => { + socket.join('chain'); return null; + }); - return entry.toRaw(); - }); + socket.hook('unwatch chain', () => { + socket.leave('chain'); + return null; + }); - socket.hook('get hashes', async (args) => { - const valid = new Validator(args); - const start = valid.i32(0, -1); - const end = valid.i32(1, -1); + socket.hook('watch mempool', () => { + socket.join('mempool'); + return null; + }); - return await this.chain.getHashes(start, end); - }); + socket.hook('unwatch mempool', () => { + socket.leave('mempool'); + return null; + }); - socket.hook('add filter', (args) => { - const valid = new Validator(args); - const chunks = valid.array(0); - - if (!chunks) - throw new Error('Invalid parameter.'); - - if (!socket.filter) - throw new Error('No filter set.'); - - const items = new Validator(chunks); - - for (let i = 0; i < chunks.length; i++) { - const data = items.buf(i); + socket.hook('set filter', (...args) => { + const valid = new Validator(args); + const data = valid.buf(0); if (!data) - throw new Error('Bad data chunk.'); + throw new Error('Invalid parameter.'); - this.filter.add(data); + socket.filter = Bloom.fromRaw(data); - if (this.node.spv) - this.pool.watch(data); + return null; + }); + + socket.hook('get tip', () => { + return this.chain.tip.toRaw(); + }); + + socket.hook('get entry', async (...args) => { + const valid = new Validator(args); + const block = valid.uintrhash(0); + + if (block == null) + throw new Error('Invalid parameter.'); + + const entry = await this.chain.getEntry(block); + + if (!entry) + return null; + + if (!await this.chain.isMainChain(entry)) + return null; + + return entry.toRaw(); + }); + + socket.hook('get hashes', async (...args) => { + const valid = new Validator(args); + const start = valid.i32(0, -1); + const end = valid.i32(1, -1); + + return await this.chain.getHashes(start, end); + }); + + socket.hook('add filter', (...args) => { + const valid = new Validator(args); + const chunks = valid.array(0); + + if (!chunks) + throw new Error('Invalid parameter.'); + + if (!socket.filter) + throw new Error('No filter set.'); + + const items = new Validator(chunks); + + for (let i = 0; i < chunks.length; i++) { + const data = items.buf(i); + + if (!data) + throw new Error('Bad data chunk.'); + + this.filter.add(data); + + if (this.node.spv) + this.pool.watch(data); + } + + return null; + }); + + socket.hook('reset filter', () => { + socket.filter = null; + return null; + }); + + socket.hook('estimate fee', (...args) => { + const valid = new Validator(args); + const blocks = valid.u32(0); + + if (!this.fees) + return this.network.feeRate; + + return this.fees.estimateFee(blocks); + }); + + socket.hook('send', (...args) => { + const valid = new Validator(args); + const data = valid.buf(0); + + if (!data) + throw new Error('Invalid parameter.'); + + const tx = TX.fromRaw(data); + + this.node.send(tx); + + return null; + }); + + socket.hook('rescan', (...args) => { + const valid = new Validator(args); + const start = valid.uintrhash(0); + + if (start == null) + throw new Error('Invalid parameter.'); + + return this.scan(socket, start); + }); + } + + /** + * Bind to chain events. + * @private + */ + + initSockets() { + const pool = this.mempool || this.pool; + + this.chain.on('connect', (entry, block, view) => { + const sockets = this.channel('chain'); + + if (!sockets) + return; + + const raw = entry.toRaw(); + + this.to('chain', 'chain connect', raw); + + for (const socket of sockets) { + const txs = this.filterBlock(socket, block); + socket.fire('block connect', raw, txs); + } + }); + + this.chain.on('disconnect', (entry, block, view) => { + const sockets = this.channel('chain'); + + if (!sockets) + return; + + const raw = entry.toRaw(); + + this.to('chain', 'chain disconnect', raw); + this.to('chain', 'block disconnect', raw); + }); + + this.chain.on('reset', (tip) => { + const sockets = this.channel('chain'); + + if (!sockets) + return; + + this.to('chain', 'chain reset', tip.toRaw()); + }); + + pool.on('tx', (tx) => { + const sockets = this.channel('mempool'); + + if (!sockets) + return; + + const raw = tx.toRaw(); + + for (const socket of sockets) { + if (!this.filterTX(socket, tx)) + continue; + + socket.fire('tx', raw); + } + }); + } + + /** + * Filter block by socket. + * @private + * @param {WebSocket} socket + * @param {Block} block + * @returns {TX[]} + */ + + filterBlock(socket, block) { + if (!socket.filter) + return []; + + const txs = []; + + for (const tx of block.txs) { + if (this.filterTX(socket, tx)) + txs.push(tx.toRaw()); } - return null; - }); + return txs; + } - socket.hook('reset filter', (args) => { - socket.filter = null; - return null; - }); + /** + * Filter transaction by socket. + * @private + * @param {WebSocket} socket + * @param {TX} tx + * @returns {Boolean} + */ - socket.hook('estimate fee', (args) => { - const valid = new Validator(args); - const blocks = valid.u32(0); + filterTX(socket, tx) { + if (!socket.filter) + return false; - if (!this.fees) - return this.network.feeRate; + let found = false; - return this.fees.estimateFee(blocks); - }); + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i]; + const hash = output.getHash(); - socket.hook('send', (args) => { - const valid = new Validator(args); - const data = valid.buf(0); - - if (!data) - throw new Error('Invalid parameter.'); - - const tx = TX.fromRaw(data); - - this.node.send(tx); - - return null; - }); - - socket.hook('rescan', (args) => { - const valid = new Validator(args); - const start = valid.uintrhash(0); - - if (start == null) - throw new Error('Invalid parameter.'); - - return this.scan(socket, start); - }); - - this.bindChain(); -}; - -/** - * Bind to chain events. - * @private - */ - -HTTPServer.prototype.bindChain = function bindChain() { - const pool = this.mempool || this.pool; - - this.chain.on('connect', (entry, block, view) => { - const list = this.channel('chain'); - - if (!list) - return; - - const raw = entry.toRaw(); - - this.to('chain', 'chain connect', raw); - - for (let item = list.head; item; item = item.next) { - const socket = item.value; - const txs = this.filterBlock(socket, block); - socket.emit('block connect', raw, txs); - } - }); - - this.chain.on('disconnect', (entry, block, view) => { - const list = this.channel('chain'); - - if (!list) - return; - - const raw = entry.toRaw(); - - this.to('chain', 'chain disconnect', raw); - this.to('chain', 'block disconnect', raw); - }); - - this.chain.on('reset', (tip) => { - const list = this.channel('chain'); - - if (!list) - return; - - const raw = tip.toRaw(); - - this.to('chain', 'chain reset', raw); - }); - - pool.on('tx', (tx) => { - const list = this.channel('mempool'); - - if (!list) - return; - - const raw = tx.toRaw(); - - for (let item = list.head; item; item = item.next) { - const socket = item.value; - - if (!this.filterTX(socket, tx)) + if (!hash) continue; - socket.emit('tx', raw); + if (socket.filter.test(hash)) { + const prevout = Outpoint.fromTX(tx, i); + socket.filter.add(prevout.toRaw()); + found = true; + } } - }); -}; -/** - * Filter block by socket. - * @private - * @param {WebSocket} socket - * @param {Block} block - * @returns {TX[]} - */ + if (found) + return true; -HTTPServer.prototype.filterBlock = function filterBlock(socket, block) { - if (!socket.filter) - return []; + if (!tx.isCoinbase()) { + for (const {prevout} of tx.inputs) { + if (socket.filter.test(prevout.toRaw())) + return true; + } + } - const txs = []; - - for (const tx of block.txs) { - if (this.filterTX(socket, tx)) - txs.push(tx.toRaw()); - } - - return txs; -}; - -/** - * Filter transaction by socket. - * @private - * @param {WebSocket} socket - * @param {TX} tx - * @returns {Boolean} - */ - -HTTPServer.prototype.filterTX = function filterTX(socket, tx) { - if (!socket.filter) return false; - - let found = false; - - for (let i = 0; i < tx.outputs.length; i++) { - const output = tx.outputs[i]; - const hash = output.getHash(); - - if (!hash) - continue; - - if (socket.filter.test(hash)) { - const prevout = Outpoint.fromTX(tx, i); - socket.filter.add(prevout.toRaw()); - found = true; - } } - if (found) - return true; + /** + * Scan using a socket's filter. + * @private + * @param {WebSocket} socket + * @param {Hash} start + * @returns {Promise} + */ - if (!tx.isCoinbase()) { - for (const {prevout} of tx.inputs) { - if (socket.filter.test(prevout.toRaw())) - return true; - } + async scan(socket, start) { + await this.node.scan(start, socket.filter, (entry, txs) => { + const block = entry.toRaw(); + const raw = []; + + for (const tx of txs) + raw.push(tx.toRaw()); + + socket.fire('block rescan', block, raw); + }); + return null; } - - return false; -}; - -/** - * Scan using a socket's filter. - * @private - * @param {WebSocket} socket - * @param {Hash} start - * @returns {Promise} - */ - -HTTPServer.prototype.scan = async function scan(socket, start) { - const scanner = this.scanner.bind(this, socket); - await this.node.scan(start, socket.filter, scanner); - return null; -}; - -/** - * Handle rescan iteration. - * @private - * @param {WebSocket} socket - * @param {ChainEntry} entry - * @param {TX[]} txs - * @returns {Promise} - */ - -HTTPServer.prototype.scanner = function scanner(socket, entry, txs) { - const block = entry.toRaw(); - const raw = []; - - for (const tx of txs) - raw.push(tx.toRaw()); - - socket.emit('block rescan', block, raw); - - return Promise.resolve(); -}; - -/** - * HTTPOptions - * @alias module:http.HTTPOptions - * @constructor - * @param {Object} options - */ - -function HTTPOptions(options) { - if (!(this instanceof HTTPOptions)) - return new HTTPOptions(options); - - this.network = Network.primary; - this.logger = null; - this.node = null; - this.apiKey = base58.encode(random.randomBytes(20)); - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); - this.noAuth = false; - - this.prefix = null; - this.host = '127.0.0.1'; - this.port = 8080; - this.ssl = false; - this.keyFile = null; - this.certFile = null; - - this.fromOptions(options); } -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {HTTPOptions} - */ +class HTTPOptions { + /** + * HTTPOptions + * @alias module:http.HTTPOptions + * @constructor + * @param {Object} options + */ -HTTPOptions.prototype.fromOptions = function fromOptions(options) { - assert(options); - assert(options.node && typeof options.node === 'object', - 'HTTP Server requires a Node.'); - - this.node = options.node; - this.network = options.node.network; - this.logger = options.node.logger; - - this.port = this.network.rpcPort; - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.apiKey != null) { - assert(typeof options.apiKey === 'string', - 'API key must be a string.'); - assert(options.apiKey.length <= 255, - 'API key must be under 256 bytes.'); - assert(util.isAscii(options.apiKey), - 'API key must be ascii.'); - this.apiKey = options.apiKey; + constructor(options) { + this.network = Network.primary; + this.logger = null; + this.node = null; + this.apiKey = base58.encode(random.randomBytes(20)); this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); + this.noAuth = false; + + this.prefix = null; + this.host = '127.0.0.1'; + this.port = 8080; + this.ssl = false; + this.keyFile = null; + this.certFile = null; + + this.fromOptions(options); } - if (options.noAuth != null) { - assert(typeof options.noAuth === 'boolean'); - this.noAuth = options.noAuth; + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {HTTPOptions} + */ + + fromOptions(options) { + assert(options); + assert(options.node && typeof options.node === 'object', + 'HTTP Server requires a Node.'); + + this.node = options.node; + this.network = options.node.network; + this.logger = options.node.logger; + + this.port = this.network.rpcPort; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.apiKey != null) { + assert(typeof options.apiKey === 'string', + 'API key must be a string.'); + assert(options.apiKey.length <= 255, + 'API key must be under 256 bytes.'); + assert(util.isAscii(options.apiKey), + 'API key must be ascii.'); + this.apiKey = options.apiKey; + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); + } + + if (options.noAuth != null) { + assert(typeof options.noAuth === 'boolean'); + this.noAuth = options.noAuth; + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.keyFile = path.join(this.prefix, 'key.pem'); + this.certFile = path.join(this.prefix, 'cert.pem'); + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(util.isU16(options.port), 'Port must be a number.'); + this.port = options.port; + } + + if (options.ssl != null) { + assert(typeof options.ssl === 'boolean'); + this.ssl = options.ssl; + } + + if (options.keyFile != null) { + assert(typeof options.keyFile === 'string'); + this.keyFile = options.keyFile; + } + + if (options.certFile != null) { + assert(typeof options.certFile === 'string'); + this.certFile = options.certFile; + } + + // Allow no-auth implicitly + // if we're listening locally. + if (!options.apiKey) { + if (this.host === '127.0.0.1' || this.host === '::1') + this.noAuth = true; + } + + return this; } - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.keyFile = path.join(this.prefix, 'key.pem'); - this.certFile = path.join(this.prefix, 'cert.pem'); + /** + * Instantiate http options from object. + * @param {Object} options + * @returns {HTTPOptions} + */ + + static fromOptions(options) { + return new HTTPOptions().fromOptions(options); } - - if (options.host != null) { - assert(typeof options.host === 'string'); - this.host = options.host; - } - - if (options.port != null) { - assert(util.isU16(options.port), 'Port must be a number.'); - this.port = options.port; - } - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.keyFile != null) { - assert(typeof options.keyFile === 'string'); - this.keyFile = options.keyFile; - } - - if (options.certFile != null) { - assert(typeof options.certFile === 'string'); - this.certFile = options.certFile; - } - - // Allow no-auth implicitly - // if we're listening locally. - if (!options.apiKey) { - if (this.host === '127.0.0.1' || this.host === '::1') - this.noAuth = true; - } - - return this; -}; - -/** - * Instantiate http options from object. - * @param {Object} options - * @returns {HTTPOptions} - */ - -HTTPOptions.fromOptions = function fromOptions(options) { - return new HTTPOptions().fromOptions(options); -}; +} /* * Helpers diff --git a/lib/http/wallet-browser.js b/lib/http/wallet-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/http/wallet-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/http/wallet.js b/lib/http/wallet.js index 277f8158..5e08ff91 100644 --- a/lib/http/wallet.js +++ b/lib/http/wallet.js @@ -8,488 +8,585 @@ 'use strict'; const assert = require('assert'); -const EventEmitter = require('events'); -const Network = require('../protocol/network'); -const Client = require('./client'); +const {Client} = require('bcurl'); -/** - * HTTPWallet - * @alias module:http.Wallet - * @constructor - * @param {String} uri - */ +class HTTPWallet extends Client { + /** + * HTTPWallet + * @alias module:http.Wallet + * @constructor + * @param {String} uri + */ -function HTTPWallet(options) { - if (!(this instanceof HTTPWallet)) - return new HTTPWallet(options); - - EventEmitter.call(this); - - if (!options) - options = {}; - - if (typeof options === 'string') - options = { uri: options }; - - this.options = options; - this.network = Network.get(options.network); - - this.client = new Client(options); - this.uri = options.uri; - this.id = null; - this.token = null; - - if (options.id) - this.id = options.id; - - if (options.token) { - this.token = options.token; - if (Buffer.isBuffer(this.token)) - this.token = this.token.toString('hex'); - this.client.token = this.token; + constructor(options) { + super(options); } - this._init(); -} + /** + * Open the client, wait for socket to connect. + * @returns {Promise} + */ -Object.setPrototypeOf(HTTPWallet.prototype, EventEmitter.prototype); + async init() { + await super.open(); -/** - * Initialize the wallet. - * @private - */ + this.on('error', (err) => { + this.emit('error', err); + }); -HTTPWallet.prototype._init = function _init() { - this.client.on('tx', (details) => { - this.emit('tx', details); - }); + this.listen('wallet tx', (details) => { + this.emit('tx', details); + }); - this.client.on('confirmed', (details) => { - this.emit('confirmed', details); - }); + this.listen('wallet confirmed', (details) => { + this.emit('confirmed', details); + }); - this.client.on('unconfirmed', (tx, details) => { - this.emit('unconfirmed', details); - }); + this.listen('wallet unconfirmed', (details) => { + this.emit('unconfirmed', details); + }); - this.client.on('conflict', (tx, details) => { - this.emit('conflict', details); - }); + this.listen('wallet conflict', (details) => { + this.emit('conflict', details); + }); - this.client.on('balance', (balance) => { - this.emit('balance', balance); - }); + this.listen('wallet updated', (details) => { + this.emit('updated', details); + }); - this.client.on('address', (receive) => { - this.emit('address', receive); - }); + this.listen('wallet address', (receive) => { + this.emit('address', receive); + }); - this.client.on('error', (err) => { - this.emit('error', err); - }); -}; + this.listen('wallet balance', (balance) => { + this.emit('balance', balance); + }); + } -/** - * Open the client and get a wallet. - * @alias HTTPWallet#open - * @returns {Promise} - */ + /** + * Open the client and get a wallet. + * @returns {Promise} + */ -HTTPWallet.prototype.open = async function open(options) { - if (options) { - if (options.id) + async open(options = {}) { + if (options.id != null) { + assert(typeof options.id === 'string'); this.id = options.id; - - if (options.token) { - this.token = options.token; - if (Buffer.isBuffer(this.token)) - this.token = this.token.toString('hex'); - this.client.token = this.token; } + + if (options.token != null) { + assert(typeof options.token === 'string'); + this.token = options.token; + } + + if (!this.id) + throw new Error('No ID provided.'); + + await this.init(); + await this.call('wallet join', this.id, this.token); } - assert(this.id, 'No ID provided.'); - - await this.client.open(); - await this.client.sendWalletAuth(); - await this.client.join(this.id, this.token); -}; - -/** - * Open the client and create a wallet. - * @alias HTTPWallet#open - * @returns {Promise} - */ - -HTTPWallet.prototype.create = async function create(options) { - await this.client.open(); - await this.client.sendWalletAuth(); - - const wallet = await this.client.createWallet(options); - - this.id = wallet.id; - this.token = wallet.token; - this.client.token = this.token; - - await this.client.join(this.id, this.token); - - return wallet; -}; - -/** - * Close the client, wait for the socket to close. - * @alias HTTPWallet#close - * @returns {Promise} - */ - -HTTPWallet.prototype.close = function close() { - return this.client.close(); -}; - -/** - * Wait for websocket disconnection. - * @private - * @returns {Promise} - */ - -HTTPWallet.prototype.onDisconnect = function onDisconnect() { - return this.client.onDisconnect(); -}; - -/** - * @see Wallet#getHistory - */ - -HTTPWallet.prototype.getHistory = function getHistory(account) { - return this.client.getHistory(this.id, account); -}; - -/** - * @see Wallet#getCoins - */ - -HTTPWallet.prototype.getCoins = function getCoins(account) { - return this.client.getCoins(this.id, account); -}; - -/** - * @see Wallet#getPending - */ - -HTTPWallet.prototype.getPending = function getPending(account) { - return this.client.getPending(this.id, account); -}; - -/** - * @see Wallet#getBalance - */ - -HTTPWallet.prototype.getBalance = function getBalance(account) { - return this.client.getBalance(this.id, account); -}; - -/** - * @see Wallet#getLast - */ - -HTTPWallet.prototype.getLast = function getLast(account, limit) { - return this.client.getLast(this.id, account, limit); -}; - -/** - * @see Wallet#getRange - */ - -HTTPWallet.prototype.getRange = function getRange(account, options) { - return this.client.getRange(this.id, account, options); -}; - -/** - * @see Wallet#getTX - */ - -HTTPWallet.prototype.getTX = function getTX(hash) { - return this.client.getWalletTX(this.id, hash); -}; - -/** - * @see Wallet#getBlocks - */ - -HTTPWallet.prototype.getBlocks = function getBlocks() { - return this.client.getWalletBlocks(this.id); -}; - -/** - * @see Wallet#getBlock - */ - -HTTPWallet.prototype.getBlock = function getBlock(height) { - return this.client.getWalletBlock(this.id, height); -}; - -/** - * @see Wallet#getCoin - */ - -HTTPWallet.prototype.getCoin = function getCoin(hash, index) { - return this.client.getWalletCoin(this.id, hash, index); -}; - -/** - * @see Wallet#zap - */ - -HTTPWallet.prototype.zap = function zap(account, age) { - return this.client.zapWallet(this.id, account, age); -}; - -/** - * @see Wallet#createTX - */ - -HTTPWallet.prototype.createTX = function createTX(options) { - return this.client.createTX(this.id, options); -}; - -/** - * @see HTTPClient#walletSend - */ - -HTTPWallet.prototype.send = function send(options) { - return this.client.send(this.id, options); -}; - -/** - * @see Wallet#sign - */ - -HTTPWallet.prototype.sign = function sign(tx, options) { - return this.client.sign(this.id, tx, options); -}; - -/** - * @see HTTPClient#getWallet - */ - -HTTPWallet.prototype.getInfo = function getInfo() { - return this.client.getWallet(this.id); -}; - -/** - * @see Wallet#getAccounts - */ - -HTTPWallet.prototype.getAccounts = function getAccounts() { - return this.client.getAccounts(this.id); -}; - -/** - * @see Wallet#master - */ - -HTTPWallet.prototype.getMaster = function getMaster() { - return this.client.getMaster(this.id); -}; - -/** - * @see Wallet#getAccount - */ - -HTTPWallet.prototype.getAccount = function getAccount(account) { - return this.client.getAccount(this.id, account); -}; - -/** - * @see Wallet#createAccount - */ - -HTTPWallet.prototype.createAccount = function createAccount(name, options) { - return this.client.createAccount(this.id, name, options); -}; - -/** - * @see Wallet#createAddress - */ - -HTTPWallet.prototype.createAddress = function createAddress(account) { - return this.client.createAddress(this.id, account); -}; - -/** - * @see Wallet#createAddress - */ - -HTTPWallet.prototype.createChange = function createChange(account) { - return this.client.createChange(this.id, account); -}; - -/** - * @see Wallet#createAddress - */ - -HTTPWallet.prototype.createNested = function createNested(account) { - return this.client.createNested(this.id, account); -}; - -/** - * @see Wallet#setPassphrase - */ - -HTTPWallet.prototype.setPassphrase = function setPassphrase(passphrase, old) { - return this.client.setPassphrase(this.id, passphrase, old); -}; - -/** - * @see Wallet#retoken - */ - -HTTPWallet.prototype.retoken = async function retoken(passphrase) { - const token = await this.client.retoken(this.id, passphrase); - - this.token = token; - this.client.token = token; - - return token; -}; - -/** - * Import private key. - * @param {Number|String} account - * @param {String} key - * @returns {Promise} - */ - -HTTPWallet.prototype.importPrivate = function importPrivate(account, key, passphrase) { - return this.client.importPrivate(this.id, account, key, passphrase); -}; - -/** - * Import public key. - * @param {Number|String} account - * @param {String} key - * @returns {Promise} - */ - -HTTPWallet.prototype.importPublic = function importPublic(account, key) { - return this.client.importPublic(this.id, account, key); -}; - -/** - * Import address. - * @param {Number|String} account - * @param {String} address - * @returns {Promise} - */ - -HTTPWallet.prototype.importAddress = function importAddress(account, address) { - return this.client.importAddress(this.id, account, address); -}; - -/** - * Lock a coin. - * @param {String} hash - * @param {Number} index - * @returns {Promise} - */ - -HTTPWallet.prototype.lockCoin = function lockCoin(hash, index) { - return this.client.lockCoin(this.id, hash, index); -}; - -/** - * Unlock a coin. - * @param {String} hash - * @param {Number} index - * @returns {Promise} - */ - -HTTPWallet.prototype.unlockCoin = function unlockCoin(hash, index) { - return this.client.unlockCoin(this.id, hash, index); -}; - -/** - * Get locked coins. - * @returns {Promise} - */ - -HTTPWallet.prototype.getLocked = function getLocked() { - return this.client.getLocked(this.id); -}; - -/** - * Lock wallet. - * @returns {Promise} - */ - -HTTPWallet.prototype.lock = function lock() { - return this.client.lock(this.id); -}; - -/** - * Unlock wallet. - * @param {String} passphrase - * @param {Number} timeout - * @returns {Promise} - */ - -HTTPWallet.prototype.unlock = function unlock(passphrase, timeout) { - return this.client.unlock(this.id, passphrase, timeout); -}; - -/** - * Get wallet key. - * @param {String} address - * @returns {Promise} - */ - -HTTPWallet.prototype.getKey = function getKey(address) { - return this.client.getKey(this.id, address); -}; - -/** - * Get wallet key WIF dump. - * @param {String} address - * @param {String?} passphrase - * @returns {Promise} - */ - -HTTPWallet.prototype.getWIF = function getWIF(address, passphrase) { - return this.client.getWIF(this.id, address, passphrase); -}; - -/** - * Add a public account/purpose key to the wallet for multisig. - * @param {(String|Number)?} account - * @param {Base58String} key - Account (bip44) or - * Purpose (bip45) key (can be in base58 form). - * @returns {Promise} - */ - -HTTPWallet.prototype.addSharedKey = function addSharedKey(account, key) { - return this.client.addSharedKey(this.id, account, key); -}; - -/** - * Remove a public account/purpose key to the wallet for multisig. - * @param {(String|Number)?} account - * @param {Base58String} key - Account (bip44) or Purpose - * (bip45) key (can be in base58 form). - * @returns {Promise} - */ - -HTTPWallet.prototype.removeSharedKey = function removeSharedKey(account, key) { - return this.client.removeSharedKey(this.id, account, key); -}; - -/** - * Resend wallet transactions. - * @returns {Promise} - */ - -HTTPWallet.prototype.resend = function resend() { - return this.client.resendWallet(this.id); -}; + /** + * Open the client and create a wallet. + * @returns {Promise} + */ + + async create(options) { + const wallet = await this.createWallet(options); + + assert(wallet); + assert(typeof wallet.id === 'string'); + assert(typeof wallet.token === 'string'); + + this.id = wallet.id; + this.token = wallet.token; + + await this.init(); + await this.call('wallet join', this.id, this.token); + + return wallet; + } + + /** + * Auth with server. + * @returns {Promise} + */ + + async auth() { + return this.call('wallet auth', this.password); + } + + /** + * Make an RPC call. + * @returns {Promise} + */ + + execute(name, params) { + return super.execute('/', name, params); + } + + /** + * Rescan the chain. + * @param {Number} height + * @returns {Promise} + */ + + rescan(height) { + return this.post('/wallet/_admin/rescan', { height }); + } + + /** + * Resend pending transactions. + * @returns {Promise} + */ + + resend() { + return this.post('/wallet/_admin/resend'); + } + + /** + * Backup the walletdb. + * @param {String} path + * @returns {Promise} + */ + + backup(path) { + return this.post('/wallet/_admin/backup', { path }); + } + + /** + * Get list of all wallet IDs. + * @returns {Promise} + */ + + getWallets() { + return this.get('/wallet/_admin/wallets'); + } + + /** + * Create a wallet. + * @param {Object} options + * @returns {Promise} + */ + + createWallet(options) { + assert(options.id, 'Must pass an id parameter'); + return this.put(`/wallet/${options.id}`, options); + } + + /** + * Get wallet transaction history. + * @returns {Promise} + */ + + getHistory(account) { + return this.get(`/wallet/${this.id}/tx/history`, { account }); + } + + /** + * Get wallet coins. + * @returns {Promise} + */ + + getCoins(account) { + return this.get(`/wallet/${this.id}/coin`, { account }); + } + + /** + * Get all unconfirmed transactions. + * @returns {Promise} + */ + + getPending(account) { + return this.get(`/wallet/${this.id}/tx/unconfirmed`, { account }); + } + + /** + * Calculate wallet balance. + * @returns {Promise} + */ + + getBalance(account) { + return this.get(`/wallet/${this.id}/balance`, { account }); + } + + /** + * Get last N wallet transactions. + * @param {Number} limit - Max number of transactions. + * @returns {Promise} + */ + + getLast(account, limit) { + return this.get(`/wallet/${this.id}/tx/last`, { account, limit }); + } + + /** + * Get wallet transactions by timestamp range. + * @param {Object} options + * @param {Number} options.start - Start time. + * @param {Number} options.end - End time. + * @param {Number?} options.limit - Max number of records. + * @param {Boolean?} options.reverse - Reverse order. + * @returns {Promise} + */ + + getRange(account, options) { + return this.get(`/wallet/${this.id}/tx/range`, { + account: account, + start: options.start, + end: options.end , + limit: options.limit, + reverse: options.reverse + }); + } + + /** + * Get transaction (only possible if the transaction + * is available in the wallet history). + * @param {Hash} hash + * @returns {Promise} + */ + + getTX(hash) { + return this.get(`/wallet/${this.id}/tx/${hash}`); + } + + /** + * Get wallet blocks. + * @param {Number} height + * @returns {Promise} + */ + + getBlocks() { + return this.get(`/wallet/${this.id}/block`); + } + + /** + * Get wallet block. + * @param {Number} height + * @returns {Promise} + */ + + getBlock(height) { + return this.get(`/wallet/${this.id}/block/${height}`); + } + + /** + * Get unspent coin (only possible if the transaction + * is available in the wallet history). + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} + */ + + getCoin(hash, index) { + return this.get(`/wallet/${this.id}/coin/${hash}/${index}`); + } + + /** + * @param {Number} now - Current time. + * @param {Number} age - Age delta (delete transactions older than `now - age`). + * @returns {Promise} + */ + + zap(account, age) { + return this.post(`/wallet/${this.id}/zap`, { account, age }); + } + + /** + * Create a transaction, fill. + * @param {Object} options + * @returns {Promise} + */ + + createTX(options) { + return this.post(`/wallet/${this.id}/create`, options); + } + + /** + * Create a transaction, fill, sign, and broadcast. + * @param {Object} options + * @param {String} options.address + * @param {Amount} options.value + * @returns {Promise} + */ + + send(options) { + return this.post(`/wallet/${this.id}/send`, options); + } + + /** + * Sign a transaction. + * @param {Object} options + * @returns {Promise} + */ + + sign(options) { + return this.post(`/wallet/${this.id}/sign`, options); + } + + /** + * Get the raw wallet JSON. + * @returns {Promise} + */ + + getInfo() { + return this.get(`/wallet/${this.id}`); + } + + /** + * Get wallet accounts. + * @returns {Promise} - Returns Array. + */ + + getAccounts() { + return this.get(`/wallet/${this.id}/account`); + } + + /** + * Get wallet master key. + * @returns {Promise} + */ + + getMaster() { + return this.get(`/wallet/${this.id}/master`); + } + + /** + * Get wallet account. + * @param {String} account + * @returns {Promise} + */ + + getAccount(account) { + return this.get(`/wallet/${this.id}/account/${account}`); + } + + /** + * Create account. + * @param {String} name + * @param {Object} options + * @returns {Promise} + */ + + createAccount(name, options) { + return this.put(`/wallet/${this.id}/account/${name}`, options); + } + + /** + * Create address. + * @param {Object} options + * @returns {Promise} + */ + + createAddress(account) { + return this.post(`/wallet/${this.id}/address`, { account }); + } + + /** + * Create change address. + * @param {Object} options + * @returns {Promise} + */ + + createChange(account) { + return this.post(`/wallet/${this.id}/change`, { account }); + } + + /** + * Create nested address. + * @param {Object} options + * @returns {Promise} + */ + + createNested(account) { + return this.post(`/wallet/${this.id}/nested`, { account }); + } + + /** + * Change or set master key's passphrase. + * @param {String|Buffer} passphrase + * @param {(String|Buffer)?} old + * @returns {Promise} + */ + + setPassphrase(passphrase, old) { + return this.post(`/wallet/${this.id}/passphrase`, { passphrase, old }); + } + + /** + * Generate a new token. + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + + async retoken(passphrase) { + const body = await this.post(`/wallet/${this.id}/retoken`, { + passphrase + }); + + assert(body); + assert(typeof body.token === 'string'); + + this.token = body.token; + + return body.token; + } + + /** + * Import private key. + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + + importPrivate(account, privateKey, passphrase) { + return this.post(`/wallet/${this.id}/import`, { + account, + privateKey, + passphrase + }); + } + + /** + * Import public key. + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + + importPublic(account, publicKey) { + return this.post(`/wallet/${this.id}/import`, { + account, + publicKey + }); + } + + /** + * Import address. + * @param {Number|String} account + * @param {String} address + * @returns {Promise} + */ + + importAddress(account, address) { + return this.post(`/wallet/${this.id}/import`, { account, address }); + } + + /** + * Lock a coin. + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + + lockCoin(hash, index) { + return this.put(`/wallet/${this.id}/locked/${hash}/${index}`); + } + + /** + * Unlock a coin. + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + + unlockCoin(hash, index) { + return this.del(`/wallet/${this.id}/locked/${hash}/${index}`); + } + + /** + * Get locked coins. + * @returns {Promise} + */ + + getLocked() { + return this.get(`/wallet/${this.id}/locked`); + } + + /** + * Lock wallet. + * @returns {Promise} + */ + + lock() { + return this.post(`/wallet/${this.id}/lock`); + } + + /** + * Unlock wallet. + * @param {String} passphrase + * @param {Number} timeout + * @returns {Promise} + */ + + unlock(passphrase, timeout) { + return this.post(`/wallet/${this.id}/unlock`, { + passphrase, + timeout + }); + } + + /** + * Get wallet key. + * @param {String} address + * @returns {Promise} + */ + + getKey(address) { + return this.get(`/wallet/${this.id}/key/${address}`); + } + + /** + * Get wallet key WIF dump. + * @param {String} address + * @param {String?} passphrase + * @returns {Promise} + */ + + getWIF(address, passphrase) { + return this.get(`/wallet/${this.id}/wif/${address}`, { passphrase }); + } + + /** + * Add a public account/purpose key to the wallet for multisig. + * @param {(String|Number)?} account + * @param {Base58String} key - Account (bip44) or + * Purpose (bip45) key (can be in base58 form). + * @returns {Promise} + */ + + addSharedKey(account, accountKey) { + return this.put(`/wallet/${this.id}/shared-key`, { + account, + accountKey + }); + } + + /** + * Remove a public account/purpose key to the wallet for multisig. + * @param {(String|Number)?} account + * @param {Base58String} key - Account (bip44) or Purpose + * (bip45) key (can be in base58 form). + * @returns {Promise} + */ + + removeSharedKey(account, accountKey) { + return this.del(`/wallet/${this.id}/shared-key`, { + account, + accountKey + }); + } + + /** + * Resend wallet transactions. + * @returns {Promise} + */ + + resend() { + return this.post(`/wallet/${this.id}/resend`); + } +} /* * Expose diff --git a/lib/net/external.js b/lib/net/external.js index b455e5cb..af2b084b 100644 --- a/lib/net/external.js +++ b/lib/net/external.js @@ -6,7 +6,7 @@ 'use strict'; -const request = require('../http/request'); +const breq = require('breq'); const IP = require('../utils/ip'); /** @@ -23,14 +23,14 @@ const external = exports; external.getIPv4 = async function getIPv4() { try { - const res = await request({ + const res = await breq({ method: 'GET', - uri: 'http://ipv4.icanhazip.com', + url: 'http://ipv4.icanhazip.com', expect: 'txt', timeout: 2000 }); - const str = res.body.trim(); + const str = res.text().trim(); const raw = IP.toBuffer(str); if (!IP.isIPv4(raw)) @@ -50,14 +50,14 @@ external.getIPv4 = async function getIPv4() { */ external.getIPv42 = async function getIPv42() { - const res = await request({ + const res = await breq({ method: 'GET', - uri: 'http://checkip.dyndns.org', + url: 'http://checkip.dyndns.org', expect: 'html', timeout: 2000 }); - const match = /IP Address:\s*([0-9a-f.:]+)/i.exec(res.body); + const match = /IP Address:\s*([0-9a-f.:]+)/i.exec(res.text()); if (!match) throw new Error('Could not find IPv4.'); @@ -78,14 +78,14 @@ external.getIPv42 = async function getIPv42() { */ external.getIPv6 = async function getIPv6() { - const res = await request({ + const res = await breq({ method: 'GET', - uri: 'http://ipv6.icanhazip.com', + url: 'http://ipv6.icanhazip.com', expect: 'txt', timeout: 2000 }); - const str = res.body.trim(); + const str = res.text().trim(); const raw = IP.toBuffer(str); if (!IP.isIPv6(raw)) diff --git a/lib/net/upnp.js b/lib/net/upnp.js index 85ec40de..72993b7a 100644 --- a/lib/net/upnp.js +++ b/lib/net/upnp.js @@ -9,7 +9,7 @@ const assert = require('assert'); const dgram = require('dgram'); const url = require('url'); -const request = require('../http/request'); +const breq = require('breq'); const co = require('../utils/co'); const Lock = require('../utils/lock'); const IP = require('../utils/ip'); @@ -220,9 +220,9 @@ UPNP.prototype.resolve = async function resolve(location, targets) { if (!targets) targets = UPNP.WAN_SERVICES; - const res = await request({ + const res = await breq({ method: 'GET', - uri: location, + url: location, timeout: UPNP.RESPONSE_TIMEOUT, expect: 'xml' }); @@ -368,9 +368,9 @@ UPNPService.prototype.soapRequest = async function soapRequest(action, args) { const type = this.serviceType; const req = this.createRequest(action, args); - const res = await request({ + const res = await breq({ method: 'POST', - uri: this.controlURL, + url: this.controlURL, timeout: UPNP.RESPONSE_TIMEOUT, expect: 'xml', headers: { diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 7d07755d..162983d7 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -137,21 +137,19 @@ function FullNode(options) { this.rpc = new RPC(this); // HTTP needs access to the node. - 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._init(); } diff --git a/lib/primitives/block.js b/lib/primitives/block.js index 9be79213..2853f6e3 100644 --- a/lib/primitives/block.js +++ b/lib/primitives/block.js @@ -572,15 +572,16 @@ Block.prototype.toJSON = function toJSON() { * @param {Network} network * @param {CoinView} view * @param {Number} height + * @param {Number} depth * @returns {Object} */ -Block.prototype.getJSON = function getJSON(network, view, height, confirmations) { +Block.prototype.getJSON = function getJSON(network, view, height, depth) { network = Network.get(network); return { hash: this.rhash(), height: height, - confirmations: confirmations, + depth: depth, version: this.version, prevBlock: util.revHex(this.prevBlock), merkleRoot: util.revHex(this.merkleRoot), diff --git a/lib/wallet/client-browser.js b/lib/wallet/client-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/wallet/client-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/wallet/client.js b/lib/wallet/client.js index 9b5393f0..1d4996aa 100644 --- a/lib/wallet/client.js +++ b/lib/wallet/client.js @@ -7,325 +7,207 @@ 'use strict'; -const IOClient = require('socket.io-client'); -const Network = require('../protocol/network'); -const AsyncObject = require('../utils/asyncobject'); +const {Client} = require('bcurl'); const TX = require('../primitives/tx'); -const {BlockMeta} = require('./records'); const Headers = require('../primitives/headers'); const util = require('../utils/util'); const BufferReader = require('../utils/reader'); -/** - * Bcoin HTTP client. - * @alias module:wallet.WalletClient - * @constructor - * @param {Object|String} options - */ +class WalletClient extends Client { + /** + * Bcoin HTTP client. + * @alias module:wallet.WalletClient + * @constructor + * @param {Object|String} options + */ -function WalletClient(options) { - if (!(this instanceof WalletClient)) - return new WalletClient(options); + constructor(options) { + super(options); + } - if (!options) - options = {}; + /** + * Open the client, wait for socket to connect. + * @returns {Promise} + */ - if (typeof options === 'string') - options = { uri: options }; + async open() { + await super.open(); - AsyncObject.call(this); + this.on('error', (err) => { + this.emit('error', err); + }); - this.options = options; - this.network = Network.get(options.network); + this.listen('block connect', (entry, txs) => { + this.emit('block connect', ...parseBlock(entry, txs)); + }); - this.uri = options.uri || `http://localhost:${this.network.rpcPort}`; - this.apiKey = options.apiKey; + this.listen('block disconnect', (entry) => { + this.emit('block disconnect', parseEntry(entry)); + }); - this.socket = null; -} + this.listen('block rescan', (entry, txs) => { + this.emit('block rescan', ...parseBlock(entry, txs)); + }); -Object.setPrototypeOf(WalletClient.prototype, AsyncObject.prototype); + this.listen('chain reset', (tip) => { + this.emit('chain reset', parseEntry(tip)); + }); -/** - * Open the client, wait for socket to connect. - * @alias WalletClient#open - * @returns {Promise} - */ + this.listen('tx', (tx) => { + this.emit('tx', TX.fromRaw(tx)); + }); -WalletClient.prototype._open = async function _open() { - this.socket = new IOClient(this.uri, { - transports: ['websocket'], - forceNew: true - }); + await this.watchChain(); + await this.watchMempool(); + } - this.socket.on('error', (err) => { - this.emit('error', err); - }); + /** + * Auth with server. + * @private + * @returns {Promise} + */ - this.socket.on('version', (info) => { - if (info.network !== this.network.type) - this.emit('error', new Error('Wrong network.')); - }); + auth() { + return this.call('auth', this.password); + } - this.socket.on('block connect', (entry, txs) => { - let block; + /** + * Make an RPC call. + * @returns {Promise} + */ - try { - block = parseBlock(entry, txs); - } catch (e) { - this.emit('error', e); - return; - } + execute(name, params) { + return super.execute('/', name, params); + } - this.emit('block connect', block.entry, block.txs); - }); + /** + * Watch the blockchain. + * @private + * @returns {Promise} + */ - this.socket.on('block disconnect', (entry) => { - let block; + watchChain() { + return this.call('watch chain'); + } - try { - block = parseEntry(entry); - } catch (e) { - this.emit('error', e); - return; - } + /** + * Watch the blockchain. + * @private + * @returns {Promise} + */ - this.emit('block disconnect', block); - }); + watchMempool() { + return this.call('watch mempool'); + } - this.socket.on('block rescan', (entry, txs, cb) => { - let block; + /** + * Get chain tip. + * @returns {Promise} + */ - try { - block = parseBlock(entry, txs); - } catch (e) { - this.emit('error', e); - cb(); - return; - } + async getTip() { + const raw = await this.call('get tip'); + return parseEntry(raw); + } - this.fire('block rescan', block.entry, block.txs).then(cb, cb); - }); + /** + * Get chain entry. + * @param {Hash} hash + * @returns {Promise} + */ - this.socket.on('chain reset', (tip) => { - let block; - - try { - block = parseEntry(tip); - } catch (e) { - this.emit('error', e); - return; - } - - this.emit('chain reset', block); - }); - - this.socket.on('tx', (tx) => { - try { - tx = parseTX(tx); - } catch (e) { - this.emit('error', e); - return; - } - this.emit('tx', tx); - }); - - await this.onConnect(); - await this.sendAuth(); - await this.watchChain(); - await this.watchMempool(); -}; - -/** - * Close the client, wait for the socket to close. - * @alias WalletClient#close - * @returns {Promise} - */ - -WalletClient.prototype._close = function _close() { - if (!this.socket) - return Promise.resolve(); - - this.socket.disconnect(); - this.socket = null; - - return Promise.resolve(); -}; - -/** - * Wait for websocket connection. - * @private - * @returns {Promise} - */ - -WalletClient.prototype.onConnect = function onConnect() { - return new Promise((resolve, reject) => { - this.socket.once('connect', resolve); - }); -}; - -/** - * Wait for websocket auth. - * @private - * @returns {Promise} - */ - -WalletClient.prototype.sendAuth = function sendAuth() { - return new Promise((resolve, reject) => { - this.socket.emit('auth', this.apiKey, wrap(resolve, reject)); - }); -}; - -/** - * Watch the blockchain. - * @private - * @returns {Promise} - */ - -WalletClient.prototype.watchChain = function watchChain() { - return new Promise((resolve, reject) => { - this.socket.emit('watch chain', wrap(resolve, reject)); - }); -}; - -/** - * Watch the blockchain. - * @private - * @returns {Promise} - */ - -WalletClient.prototype.watchMempool = function watchMempool() { - return new Promise((resolve, reject) => { - this.socket.emit('watch mempool', wrap(resolve, reject)); - }); -}; - -/** - * Get chain tip. - * @returns {Promise} - */ - -WalletClient.prototype.getTip = function getTip() { - return new Promise((resolve, reject) => { - this.socket.emit('get tip', wrap(resolve, reject, parseEntry)); - }); -}; - -/** - * Get chain entry. - * @param {Hash} hash - * @returns {Promise} - */ - -WalletClient.prototype.getEntry = function getEntry(block) { - return new Promise((resolve, reject) => { + async getEntry(block) { if (typeof block === 'string') block = util.revHex(block); - this.socket.emit('get entry', block, wrap(resolve, reject, parseEntry)); - }); -}; + const raw = await this.call('get entry', block); + return parseEntry(raw); + } -/** - * Get hashes. - * @param {Number} [start=-1] - * @param {Number} [end=-1] - * @returns {Promise} - */ + /** + * Get hashes. + * @param {Number} [start=-1] + * @param {Number} [end=-1] + * @returns {Promise} + */ -WalletClient.prototype.getHashes = function getHashes(start = -1, end = -1) { - return new Promise((resolve, reject) => { - this.socket.emit('get hashes', start, end, wrap(resolve, reject)); - }); -}; + getHashes(start = -1, end = -1) { + return this.call('get hashes', start, end); + } -/** - * Send a transaction. Do not wait for promise. - * @param {TX} tx - * @returns {Promise} - */ + /** + * Send a transaction. Do not wait for promise. + * @param {TX} tx + * @returns {Promise} + */ -WalletClient.prototype.send = function send(tx) { - return new Promise((resolve, reject) => { - this.socket.emit('send', tx.toRaw(), wrap(resolve, reject)); - }); -}; + send(tx) { + return this.call('send', tx.toRaw()); + } -/** - * Set bloom filter. - * @param {Bloom} filter - * @returns {Promise} - */ + /** + * Set bloom filter. + * @param {Bloom} filter + * @returns {Promise} + */ -WalletClient.prototype.setFilter = function setFilter(filter) { - return new Promise((resolve, reject) => { - this.socket.emit('set filter', filter.toRaw(), wrap(resolve, reject)); - }); -}; + setFilter(filter) { + return this.call('set filter', filter.toRaw()); + } -/** - * Add data to filter. - * @param {Buffer} data - * @returns {Promise} - */ + /** + * Add data to filter. + * @param {Buffer} data + * @returns {Promise} + */ -WalletClient.prototype.addFilter = function addFilter(chunks) { - if (!Array.isArray(chunks)) - chunks = [chunks]; + addFilter(chunks) { + if (!Array.isArray(chunks)) + chunks = [chunks]; - return new Promise((resolve, reject) => { - this.socket.emit('add filter', chunks, wrap(resolve, reject)); - }); -}; + return this.call('add filter', chunks); + } -/** - * Reset filter. - * @returns {Promise} - */ + /** + * Reset filter. + * @returns {Promise} + */ -WalletClient.prototype.resetFilter = function resetFilter() { - return new Promise((resolve, reject) => { - this.socket.emit('reset filter', wrap(resolve, reject)); - }); -}; + resetFilter() { + return this.call('reset filter'); + } -/** - * Esimate smart fee. - * @param {Number?} blocks - * @returns {Promise} - */ + /** + * Esimate smart fee. + * @param {Number?} blocks + * @returns {Promise} + */ -WalletClient.prototype.estimateFee = function estimateFee(blocks) { - return new Promise((resolve, reject) => { - this.socket.emit('estimate fee', blocks, wrap(resolve, reject)); - }); -}; + estimateFee(blocks) { + return this.call('estimate fee', blocks); + } -/** - * Rescan for any missed transactions. - * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ -WalletClient.prototype.rescan = function rescan(start) { - return new Promise((resolve, reject) => { + rescan(start) { if (typeof start === 'string') start = util.revHex(start); - this.socket.emit('rescan', start, wrap(resolve, reject)); - }); -}; + return this.call('rescan', start); + } +} /* * Helpers */ -function parseEntry(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, 'hex'); - +function parseEntry(data) { const block = Headers.fromHead(data); const br = new BufferReader(data); @@ -334,7 +216,7 @@ function parseEntry(data, enc) { const height = br.readU32(); const hash = block.hash('hex'); - return new BlockMeta(hash, height, block.time); + return { hash, height, time: block.time }; } function parseBlock(entry, txs) { @@ -342,48 +224,11 @@ function parseBlock(entry, txs) { const out = []; for (const raw of txs) { - const tx = parseTX(raw); + const tx = TX.fromRaw(raw); out.push(tx); } - return new BlockResult(block, out); -} - -function parseTX(data) { - return TX.fromRaw(data, 'hex'); -} - -function BlockResult(entry, txs) { - this.entry = entry; - this.txs = txs; -} - -function wrap(resolve, reject, parse) { - return function(err, result) { - if (err) { - reject(new Error(err.message)); - return; - } - - if (!result) { - resolve(null); - return; - } - - if (!parse) { - resolve(result); - return; - } - - try { - result = parse(result); - } catch (e) { - reject(e); - return; - } - - resolve(result); - }; + return [block, out]; } /* diff --git a/lib/wallet/http-browser.js b/lib/wallet/http-browser.js deleted file mode 100644 index 21227270..00000000 --- a/lib/wallet/http-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index af1145bf..fd6086d5 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -9,7 +9,7 @@ const assert = require('assert'); const path = require('path'); -const HTTPBase = require('../http/base'); +const {Server} = require('bweb'); const util = require('../utils/util'); const base58 = require('../utils/base58'); const MTX = require('../primitives/mtx'); @@ -27,106 +27,108 @@ const HDPrivateKey = require('../hd/private'); const HDPublicKey = require('../hd/public'); const common = require('./common'); -/** - * HTTPServer - * @alias module:wallet.HTTPServer - * @constructor - * @param {Object} options - * @see HTTPBase - * @emits HTTPServer#socket - */ +class HTTPServer extends Server { + /** + * HTTPServer + * @alias module:wallet.HTTPServer + * @constructor + * @param {Object} options + * @see HTTPBase + * @emits HTTPServer#socket + */ -function HTTPServer(options) { - if (!(this instanceof HTTPServer)) - return new HTTPServer(options); + constructor(options) { + super(new HTTPOptions(options)); - options = new HTTPOptions(options); + this.network = this.options.network; + this.logger = this.options.logger.context('http'); + this.wdb = this.options.walletdb; + this.rpc = this.wdb.rpc; - HTTPBase.call(this, options); - - this.options = options; - this.network = this.options.network; - this.logger = this.options.logger.context('http'); - this.wdb = this.options.walletdb; - - this.rpc = this.wdb.rpc; - - this.init(); -} - -Object.setPrototypeOf(HTTPServer.prototype, HTTPBase.prototype); - -/** - * Attach to server. - * @private - * @param {HTTPServer} server - */ - -HTTPServer.prototype.attach = function attach(server) { - server.mount('/wallet', this); -}; - -/** - * Initialize http server. - * @private - */ - -HTTPServer.prototype.init = function init() { - this.on('request', (req, res) => { - if (req.method === 'POST' && req.pathname === '/') - return; - - this.logger.debug('Request for method=%s path=%s (%s).', - req.method, req.pathname, req.socket.remoteAddress); - }); - - this.on('listening', (address) => { - this.logger.info('HTTP server listening on %s (port=%d).', - address.address, address.port); - }); - - this.initRouter(); - this.initSockets(); -}; - -/** - * Initialize routes. - * @private - */ - -HTTPServer.prototype.initRouter = function initRouter() { - this.use(this.cors()); - - if (!this.options.noAuth) { - this.use(this.basicAuth({ - password: this.options.apiKey, - realm: 'wallet' - })); + this.init(); } - this.use(this.bodyParser({ - contentType: 'json' - })); + /** + * Initialize http server. + * @private + */ - this.use(this.jsonRPC(this.rpc)); + init() { + this.on('request', (req, res) => { + if (req.method === 'POST' && req.pathname === '/') + return; - this.hook(async (req, res) => { - const valid = Validator.fromRequest(req); + this.logger.debug('Request for method=%s path=%s (%s).', + req.method, req.pathname, req.socket.remoteAddress); + }); - if (req.path.length === 0) - return; + this.on('listening', (address) => { + this.logger.info('HTTP server listening on %s (port=%d).', + address.address, address.port); + }); - if (req.path[0] === '_admin') - return; + this.initRouter(); + this.initSockets(); + } - if (req.method === 'PUT' && req.path.length === 1) - return; + /** + * Initialize routes. + * @private + */ - const id = valid.str('id'); - const token = valid.buf('token'); + initRouter() { + this.use(this.cors()); - if (!this.options.walletAuth) { - const wallet = await this.wdb.get(id); + if (!this.options.noAuth) { + this.use(this.basicAuth({ + password: this.options.apiKey, + realm: 'wallet' + })); + } + + this.use(this.bodyParser({ + type: 'json' + })); + + this.use(this.jsonRPC()); + this.use(this.router()); + + this.hook(async (req, res) => { + const valid = Validator.fromRequest(req); + + if (req.path.length === 0) + return; + + if (req.path[0] === '_admin') + return; + + if (req.method === 'PUT' && req.path.length === 1) + return; + + const id = valid.str('id'); + const token = valid.buf('token'); + + if (!this.options.walletAuth) { + const wallet = await this.wdb.get(id); + + if (!wallet) { + res.send(404); + return; + } + + req.wallet = wallet; + + return; + } + + let wallet; + try { + wallet = await this.wdb.auth(id, token); + } catch (err) { + this.logger.info('Auth failure for %s: %s.', id, err.message); + res.error(403, err); + return; + } if (!wallet) { res.send(404); @@ -135,929 +137,921 @@ HTTPServer.prototype.initRouter = function initRouter() { req.wallet = wallet; - return; - } - - let wallet; - try { - wallet = await this.wdb.auth(id, token); - } catch (err) { - this.logger.info('Auth failure for %s: %s.', id, err.message); - res.error(403, err); - return; - } - - if (!wallet) { - res.send(404); - return; - } - - req.wallet = wallet; - - this.logger.info('Successful auth for %s.', id); - }); - - // Rescan - this.post('/_admin/rescan', async (req, res) => { - const valid = Validator.fromRequest(req); - const height = valid.u32('height'); - - res.send(200, { success: true }); - - await this.wdb.rescan(height); - }); - - // Resend - this.post('/_admin/resend', async (req, res) => { - await this.wdb.resend(); - - res.send(200, { success: true }); - }); - - // Backup WalletDB - this.post('/_admin/backup', async (req, res) => { - const valid = Validator.fromRequest(req); - const path = valid.str('path'); - - enforce(path, 'Path is required.'); - - await this.wdb.backup(path); - - res.send(200, { success: true }); - }); - - // List wallets - this.get('/_admin/wallets', async (req, res) => { - const wallets = await this.wdb.getWallets(); - res.send(200, wallets); - }); - - // Get wallet - this.get('/:id', async (req, res) => { - const balance = await req.wallet.getBalance(); - res.send(200, req.wallet.toJSON(false, balance)); - }); - - // Get wallet master key - this.get('/:id/master', (req, res) => { - res.send(200, req.wallet.master.toJSON(true)); - }); - - // Create wallet - this.put('/:id', async (req, res) => { - const valid = Validator.fromRequest(req); - - let master = valid.str('master'); - let mnemonic = valid.str('mnemonic'); - let accountKey = valid.str('accountKey'); - - if (master) - master = HDPrivateKey.fromBase58(master, this.network); - - if (mnemonic) - mnemonic = Mnemonic.fromPhrase(mnemonic); - - if (accountKey) - accountKey = HDPublicKey.fromBase58(accountKey, this.network); - - const wallet = await this.wdb.create({ - id: valid.str('id'), - type: valid.str('type'), - m: valid.u32('m'), - n: valid.u32('n'), - passphrase: valid.str('passphrase'), - master: master, - mnemonic: mnemonic, - witness: valid.bool('witness'), - accountKey: accountKey, - watchOnly: valid.bool('watchOnly') + this.logger.info('Successful auth for %s.', id); }); - const balance = await wallet.getBalance(); + // Rescan + this.post('/_admin/rescan', async (req, res) => { + const valid = Validator.fromRequest(req); + const height = valid.u32('height'); - res.send(200, wallet.toJSON(false, balance)); - }); + res.json(200, { success: true }); - // List accounts - this.get('/:id/account', async (req, res) => { - const accounts = await req.wallet.getAccounts(); - res.send(200, accounts); - }); + await this.wdb.rescan(height); + }); - // Get account - this.get('/:id/account/:account', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const account = await req.wallet.getAccount(acct); + // Resend + this.post('/_admin/resend', async (req, res) => { + await this.wdb.resend(); - if (!account) { - res.send(404); - return; - } + res.json(200, { success: true }); + }); - const balance = await req.wallet.getBalance(account.accountIndex); + // Backup WalletDB + this.post('/_admin/backup', async (req, res) => { + const valid = Validator.fromRequest(req); + const path = valid.str('path'); - res.send(200, account.toJSON(balance)); - }); + enforce(path, 'Path is required.'); - // Create account - this.put('/:id/account/:account', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); + await this.wdb.backup(path); - let accountKey = valid.get('accountKey'); + res.json(200, { success: true }); + }); - if (accountKey) - accountKey = HDPublicKey.fromBase58(accountKey, this.network); + // List wallets + this.get('/_admin/wallets', async (req, res) => { + const wallets = await this.wdb.getWallets(); + res.json(200, wallets); + }); - const options = { - name: valid.str('account'), - witness: valid.bool('witness'), - watchOnly: valid.bool('watchOnly'), - type: valid.str('type'), - m: valid.u32('m'), - n: valid.u32('n'), - accountKey: accountKey, - lookahead: valid.u32('lookahead') - }; + // Get wallet + this.get('/:id', async (req, res) => { + const balance = await req.wallet.getBalance(); + res.json(200, req.wallet.toJSON(false, balance)); + }); - const account = await req.wallet.createAccount(options, passphrase); - const balance = await req.wallet.getBalance(account.accountIndex); + // Get wallet master key + this.get('/:id/master', (req, res) => { + res.json(200, req.wallet.master.toJSON(true)); + }); - res.send(200, account.toJSON(balance)); - }); + // Create wallet + this.put('/:id', async (req, res) => { + const valid = Validator.fromRequest(req); - // Change passphrase - this.post('/:id/passphrase', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const old = valid.str('old'); + let master = valid.str('master'); + let mnemonic = valid.str('mnemonic'); + let accountKey = valid.str('accountKey'); - enforce(passphrase, 'Passphrase is required.'); + if (master) + master = HDPrivateKey.fromBase58(master, this.network); - await req.wallet.setPassphrase(passphrase, old); + if (mnemonic) + mnemonic = Mnemonic.fromPhrase(mnemonic); - res.send(200, { success: true }); - }); + if (accountKey) + accountKey = HDPublicKey.fromBase58(accountKey, this.network); - // Unlock wallet - this.post('/:id/unlock', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const timeout = valid.u32('timeout'); + const wallet = await this.wdb.create({ + id: valid.str('id'), + type: valid.str('type'), + m: valid.u32('m'), + n: valid.u32('n'), + passphrase: valid.str('passphrase'), + master: master, + mnemonic: mnemonic, + witness: valid.bool('witness'), + accountKey: accountKey, + watchOnly: valid.bool('watchOnly') + }); - enforce(passphrase, 'Passphrase is required.'); + const balance = await wallet.getBalance(); - await req.wallet.unlock(passphrase, timeout); + res.json(200, wallet.toJSON(false, balance)); + }); - res.send(200, { success: true }); - }); + // List accounts + this.get('/:id/account', async (req, res) => { + const accounts = await req.wallet.getAccounts(); + res.json(200, accounts); + }); - // Lock wallet - this.post('/:id/lock', async (req, res) => { - await req.wallet.lock(); - res.send(200, { success: true }); - }); + // Get account + this.get('/:id/account/:account', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const account = await req.wallet.getAccount(acct); - // Import key - this.post('/:id/import', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const passphrase = valid.str('passphrase'); - const pub = valid.buf('publicKey'); - const priv = valid.str('privateKey'); - const b58 = valid.str('address'); + if (!account) { + res.send(404); + return; + } - if (pub) { - const key = KeyRing.fromPublic(pub); - await req.wallet.importKey(acct, key); - res.send(200, { success: true }); - return; - } + const balance = await req.wallet.getBalance(account.accountIndex); - if (priv) { - const key = KeyRing.fromSecret(priv, this.network); - await req.wallet.importKey(acct, key, passphrase); - res.send(200, { success: true }); - return; - } + res.json(200, account.toJSON(balance)); + }); + + // Create account + this.put('/:id/account/:account', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + + let accountKey = valid.get('accountKey'); + + if (accountKey) + accountKey = HDPublicKey.fromBase58(accountKey, this.network); + + const options = { + name: valid.str('account'), + witness: valid.bool('witness'), + watchOnly: valid.bool('watchOnly'), + type: valid.str('type'), + m: valid.u32('m'), + n: valid.u32('n'), + accountKey: accountKey, + lookahead: valid.u32('lookahead') + }; + + const account = await req.wallet.createAccount(options, passphrase); + const balance = await req.wallet.getBalance(account.accountIndex); + + res.json(200, account.toJSON(balance)); + }); + + // Change passphrase + this.post('/:id/passphrase', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const old = valid.str('old'); + + enforce(passphrase, 'Passphrase is required.'); + + await req.wallet.setPassphrase(passphrase, old); + + res.json(200, { success: true }); + }); + + // Unlock wallet + this.post('/:id/unlock', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const timeout = valid.u32('timeout'); + + enforce(passphrase, 'Passphrase is required.'); + + await req.wallet.unlock(passphrase, timeout); + + res.json(200, { success: true }); + }); + + // Lock wallet + this.post('/:id/lock', async (req, res) => { + await req.wallet.lock(); + res.json(200, { success: true }); + }); + + // Import key + this.post('/:id/import', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const passphrase = valid.str('passphrase'); + const pub = valid.buf('publicKey'); + const priv = valid.str('privateKey'); + const b58 = valid.str('address'); + + if (pub) { + const key = KeyRing.fromPublic(pub); + await req.wallet.importKey(acct, key); + res.json(200, { success: true }); + return; + } + + if (priv) { + const key = KeyRing.fromSecret(priv, this.network); + await req.wallet.importKey(acct, key, passphrase); + res.json(200, { success: true }); + return; + } + + if (b58) { + const addr = Address.fromString(b58, this.network); + await req.wallet.importAddress(acct, addr); + res.json(200, { success: true }); + return; + } + + enforce(false, 'Key or address is required.'); + }); + + // Generate new token + this.post('/:id/retoken', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const token = await req.wallet.retoken(passphrase); + + res.json(200, { + token: token.toString('hex') + }); + }); + + // Send TX + this.post('/:id/send', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const outputs = valid.array('outputs', []); + + const options = { + rate: valid.u64('rate'), + blocks: valid.u32('blocks'), + maxFee: valid.u64('maxFee'), + selection: valid.str('selection'), + smart: valid.bool('smart'), + subtractFee: valid.bool('subtractFee'), + subtractIndex: valid.i32('subtractIndex'), + depth: valid.u32(['confirmations', 'depth']), + outputs: [] + }; + + for (const output of outputs) { + const valid = new Validator(output); + + let addr = valid.str('address'); + let script = valid.buf('script'); + + if (addr) + addr = Address.fromString(addr, this.network); + + if (script) + script = Script.fromRaw(script); + + options.outputs.push({ + address: addr, + script: script, + value: valid.u64('value') + }); + } + + const tx = await req.wallet.send(options, passphrase); + + const details = await req.wallet.getDetails(tx.hash('hex')); + + res.json(200, details.toJSON(this.network, this.wdb.height)); + }); + + // Create TX + this.post('/:id/create', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const outputs = valid.array('outputs', []); + + const options = { + rate: valid.u64('rate'), + maxFee: valid.u64('maxFee'), + selection: valid.str('selection'), + smart: valid.bool('smart'), + subtractFee: valid.bool('subtractFee'), + subtractIndex: valid.i32('subtractIndex'), + depth: valid.u32(['confirmations', 'depth']), + outputs: [] + }; + + for (const output of outputs) { + const valid = new Validator(output); + + let addr = valid.str('address'); + let script = valid.buf('script'); + + if (addr) + addr = Address.fromString(addr, this.network); + + if (script) + script = Script.fromRaw(script); + + options.outputs.push({ + address: addr, + script: script, + value: valid.u64('value') + }); + } + + const tx = await req.wallet.createTX(options); + + await req.wallet.sign(tx, passphrase); + + res.json(200, tx.getJSON(this.network)); + }); + + // Sign TX + this.post('/:id/sign', async (req, res) => { + const valid = Validator.fromRequest(req); + const passphrase = valid.str('passphrase'); + const raw = valid.buf('tx'); + + enforce(raw, 'TX is required.'); + + const tx = MTX.fromRaw(raw); + tx.view = await req.wallet.getCoinView(tx); + + await req.wallet.sign(tx, passphrase); + + res.json(200, tx.getJSON(this.network)); + }); + + // Zap Wallet TXs + this.post('/:id/zap', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const age = valid.u32('age'); + + enforce(age, 'Age is required.'); + + await req.wallet.zap(acct, age); + + res.json(200, { success: true }); + }); + + // Abandon Wallet TX + this.del('/:id/tx/:hash', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + + enforce(hash, 'Hash is required.'); + + await req.wallet.abandon(hash); + + res.json(200, { success: true }); + }); + + // List blocks + this.get('/:id/block', async (req, res) => { + const heights = await req.wallet.getBlocks(); + res.json(200, heights); + }); + + // Get Block Record + this.get('/:id/block/:height', async (req, res) => { + const valid = Validator.fromRequest(req); + const height = valid.u32('height'); + + enforce(height != null, 'Height is required.'); + + const block = await req.wallet.getBlock(height); + + if (!block) { + res.send(404); + return; + } + + res.json(200, block.toJSON()); + }); + + // Add key + this.put('/:id/shared-key', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const b58 = valid.str('accountKey'); + + enforce(b58, 'Key is required.'); + + const key = HDPublicKey.fromBase58(b58, this.network); + + await req.wallet.addSharedKey(acct, key); + + res.json(200, { success: true }); + }); + + // Remove key + this.del('/:id/shared-key', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const b58 = valid.str('accountKey'); + + enforce(b58, 'Key is required.'); + + const key = HDPublicKey.fromBase58(b58, this.network); + + await req.wallet.removeSharedKey(acct, key); + + res.json(200, { success: true }); + }); + + // Get key by address + this.get('/:id/key/:address', async (req, res) => { + const valid = Validator.fromRequest(req); + const b58 = valid.str('address'); + + enforce(b58, 'Address is required.'); - if (b58) { const addr = Address.fromString(b58, this.network); - await req.wallet.importAddress(acct, addr); - res.send(200, { success: true }); - return; - } + const key = await req.wallet.getKey(addr); - enforce(false, 'Key or address is required.'); - }); + if (!key) { + res.send(404); + return; + } - // Generate new token - this.post('/:id/retoken', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const token = await req.wallet.retoken(passphrase); - - res.send(200, { - token: token.toString('hex') + res.json(200, key.toJSON(this.network)); }); - }); - // Send TX - this.post('/:id/send', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const outputs = valid.array('outputs', []); - - const options = { - rate: valid.u64('rate'), - blocks: valid.u32('blocks'), - maxFee: valid.u64('maxFee'), - selection: valid.str('selection'), - smart: valid.bool('smart'), - subtractFee: valid.bool('subtractFee'), - subtractIndex: valid.i32('subtractIndex'), - depth: valid.u32(['confirmations', 'depth']), - outputs: [] - }; + // Get private key + this.get('/:id/wif/:address', async (req, res) => { + const valid = Validator.fromRequest(req); + const b58 = valid.str('address'); + const passphrase = valid.str('passphrase'); - for (const output of outputs) { - const valid = new Validator(output); + enforce(b58, 'Address is required.'); - let addr = valid.str('address'); - let script = valid.buf('script'); + const addr = Address.fromString(b58, this.network); + const key = await req.wallet.getPrivateKey(addr, passphrase); - if (addr) - addr = Address.fromString(addr, this.network); + if (!key) { + res.send(404); + return; + } - if (script) - script = Script.fromRaw(script); + res.json(200, { privateKey: key.toSecret(this.network) }); + }); - options.outputs.push({ - address: addr, - script: script, - value: valid.u64('value') - }); - } + // Create address + this.post('/:id/address', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const addr = await req.wallet.createReceive(acct); - const tx = await req.wallet.send(options, passphrase); + res.json(200, addr.toJSON(this.network)); + }); - const details = await req.wallet.getDetails(tx.hash('hex')); + // Create change address + this.post('/:id/change', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const addr = await req.wallet.createChange(acct); - res.send(200, details.toJSON(this.network, this.wdb.height)); - }); + res.json(200, addr.toJSON(this.network)); + }); - // Create TX - this.post('/:id/create', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const outputs = valid.array('outputs', []); + // Create nested address + this.post('/:id/nested', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const addr = await req.wallet.createNested(acct); - const options = { - rate: valid.u64('rate'), - maxFee: valid.u64('maxFee'), - selection: valid.str('selection'), - smart: valid.bool('smart'), - subtractFee: valid.bool('subtractFee'), - subtractIndex: valid.i32('subtractIndex'), - depth: valid.u32(['confirmations', 'depth']), - outputs: [] - }; + res.json(200, addr.toJSON(this.network)); + }); - for (const output of outputs) { - const valid = new Validator(output); + // Wallet Balance + this.get('/:id/balance', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const balance = await req.wallet.getBalance(acct); - let addr = valid.str('address'); - let script = valid.buf('script'); + if (!balance) { + res.send(404); + return; + } - if (addr) - addr = Address.fromString(addr, this.network); + res.json(200, balance.toJSON()); + }); - if (script) - script = Script.fromRaw(script); + // Wallet UTXOs + this.get('/:id/coin', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const coins = await req.wallet.getCoins(acct); + const result = []; - options.outputs.push({ - address: addr, - script: script, - value: valid.u64('value') - }); - } + common.sortCoins(coins); - const tx = await req.wallet.createTX(options); + for (const coin of coins) + result.push(coin.getJSON(this.network)); - await req.wallet.sign(tx, passphrase); + res.json(200, result); + }); - res.send(200, tx.getJSON(this.network)); - }); + // Locked coins + this.get('/:id/locked', async (req, res) => { + const locked = req.wallet.getLocked(); + const result = []; - // Sign TX - this.post('/:id/sign', async (req, res) => { - const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const raw = valid.buf('tx'); + for (const outpoint of locked) + result.push(outpoint.toJSON()); - enforce(raw, 'TX is required.'); + res.json(200, result); + }); - const tx = MTX.fromRaw(raw); - tx.view = await req.wallet.getCoinView(tx); + // Lock coin + this.put('/:id/locked/:hash/:index', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + const index = valid.u32('index'); - await req.wallet.sign(tx, passphrase); + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); - res.send(200, tx.getJSON(this.network)); - }); + const outpoint = new Outpoint(hash, index); - // Zap Wallet TXs - this.post('/:id/zap', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const age = valid.u32('age'); + req.wallet.lockCoin(outpoint); - enforce(age, 'Age is required.'); + res.json(200, { success: true }); + }); - await req.wallet.zap(acct, age); + // Unlock coin + this.del('/:id/locked/:hash/:index', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + const index = valid.u32('index'); - res.send(200, { success: true }); - }); + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); - // Abandon Wallet TX - this.del('/:id/tx/:hash', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); + const outpoint = new Outpoint(hash, index); - enforce(hash, 'Hash is required.'); + req.wallet.unlockCoin(outpoint); - await req.wallet.abandon(hash); + res.json(200, { success: true }); + }); - res.send(200, { success: true }); - }); + // Wallet Coin + this.get('/:id/coin/:hash/:index', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); + const index = valid.u32('index'); - // List blocks - this.get('/:id/block', async (req, res) => { - const heights = await req.wallet.getBlocks(); - res.send(200, heights); - }); + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); - // Get Block Record - this.get('/:id/block/:height', async (req, res) => { - const valid = Validator.fromRequest(req); - const height = valid.u32('height'); + const coin = await req.wallet.getCoin(hash, index); - enforce(height != null, 'Height is required.'); + if (!coin) { + res.send(404); + return; + } - const block = await req.wallet.getBlock(height); + res.json(200, coin.getJSON(this.network)); + }); - if (!block) { - res.send(404); - return; - } + // Wallet TXs + this.get('/:id/tx/history', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const txs = await req.wallet.getHistory(acct); - res.send(200, block.toJSON()); - }); + common.sortTX(txs); - // Add key - this.put('/:id/shared-key', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const b58 = valid.str('accountKey'); + const details = await req.wallet.toDetails(txs); - enforce(b58, 'Key is required.'); + const result = []; - const key = HDPublicKey.fromBase58(b58, this.network); + for (const item of details) + result.push(item.toJSON(this.network, this.wdb.height)); - await req.wallet.addSharedKey(acct, key); + res.json(200, result); + }); - res.send(200, { success: true }); - }); + // Wallet Pending TXs + this.get('/:id/tx/unconfirmed', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const txs = await req.wallet.getPending(acct); - // Remove key - this.del('/:id/shared-key', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const b58 = valid.str('accountKey'); + common.sortTX(txs); - enforce(b58, 'Key is required.'); + const details = await req.wallet.toDetails(txs); + const result = []; - const key = HDPublicKey.fromBase58(b58, this.network); + for (const item of details) + result.push(item.toJSON(this.network, this.wdb.height)); - await req.wallet.removeSharedKey(acct, key); + res.json(200, result); + }); - res.send(200, { success: true }); - }); + // Wallet TXs within time range + this.get('/:id/tx/range', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); - // Get key by address - this.get('/:id/key/:address', async (req, res) => { - const valid = Validator.fromRequest(req); - const b58 = valid.str('address'); + const options = { + start: valid.u32('start'), + end: valid.u32('end'), + limit: valid.u32('limit'), + reverse: valid.bool('reverse') + }; - enforce(b58, 'Address is required.'); + const txs = await req.wallet.getRange(acct, options); + const details = await req.wallet.toDetails(txs); + const result = []; - const addr = Address.fromString(b58, this.network); - const key = await req.wallet.getKey(addr); + for (const item of details) + result.push(item.toJSON(this.network, this.wdb.height)); - if (!key) { - res.send(404); - return; - } + res.json(200, result); + }); - res.send(200, key.toJSON(this.network)); - }); + // Last Wallet TXs + this.get('/:id/tx/last', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const limit = valid.u32('limit'); + const txs = await req.wallet.getLast(acct, limit); + const details = await req.wallet.toDetails(txs); + const result = []; - // Get private key - this.get('/:id/wif/:address', async (req, res) => { - const valid = Validator.fromRequest(req); - const b58 = valid.str('address'); - const passphrase = valid.str('passphrase'); + for (const item of details) + result.push(item.toJSON(this.network, this.wdb.height)); - enforce(b58, 'Address is required.'); + res.json(200, result); + }); - const addr = Address.fromString(b58, this.network); - const key = await req.wallet.getPrivateKey(addr, passphrase); + // Wallet TX + this.get('/:id/tx/:hash', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.rhash('hash'); - if (!key) { - res.send(404); - return; - } + enforce(hash, 'Hash is required.'); - res.send(200, { privateKey: key.toSecret(this.network) }); - }); + const tx = await req.wallet.getTX(hash); - // Create address - this.post('/:id/address', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const addr = await req.wallet.createReceive(acct); + if (!tx) { + res.send(404); + return; + } - res.send(200, addr.toJSON(this.network)); - }); + const details = await req.wallet.toDetails(tx); - // Create change address - this.post('/:id/change', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const addr = await req.wallet.createChange(acct); + res.json(200, details.toJSON(this.network, this.wdb.height)); + }); - res.send(200, addr.toJSON(this.network)); - }); + // Resend + this.post('/:id/resend', async (req, res) => { + await req.wallet.resend(); + res.json(200, { success: true }); + }); + } - // Create nested address - this.post('/:id/nested', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const addr = await req.wallet.createNested(acct); + /** + * Initialize websockets. + * @private + */ - res.send(200, addr.toJSON(this.network)); - }); + initSockets() { + this.wdb.on('tx', (w, tx, details) => { + if (!this.channel(`w:${w.id}`)) + return; - // Wallet Balance - this.get('/:id/balance', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const balance = await req.wallet.getBalance(acct); + const json = details.toJSON(this.network, this.wdb.height); + this.to(`w:${w.id}`, 'wallet tx', json); + }); - if (!balance) { - res.send(404); - return; - } + this.wdb.on('confirmed', (w, tx, details) => { + if (!this.channel(`w:${w.id}`)) + return; - res.send(200, balance.toJSON()); - }); + const json = details.toJSON(this.network, this.wdb.height); + this.to(`w:${w.id}`, 'wallet confirmed', json); + }); - // Wallet UTXOs - this.get('/:id/coin', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const coins = await req.wallet.getCoins(acct); - const result = []; + this.wdb.on('unconfirmed', (w, tx, details) => { + if (!this.channel(`w:${w.id}`)) + return; - common.sortCoins(coins); + const json = details.toJSON(this.network, this.wdb.height); + this.to(`w:${w.id}`, 'wallet unconfirmed', json); + }); - for (const coin of coins) - result.push(coin.getJSON(this.network)); + this.wdb.on('conflict', (w, tx, details) => { + if (!this.channel(`w:${w.id}`)) + return; - res.send(200, result); - }); + const json = details.toJSON(this.network, this.wdb.height); + this.to(`w:${w.id}`, 'wallet conflict', json); + }); - // Locked coins - this.get('/:id/locked', async (req, res) => { - const locked = req.wallet.getLocked(); - const result = []; + this.wdb.on('balance', (w, balance) => { + if (!this.channel(`w:${w.id}`)) + return; - for (const outpoint of locked) - result.push(outpoint.toJSON()); + const json = balance.toJSON(); + this.to(`w:${w.id}`, 'wallet balance', json); + }); - res.send(200, result); - }); + this.wdb.on('address', (w, receive) => { + if (!this.channel(`w:${w.id}`)) + return; - // Lock coin - this.put('/:id/locked/:hash/:index', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - const index = valid.u32('index'); + const json = []; - enforce(hash, 'Hash is required.'); - enforce(index != null, 'Index is required.'); + for (const addr of receive) + json.push(addr.toJSON(this.network)); - const outpoint = new Outpoint(hash, index); + this.to(`w:${w.id}`, 'wallet address', json); + }); + } - req.wallet.lockCoin(outpoint); + /** + * Handle new websocket. + * @private + * @param {WebSocket} socket + */ - res.send(200, { success: true }); - }); + handleSocket(socket) { + socket.hook('wallet auth', (...args) => { + if (socket.auth) + throw new Error('Already authed.'); - // Unlock coin - this.del('/:id/locked/:hash/:index', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - const index = valid.u32('index'); + if (!this.options.noAuth) { + const valid = new Validator(args); + const key = valid.str(0, ''); - enforce(hash, 'Hash is required.'); - enforce(index != null, 'Index is required.'); + if (key.length > 255) + throw new Error('Invalid API key.'); - const outpoint = new Outpoint(hash, index); + const data = Buffer.from(key, 'utf8'); + const hash = digest.hash256(data); - req.wallet.unlockCoin(outpoint); + if (!ccmp(hash, this.options.apiHash)) + throw new Error('Invalid API key.'); + } - res.send(200, { success: true }); - }); + socket.auth = true; - // Wallet Coin - this.get('/:id/coin/:hash/:index', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - const index = valid.u32('index'); + this.logger.info('Successful auth from %s.', socket.remoteAddress); - enforce(hash, 'Hash is required.'); - enforce(index != null, 'Index is required.'); + this.handleAuth(socket); - const coin = await req.wallet.getCoin(hash, index); - - if (!coin) { - res.send(404); - return; - } - - res.send(200, coin.getJSON(this.network)); - }); - - // Wallet TXs - this.get('/:id/tx/history', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const txs = await req.wallet.getHistory(acct); - - common.sortTX(txs); - - const details = await req.wallet.toDetails(txs); - - const result = []; - - for (const item of details) - result.push(item.toJSON(this.network, this.wdb.height)); - - res.send(200, result); - }); - - // Wallet Pending TXs - this.get('/:id/tx/unconfirmed', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const txs = await req.wallet.getPending(acct); - - common.sortTX(txs); - - const details = await req.wallet.toDetails(txs); - const result = []; - - for (const item of details) - result.push(item.toJSON(this.network, this.wdb.height)); - - res.send(200, result); - }); - - // Wallet TXs within time range - this.get('/:id/tx/range', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - - const options = { - start: valid.u32('start'), - end: valid.u32('end'), - limit: valid.u32('limit'), - reverse: valid.bool('reverse') - }; - - const txs = await req.wallet.getRange(acct, options); - const details = await req.wallet.toDetails(txs); - const result = []; - - for (const item of details) - result.push(item.toJSON(this.network, this.wdb.height)); - - res.send(200, result); - }); - - // Last Wallet TXs - this.get('/:id/tx/last', async (req, res) => { - const valid = Validator.fromRequest(req); - const acct = valid.str('account'); - const limit = valid.u32('limit'); - const txs = await req.wallet.getLast(acct, limit); - const details = await req.wallet.toDetails(txs); - const result = []; - - for (const item of details) - result.push(item.toJSON(this.network, this.wdb.height)); - - res.send(200, result); - }); - - // Wallet TX - this.get('/:id/tx/:hash', async (req, res) => { - const valid = Validator.fromRequest(req); - const hash = valid.rhash('hash'); - - enforce(hash, 'Hash is required.'); - - const tx = await req.wallet.getTX(hash); - - if (!tx) { - res.send(404); - return; - } - - const details = await req.wallet.toDetails(tx); - - res.send(200, details.toJSON(this.network, this.wdb.height)); - }); - - // Resend - this.post('/:id/resend', async (req, res) => { - await req.wallet.resend(); - res.send(200, { success: true }); - }); -}; - -/** - * Initialize websockets. - * @private - */ - -HTTPServer.prototype.initSockets = function initSockets() { - if (!this.io) - return; - - this.on('socket', (socket) => { - this.handleSocket(socket); - }); - - this.wdb.on('tx', (w, tx, details) => { - const json = details.toJSON(this.network, this.wdb.height); - this.to(`w:${w.id}`, 'wallet tx', json); - }); - - this.wdb.on('confirmed', (w, tx, details) => { - const json = details.toJSON(this.network, this.wdb.height); - this.to(`w:${w.id}`, 'wallet confirmed', json); - }); - - this.wdb.on('unconfirmed', (w, tx, details) => { - const json = details.toJSON(this.network, this.wdb.height); - this.to(`w:${w.id}`, 'wallet unconfirmed', json); - }); - - this.wdb.on('conflict', (w, tx, details) => { - const json = details.toJSON(this.network, this.wdb.height); - this.to(`w:${w.id}`, 'wallet conflict', json); - }); - - this.wdb.on('balance', (w, balance) => { - const json = balance.toJSON(); - this.to(`w:${w.id}`, 'wallet balance', json); - }); - - this.wdb.on('address', (w, receive) => { - const json = []; - - for (const addr of receive) - json.push(addr.toJSON(this.network)); - - this.to(`w:${w.id}`, 'wallet address', json); - }); -}; - -/** - * Handle new websocket. - * @private - * @param {WebSocket} socket - */ - -HTTPServer.prototype.handleSocket = function handleSocket(socket) { - socket.hook('wallet auth', (args) => { - if (socket.auth) - throw new Error('Already authed.'); - - if (!this.options.noAuth) { - const valid = new Validator(args); - const key = valid.str(0, ''); - - if (key.length > 255) - throw new Error('Invalid API key.'); - - const data = Buffer.from(key, 'utf8'); - const hash = digest.hash256(data); - - if (!ccmp(hash, this.options.apiHash)) - throw new Error('Invalid API key.'); - } - - socket.auth = true; - - this.logger.info('Successful auth from %s.', socket.remoteAddress); - - this.handleAuth(socket); - - return null; - }); -}; - -/** - * Handle new auth'd websocket. - * @private - * @param {WebSocket} socket - */ - -HTTPServer.prototype.handleAuth = function handleAuth(socket) { - socket.hook('wallet join', async (args) => { - const valid = new Validator(args); - const id = valid.str(0, ''); - const token = valid.buf(1); - - if (!id) - throw new Error('Invalid parameter.'); - - if (!this.options.walletAuth) { - socket.join(`w:${id}`); return null; - } + }); + } - if (!token) - throw new Error('Invalid parameter.'); + /** + * Handle new auth'd websocket. + * @private + * @param {WebSocket} socket + */ - let wallet; - try { - wallet = await this.wdb.auth(id, token); - } catch (e) { - this.logger.info('Wallet auth failure for %s: %s.', id, e.message); - throw new Error('Bad token.'); - } + handleAuth(socket) { + socket.hook('wallet join', async (...args) => { + const valid = new Validator(args); + const id = valid.str(0, ''); + const token = valid.buf(1); - if (!wallet) - throw new Error('Wallet does not exist.'); + if (!id) + throw new Error('Invalid parameter.'); - this.logger.info('Successful wallet auth for %s.', id); + if (!this.options.walletAuth) { + socket.join(`w:${id}`); + return null; + } - socket.join(`w:${id}`); + if (!token) + throw new Error('Invalid parameter.'); - return null; - }); + let wallet; + try { + wallet = await this.wdb.auth(id, token); + } catch (e) { + this.logger.info('Wallet auth failure for %s: %s.', id, e.message); + throw new Error('Bad token.'); + } - socket.hook('wallet leave', (args) => { - const valid = new Validator(args); - const id = valid.str(0, ''); + if (!wallet) + throw new Error('Wallet does not exist.'); - if (!id) - throw new Error('Invalid parameter.'); + this.logger.info('Successful wallet auth for %s.', id); - socket.leave(`w:${id}`); + socket.join(`w:${id}`); - return null; - }); -}; + return null; + }); -/** - * HTTPOptions - * @alias module:http.HTTPOptions - * @constructor - * @param {Object} options - */ + socket.hook('wallet leave', (...args) => { + const valid = new Validator(args); + const id = valid.str(0, ''); -function HTTPOptions(options) { - if (!(this instanceof HTTPOptions)) - return new HTTPOptions(options); + if (!id) + throw new Error('Invalid parameter.'); - this.network = Network.primary; - this.logger = null; - this.walletdb = null; - this.apiKey = base58.encode(random.randomBytes(20)); - this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); - this.serviceHash = this.apiHash; - this.noAuth = false; - this.walletAuth = false; + socket.leave(`w:${id}`); - this.prefix = null; - this.host = '127.0.0.1'; - this.port = 8080; - this.ssl = false; - this.keyFile = null; - this.certFile = null; - - this.fromOptions(options); + return null; + }); + } } -/** - * Inject properties from object. - * @private - * @param {Object} options - * @returns {HTTPOptions} - */ +class HTTPOptions { + /** + * HTTPOptions + * @alias module:http.HTTPOptions + * @constructor + * @param {Object} options + */ -HTTPOptions.prototype.fromOptions = function fromOptions(options) { - assert(options); - assert(options.walletdb && typeof options.walletdb === '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; - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.apiKey != null) { - assert(typeof options.apiKey === 'string', - 'API key must be a string.'); - assert(options.apiKey.length <= 255, - 'API key must be under 255 bytes.'); - assert(util.isAscii(options.apiKey), - 'API key must be ASCII.'); - this.apiKey = options.apiKey; + constructor(options) { + this.network = Network.primary; + this.logger = null; + this.walletdb = null; + this.apiKey = base58.encode(random.randomBytes(20)); this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); + this.serviceHash = this.apiHash; + this.noAuth = false; + this.walletAuth = false; + + this.prefix = null; + this.host = '127.0.0.1'; + this.port = 8080; + this.ssl = false; + this.keyFile = null; + this.certFile = null; + + this.fromOptions(options); } - if (options.noAuth != null) { - assert(typeof options.noAuth === 'boolean'); - this.noAuth = options.noAuth; + /** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {HTTPOptions} + */ + + fromOptions(options) { + assert(options); + assert(options.walletdb && typeof options.walletdb === '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; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.apiKey != null) { + assert(typeof options.apiKey === 'string', + 'API key must be a string.'); + assert(options.apiKey.length <= 255, + 'API key must be under 255 bytes.'); + assert(util.isAscii(options.apiKey), + 'API key must be ASCII.'); + this.apiKey = options.apiKey; + this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii')); + } + + if (options.noAuth != null) { + assert(typeof options.noAuth === 'boolean'); + this.noAuth = options.noAuth; + } + + if (options.walletAuth != null) { + assert(typeof options.walletAuth === 'boolean'); + this.walletAuth = options.walletAuth; + } + + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.keyFile = path.join(this.prefix, 'key.pem'); + this.certFile = path.join(this.prefix, 'cert.pem'); + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(util.isU16(options.port), 'Port must be a number.'); + this.port = options.port; + } + + if (options.ssl != null) { + assert(typeof options.ssl === 'boolean'); + this.ssl = options.ssl; + } + + if (options.keyFile != null) { + assert(typeof options.keyFile === 'string'); + this.keyFile = options.keyFile; + } + + if (options.certFile != null) { + assert(typeof options.certFile === 'string'); + this.certFile = options.certFile; + } + + // Allow no-auth implicitly + // if we're listening locally. + if (!options.apiKey) { + if (this.host === '127.0.0.1' || this.host === '::1') + this.noAuth = true; + } + + return this; } - if (options.walletAuth != null) { - assert(typeof options.walletAuth === 'boolean'); - this.walletAuth = options.walletAuth; + /** + * Instantiate http options from object. + * @param {Object} options + * @returns {HTTPOptions} + */ + + static fromOptions(options) { + return new HTTPOptions().fromOptions(options); } - - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.keyFile = path.join(this.prefix, 'key.pem'); - this.certFile = path.join(this.prefix, 'cert.pem'); - } - - if (options.host != null) { - assert(typeof options.host === 'string'); - this.host = options.host; - } - - if (options.port != null) { - assert(util.isU16(options.port), 'Port must be a number.'); - this.port = options.port; - } - - if (options.ssl != null) { - assert(typeof options.ssl === 'boolean'); - this.ssl = options.ssl; - } - - if (options.keyFile != null) { - assert(typeof options.keyFile === 'string'); - this.keyFile = options.keyFile; - } - - if (options.certFile != null) { - assert(typeof options.certFile === 'string'); - this.certFile = options.certFile; - } - - // Allow no-auth implicitly - // if we're listening locally. - if (!options.apiKey) { - if (this.host === '127.0.0.1' || this.host === '::1') - this.noAuth = true; - } - - return this; -}; - -/** - * Instantiate http options from object. - * @param {Object} options - * @returns {HTTPOptions} - */ - -HTTPOptions.fromOptions = function fromOptions(options) { - return new HTTPOptions().fromOptions(options); -}; +} /* * Helpers diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 4334e6e3..95364884 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -56,10 +56,7 @@ plugin.init = function init(node) { listen: false }); - if (node.http && wdb.http) - wdb.http.attach(node.http); - - wdb.rpc.attach(node.rpc); + wdb.http.attach('/wallet', node.http); return wdb; }; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 854bcab8..b8f9f19b 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -7,6 +7,7 @@ 'use strict'; const assert = require('assert'); +const bweb = require('bweb'); const fs = require('../utils/fs'); const util = require('../utils/util'); const digest = require('../crypto/digest'); @@ -20,1550 +21,1547 @@ const Outpoint = require('../primitives/outpoint'); const Output = require('../primitives/output'); const TX = require('../primitives/tx'); const encoding = require('../utils/encoding'); -const RPCBase = require('../http/rpcbase'); const pkg = require('../pkg'); const Validator = require('../utils/validator'); const Lock = require('../utils/lock'); const common = require('./common'); -const RPCError = RPCBase.RPCError; -const errs = RPCBase.errors; -const MAGIC_STRING = RPCBase.MAGIC_STRING; +const RPCBase = bweb.RPC; +const RPCError = bweb.RPCError; +const errs = bweb.errors; +const MAGIC_STRING = 'Bitcoin Signed Message:\n'; -/** - * Bitcoin Core RPC - * @alias module:wallet.RPC - * @constructor - * @param {WalletDB} wdb - */ +class RPC extends RPCBase { + /** + * Bitcoin Core RPC + * @alias module:wallet.RPC + * @constructor + * @param {WalletDB} wdb + */ -function RPC(wdb) { - if (!(this instanceof RPC)) - return new RPC(wdb); + constructor(wdb) { + super(); - RPCBase.call(this); + assert(wdb, 'RPC requires a WalletDB.'); - assert(wdb, 'RPC requires a WalletDB.'); + this.wdb = wdb; + this.network = wdb.network; + this.logger = wdb.logger.context('rpc'); + this.client = wdb.client; + this.locker = new Lock(); - this.wdb = wdb; - this.network = wdb.network; - this.logger = wdb.logger.context('rpc'); - this.client = wdb.client; - this.locker = new Lock(); + this.wallet = null; - this.wallet = null; - - this.init(); -} - -Object.setPrototypeOf(RPC.prototype, RPCBase.prototype); - -RPC.prototype.init = function init() { - this.add('help', this.help); - this.add('stop', this.stop); - this.add('fundrawtransaction', this.fundRawTransaction); - this.add('resendwallettransactions', this.resendWalletTransactions); - this.add('abandontransaction', this.abandonTransaction); - this.add('addmultisigaddress', this.addMultisigAddress); - this.add('addwitnessaddress', this.addWitnessAddress); - this.add('backupwallet', this.backupWallet); - this.add('dumpprivkey', this.dumpPrivKey); - this.add('dumpwallet', this.dumpWallet); - this.add('encryptwallet', this.encryptWallet); - this.add('getaccountaddress', this.getAccountAddress); - this.add('getaccount', this.getAccount); - this.add('getaddressesbyaccount', this.getAddressesByAccount); - this.add('getbalance', this.getBalance); - this.add('getnewaddress', this.getNewAddress); - this.add('getrawchangeaddress', this.getRawChangeAddress); - this.add('getreceivedbyaccount', this.getReceivedByAccount); - this.add('getreceivedbyaddress', this.getReceivedByAddress); - this.add('gettransaction', this.getTransaction); - this.add('getunconfirmedbalance', this.getUnconfirmedBalance); - this.add('getwalletinfo', this.getWalletInfo); - this.add('importprivkey', this.importPrivKey); - this.add('importwallet', this.importWallet); - this.add('importaddress', this.importAddress); - this.add('importprunedfunds', this.importPrunedFunds); - this.add('importpubkey', this.importPubkey); - this.add('keypoolrefill', this.keyPoolRefill); - this.add('listaccounts', this.listAccounts); - this.add('listaddressgroupings', this.listAddressGroupings); - this.add('listlockunspent', this.listLockUnspent); - this.add('listreceivedbyaccount', this.listReceivedByAccount); - this.add('listreceivedbyaddress', this.listReceivedByAddress); - this.add('listsinceblock', this.listSinceBlock); - this.add('listtransactions', this.listTransactions); - this.add('listunspent', this.listUnspent); - this.add('lockunspent', this.lockUnspent); - this.add('move', this.move); - this.add('sendfrom', this.sendFrom); - this.add('sendmany', this.sendMany); - this.add('sendtoaddress', this.sendToAddress); - this.add('setaccount', this.setAccount); - this.add('settxfee', this.setTXFee); - this.add('signmessage', this.signMessage); - this.add('walletlock', this.walletLock); - this.add('walletpassphrasechange', this.walletPassphraseChange); - this.add('walletpassphrase', this.walletPassphrase); - this.add('removeprunedfunds', this.removePrunedFunds); - this.add('selectwallet', this.selectWallet); - this.add('getmemoryinfo', this.getMemoryInfo); - this.add('setloglevel', this.setLogLevel); -}; - -RPC.prototype.help = async function help(args, _help) { - if (args.length === 0) - return 'Select a command.'; - - const json = { - method: args[0], - params: [] - }; - - return await this.execute(json, true); -}; - -RPC.prototype.stop = async function stop(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'stop'); - - this.wdb.close(); - - return 'Stopping.'; -}; - -RPC.prototype.fundRawTransaction = async function fundRawTransaction(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'fundrawtransaction "hexstring" ( options )'); + this.init(); } - const wallet = this.wallet; - const valid = new Validator(args); - const data = valid.buf(0); - const options = valid.obj(1); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); - - const tx = MTX.fromRaw(data); - - if (tx.outputs.length === 0) { - throw new RPCError(errs.INVALID_PARAMETER, - 'TX must have at least one output.'); + init() { + this.add('help', this.help); + this.add('stop', this.stop); + this.add('fundrawtransaction', this.fundRawTransaction); + this.add('resendwallettransactions', this.resendWalletTransactions); + this.add('abandontransaction', this.abandonTransaction); + this.add('addmultisigaddress', this.addMultisigAddress); + this.add('addwitnessaddress', this.addWitnessAddress); + this.add('backupwallet', this.backupWallet); + this.add('dumpprivkey', this.dumpPrivKey); + this.add('dumpwallet', this.dumpWallet); + this.add('encryptwallet', this.encryptWallet); + this.add('getaccountaddress', this.getAccountAddress); + this.add('getaccount', this.getAccount); + this.add('getaddressesbyaccount', this.getAddressesByAccount); + this.add('getbalance', this.getBalance); + this.add('getnewaddress', this.getNewAddress); + this.add('getrawchangeaddress', this.getRawChangeAddress); + this.add('getreceivedbyaccount', this.getReceivedByAccount); + this.add('getreceivedbyaddress', this.getReceivedByAddress); + this.add('gettransaction', this.getTransaction); + this.add('getunconfirmedbalance', this.getUnconfirmedBalance); + this.add('getwalletinfo', this.getWalletInfo); + this.add('importprivkey', this.importPrivKey); + this.add('importwallet', this.importWallet); + this.add('importaddress', this.importAddress); + this.add('importprunedfunds', this.importPrunedFunds); + this.add('importpubkey', this.importPubkey); + this.add('keypoolrefill', this.keyPoolRefill); + this.add('listaccounts', this.listAccounts); + this.add('listaddressgroupings', this.listAddressGroupings); + this.add('listlockunspent', this.listLockUnspent); + this.add('listreceivedbyaccount', this.listReceivedByAccount); + this.add('listreceivedbyaddress', this.listReceivedByAddress); + this.add('listsinceblock', this.listSinceBlock); + this.add('listtransactions', this.listTransactions); + this.add('listunspent', this.listUnspent); + this.add('lockunspent', this.lockUnspent); + this.add('move', this.move); + this.add('sendfrom', this.sendFrom); + this.add('sendmany', this.sendMany); + this.add('sendtoaddress', this.sendToAddress); + this.add('setaccount', this.setAccount); + this.add('settxfee', this.setTXFee); + this.add('signmessage', this.signMessage); + this.add('walletlock', this.walletLock); + this.add('walletpassphrasechange', this.walletPassphraseChange); + this.add('walletpassphrase', this.walletPassphrase); + this.add('removeprunedfunds', this.removePrunedFunds); + this.add('selectwallet', this.selectWallet); + this.add('getmemoryinfo', this.getMemoryInfo); + this.add('setloglevel', this.setLogLevel); } - let rate = null; - let change = null; + async help(args, _help) { + if (args.length === 0) + return 'Select a command.'; - if (options) { - const valid = new Validator(options); + const json = { + method: args[0], + params: [] + }; - rate = valid.ufixed('feeRate', 8); - change = valid.str('changeAddress'); - - if (change) - change = parseAddress(change, this.network); + return await this.execute(json, true); } - await wallet.fund(tx, { - rate: rate, - changeAddress: change - }); + async stop(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'stop'); - return { - hex: tx.toRaw().toString('hex'), - changepos: tx.changeIndex, - fee: Amount.btc(tx.getFee(), true) - }; -}; + this.wdb.close(); + + return 'Stopping.'; + } + + async fundRawTransaction(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'fundrawtransaction "hexstring" ( options )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const data = valid.buf(0); + const options = valid.obj(1); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.'); + + const tx = MTX.fromRaw(data); + + if (tx.outputs.length === 0) { + throw new RPCError(errs.INVALID_PARAMETER, + 'TX must have at least one output.'); + } + + let rate = null; + let change = null; + + if (options) { + const valid = new Validator(options); + + rate = valid.ufixed('feeRate', 8); + change = valid.str('changeAddress'); + + if (change) + change = parseAddress(change, this.network); + } + + await wallet.fund(tx, { + rate: rate, + changeAddress: change + }); + + return { + hex: tx.toRaw().toString('hex'), + changepos: tx.changeIndex, + fee: Amount.btc(tx.getFee(), true) + }; + } /* * Wallet */ -RPC.prototype.resendWalletTransactions = async function resendWalletTransactions(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'resendwallettransactions'); + async resendWalletTransactions(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'resendwallettransactions'); - const wallet = this.wallet; - const txs = await wallet.resend(); - const hashes = []; + const wallet = this.wallet; + const txs = await wallet.resend(); + const hashes = []; - for (const tx of txs) - hashes.push(tx.txid()); + for (const tx of txs) + hashes.push(tx.txid()); - return hashes; -}; - -RPC.prototype.addMultisigAddress = async function addMultisigAddress(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'addmultisigaddress nrequired ["key",...] ( "account" )'); + return hashes; } - // Impossible to implement in bcoin (no address book). - throw new Error('Not implemented.'); -}; + async addMultisigAddress(args, help) { + if (help || args.length < 2 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'addmultisigaddress nrequired ["key",...] ( "account" )'); + } -RPC.prototype.addWitnessAddress = async function addWitnessAddress(args, help) { - if (help || args.length < 1 || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'addwitnessaddress "address"'); + // Impossible to implement in bcoin (no address book). + throw new Error('Not implemented.'); + } - // Unlikely to be implemented. - throw new Error('Not implemented.'); -}; + async addWitnessAddress(args, help) { + if (help || args.length < 1 || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'addwitnessaddress "address"'); -RPC.prototype.backupWallet = async function backupWallet(args, help) { - const valid = new Validator(args); - const dest = valid.str(0); + // Unlikely to be implemented. + throw new Error('Not implemented.'); + } - if (help || args.length !== 1 || !dest) - throw new RPCError(errs.MISC_ERROR, 'backupwallet "destination"'); + async backupWallet(args, help) { + const valid = new Validator(args); + const dest = valid.str(0); - await this.wdb.backup(dest); + if (help || args.length !== 1 || !dest) + throw new RPCError(errs.MISC_ERROR, 'backupwallet "destination"'); - return null; -}; + await this.wdb.backup(dest); -RPC.prototype.dumpPrivKey = async function dumpPrivKey(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'dumpprivkey "bitcoinaddress"'); + return null; + } - const wallet = this.wallet; - const valid = new Validator(args); - const addr = valid.str(0, ''); + async dumpPrivKey(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'dumpprivkey "bitcoinaddress"'); - const hash = parseHash(addr, this.network); - const ring = await wallet.getPrivateKey(hash); + const wallet = this.wallet; + const valid = new Validator(args); + const addr = valid.str(0, ''); - if (!ring) - throw new RPCError(errs.MISC_ERROR, 'Key not found.'); - - return ring.toSecret(this.network); -}; - -RPC.prototype.dumpWallet = async function dumpWallet(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'dumpwallet "filename"'); - - const wallet = this.wallet; - const valid = new Validator(args); - const file = valid.str(0); - - if (!file) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - - const tip = await this.wdb.getTip(); - const time = util.date(); - - const out = [ - util.fmt('# Wallet Dump created by Bcoin %s', pkg.version), - util.fmt('# * Created on %s', time), - util.fmt('# * Best block at time of backup was %d (%s).', - tip.height, util.revHex(tip.hash)), - util.fmt('# * File: %s', file), - '' - ]; - - const hashes = await wallet.getAddressHashes(); - - for (const hash of hashes) { + const hash = parseHash(addr, this.network); const ring = await wallet.getPrivateKey(hash); if (!ring) - continue; + throw new RPCError(errs.MISC_ERROR, 'Key not found.'); - const addr = ring.getAddress('string'); - - let fmt = '%s %s label= addr=%s'; - - if (ring.branch === 1) - fmt = '%s %s change=1 addr=%s'; - - const str = util.fmt(fmt, ring.toSecret(this.network), time, addr); - - out.push(str); + return ring.toSecret(this.network); } - out.push(''); - out.push('# End of dump'); - out.push(''); + async dumpWallet(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'dumpwallet "filename"'); - const dump = out.join('\n'); + const wallet = this.wallet; + const valid = new Validator(args); + const file = valid.str(0); - if (fs.unsupported) - return dump; + if (!file) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - await fs.writeFile(file, dump, 'utf8'); + const tip = await this.wdb.getTip(); + const time = util.date(); - return null; -}; + const out = [ + util.fmt('# Wallet Dump created by Bcoin %s', pkg.version), + util.fmt('# * Created on %s', time), + util.fmt('# * Best block at time of backup was %d (%s).', + tip.height, util.revHex(tip.hash)), + util.fmt('# * File: %s', file), + '' + ]; -RPC.prototype.encryptWallet = async function encryptWallet(args, help) { - const wallet = this.wallet; + const hashes = await wallet.getAddressHashes(); - if (!wallet.master.encrypted && (help || args.length !== 1)) - throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"'); + for (const hash of hashes) { + const ring = await wallet.getPrivateKey(hash); - const valid = new Validator(args); - const passphrase = valid.str(0, ''); + if (!ring) + continue; - if (wallet.master.encrypted) { - throw new RPCError(errs.WALLET_WRONG_ENC_STATE, - 'Already running with an encrypted wallet.'); - } + const addr = ring.getAddress('string'); - if (passphrase.length < 1) - throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"'); + let fmt = '%s %s label= addr=%s'; - try { - await wallet.encrypt(passphrase); - } catch (e) { - throw new RPCError(errs.WALLET_ENCRYPTION_FAILED, 'Encryption failed.'); - } + if (ring.branch === 1) + fmt = '%s %s change=1 addr=%s'; - return 'wallet encrypted; we do not need to stop!'; -}; + const str = util.fmt(fmt, ring.toSecret(this.network), time, addr); -RPC.prototype.getAccountAddress = async function getAccountAddress(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getaccountaddress "account"'); - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0, ''); - - if (!name) - name = 'default'; - - const addr = await wallet.receiveAddress(name); - - if (!addr) - return ''; - - return addr.toString(this.network); -}; - -RPC.prototype.getAccount = async function getAccount(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getaccount "bitcoinaddress"'); - - const wallet = this.wallet; - const valid = new Validator(args); - const addr = valid.str(0, ''); - - const hash = parseHash(addr, this.network); - const path = await wallet.getPath(hash); - - if (!path) - return ''; - - return path.name; -}; - -RPC.prototype.getAddressesByAccount = async function getAddressesByAccount(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getaddressesbyaccount "account"'); - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0, ''); - const addrs = []; - - if (name === '') - name = 'default'; - - const paths = await wallet.getPaths(name); - - for (const path of paths) { - const addr = path.toAddress(); - addrs.push(addr.toString(this.network)); - } - - return addrs; -}; - -RPC.prototype.getBalance = async function getBalance(args, help) { - if (help || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'getbalance ( "account" minconf includeWatchonly )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - const minconf = valid.u32(1, 1); - const watchOnly = valid.bool(2, false); - - if (name === '') - name = 'default'; - - if (name === '*') - name = null; - - if (wallet.watchOnly !== watchOnly) - return 0; - - const balance = await wallet.getBalance(name); - - let value; - if (minconf > 0) - value = balance.confirmed; - else - value = balance.unconfirmed; - - return Amount.btc(value, true); -}; - -RPC.prototype.getNewAddress = async function getNewAddress(args, help) { - if (help || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'getnewaddress ( "account" )'); - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - - if (name === '') - name = 'default'; - - const addr = await wallet.createReceive(name); - - return addr.getAddress('string'); -}; - -RPC.prototype.getRawChangeAddress = async function getRawChangeAddress(args, help) { - if (help || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress'); - - const wallet = this.wallet; - const addr = await wallet.createChange(); - - return addr.getAddress('string'); -}; - -RPC.prototype.getReceivedByAccount = async function getReceivedByAccount(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'getreceivedbyaccount "account" ( minconf )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - const minconf = valid.u32(1, 0); - const height = this.wdb.state.height; - - if (name === '') - name = 'default'; - - const paths = await wallet.getPaths(name); - const filter = new Set(); - - for (const path of paths) - filter.add(path.hash); - - const txs = await wallet.getHistory(name); - - let total = 0; - let lastConf = -1; - - for (const wtx of txs) { - const conf = wtx.getDepth(height); - - if (conf < minconf) - continue; - - if (lastConf === -1 || conf < lastConf) - lastConf = conf; - - for (const output of wtx.tx.outputs) { - const hash = output.getHash('hex'); - if (hash && filter.has(hash)) - total += output.value; + out.push(str); } + + out.push(''); + out.push('# End of dump'); + out.push(''); + + const dump = out.join('\n'); + + if (fs.unsupported) + return dump; + + await fs.writeFile(file, dump, 'utf8'); + + return null; } - return Amount.btc(total, true); -}; + async encryptWallet(args, help) { + const wallet = this.wallet; -RPC.prototype.getReceivedByAddress = async function getReceivedByAddress(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'getreceivedbyaddress "bitcoinaddress" ( minconf )'); - } + if (!wallet.master.encrypted && (help || args.length !== 1)) + throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"'); - const wallet = this.wallet; - const valid = new Validator(args); - const addr = valid.str(0, ''); - const minconf = valid.u32(1, 0); - const height = this.wdb.state.height; + const valid = new Validator(args); + const passphrase = valid.str(0, ''); - const hash = parseHash(addr, this.network); - const txs = await wallet.getHistory(); - - let total = 0; - - for (const wtx of txs) { - if (wtx.getDepth(height) < minconf) - continue; - - for (const output of wtx.tx.outputs) { - if (output.getHash('hex') === hash) - total += output.value; + if (wallet.master.encrypted) { + throw new RPCError(errs.WALLET_WRONG_ENC_STATE, + 'Already running with an encrypted wallet.'); } - } - return Amount.btc(total, true); -}; + if (passphrase.length < 1) + throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"'); -RPC.prototype._toWalletTX = async function _toWalletTX(wtx) { - const wallet = this.wallet; - const details = await wallet.toDetails(wtx); - - if (!details) - throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); - - let receive = true; - for (const member of details.inputs) { - if (member.path) { - receive = false; - break; + try { + await wallet.encrypt(passphrase); + } catch (e) { + throw new RPCError(errs.WALLET_ENCRYPTION_FAILED, 'Encryption failed.'); } + + return 'wallet encrypted; we do not need to stop!'; } - const det = []; - let sent = 0; - let received = 0; + async getAccountAddress(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'getaccountaddress "account"'); - for (let i = 0; i < details.outputs.length; i++) { - const member = details.outputs[i]; + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0, ''); - if (member.path) { - if (member.path.branch === 1) + if (!name) + name = 'default'; + + const addr = await wallet.receiveAddress(name); + + if (!addr) + return ''; + + return addr.toString(this.network); + } + + async getAccount(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'getaccount "bitcoinaddress"'); + + const wallet = this.wallet; + const valid = new Validator(args); + const addr = valid.str(0, ''); + + const hash = parseHash(addr, this.network); + const path = await wallet.getPath(hash); + + if (!path) + return ''; + + return path.name; + } + + async getAddressesByAccount(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'getaddressesbyaccount "account"'); + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0, ''); + const addrs = []; + + if (name === '') + name = 'default'; + + const paths = await wallet.getPaths(name); + + for (const path of paths) { + const addr = path.toAddress(); + addrs.push(addr.toString(this.network)); + } + + return addrs; + } + + async getBalance(args, help) { + if (help || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'getbalance ( "account" minconf includeWatchonly )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const minconf = valid.u32(1, 1); + const watchOnly = valid.bool(2, false); + + if (name === '') + name = 'default'; + + if (name === '*') + name = null; + + if (wallet.watchOnly !== watchOnly) + return 0; + + const balance = await wallet.getBalance(name); + + let value; + if (minconf > 0) + value = balance.confirmed; + else + value = balance.unconfirmed; + + return Amount.btc(value, true); + } + + async getNewAddress(args, help) { + if (help || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'getnewaddress ( "account" )'); + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + + if (name === '') + name = 'default'; + + const addr = await wallet.createReceive(name); + + return addr.getAddress('string'); + } + + async getRawChangeAddress(args, help) { + if (help || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress'); + + const wallet = this.wallet; + const addr = await wallet.createChange(); + + return addr.getAddress('string'); + } + + async getReceivedByAccount(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'getreceivedbyaccount "account" ( minconf )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const minconf = valid.u32(1, 0); + const height = this.wdb.state.height; + + if (name === '') + name = 'default'; + + const paths = await wallet.getPaths(name); + const filter = new Set(); + + for (const path of paths) + filter.add(path.hash); + + const txs = await wallet.getHistory(name); + + let total = 0; + let lastConf = -1; + + for (const wtx of txs) { + const conf = wtx.getDepth(height); + + if (conf < minconf) + continue; + + if (lastConf === -1 || conf < lastConf) + lastConf = conf; + + for (const output of wtx.tx.outputs) { + const hash = output.getHash('hex'); + if (hash && filter.has(hash)) + total += output.value; + } + } + + return Amount.btc(total, true); + } + + async getReceivedByAddress(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'getreceivedbyaddress "bitcoinaddress" ( minconf )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const addr = valid.str(0, ''); + const minconf = valid.u32(1, 0); + const height = this.wdb.state.height; + + const hash = parseHash(addr, this.network); + const txs = await wallet.getHistory(); + + let total = 0; + + for (const wtx of txs) { + if (wtx.getDepth(height) < minconf) + continue; + + for (const output of wtx.tx.outputs) { + if (output.getHash('hex') === hash) + total += output.value; + } + } + + return Amount.btc(total, true); + } + + async _toWalletTX(wtx) { + const wallet = this.wallet; + const details = await wallet.toDetails(wtx); + + if (!details) + throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); + + let receive = true; + for (const member of details.inputs) { + if (member.path) { + receive = false; + break; + } + } + + const det = []; + let sent = 0; + let received = 0; + + for (let i = 0; i < details.outputs.length; i++) { + const member = details.outputs[i]; + + if (member.path) { + if (member.path.branch === 1) + continue; + + det.push({ + account: member.path.name, + address: member.address.toString(this.network), + category: 'receive', + amount: Amount.btc(member.value, true), + label: member.path.name, + vout: i + }); + + received += member.value; + + continue; + } + + if (receive) continue; det.push({ - account: member.path.name, - address: member.address.toString(this.network), - category: 'receive', - amount: Amount.btc(member.value, true), - label: member.path.name, + account: '', + address: member.address + ? member.address.toString(this.network) + : null, + category: 'send', + amount: -(Amount.btc(member.value, true)), + fee: -(Amount.btc(details.fee, true)), vout: i }); - received += member.value; - - continue; + sent += member.value; } - if (receive) - continue; + return { + amount: Amount.btc(receive ? received : -sent, true), + confirmations: details.confirmations, + blockhash: details.block ? util.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.time, + txid: util.revHex(details.hash), + walletconflicts: [], + time: details.mtime, + timereceived: details.mtime, + 'bip125-replaceable': 'no', + details: det, + hex: details.tx.toRaw().toString('hex') + }; + } - det.push({ - account: '', + async getTransaction(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'gettransaction "txid" ( includeWatchonly )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const hash = valid.rhash(0); + const watchOnly = valid.bool(1, false); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter'); + + const wtx = await wallet.getTX(hash); + + if (!wtx) + throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); + + return await this._toWalletTX(wtx, watchOnly); + } + + async abandonTransaction(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'abandontransaction "txid"'); + + const wallet = this.wallet; + const valid = new Validator(args); + const hash = valid.rhash(0); + + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + + const result = await wallet.abandon(hash); + + if (!result) + throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.'); + + return null; + } + + async getUnconfirmedBalance(args, help) { + if (help || args.length > 0) + throw new RPCError(errs.MISC_ERROR, 'getunconfirmedbalance'); + + const wallet = this.wallet; + const balance = await wallet.getBalance(); + + return Amount.btc(balance.unconfirmed, true); + } + + async getWalletInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getwalletinfo'); + + const wallet = this.wallet; + const balance = await wallet.getBalance(); + + return { + walletid: wallet.id, + walletversion: 6, + balance: Amount.btc(balance.unconfirmed, true), + unconfirmed_balance: Amount.btc(balance.unconfirmed, true), + txcount: balance.tx, + keypoololdest: 0, + keypoolsize: 0, + unlocked_until: wallet.master.until, + paytxfee: Amount.btc(this.wdb.feeRate, true) + }; + } + + async importPrivKey(args, help) { + if (help || args.length < 1 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'importprivkey "bitcoinprivkey" ( "label" rescan )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const secret = valid.str(0); + const rescan = valid.bool(2, false); + + const key = parseSecret(secret, this.network); + + await wallet.importKey(0, key); + + if (rescan) + await this.wdb.rescan(0); + + return null; + } + + async importWallet(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'importwallet "filename" ( rescan )'); + + const wallet = this.wallet; + const valid = new Validator(args); + const file = valid.str(0); + const rescan = valid.bool(1, false); + + if (fs.unsupported) + throw new RPCError(errs.INTERNAL_ERROR, 'FS not available.'); + + const data = await fs.readFile(file, 'utf8'); + const lines = data.split(/\n+/); + const keys = []; + + for (let line of lines) { + line = line.trim(); + + if (line.length === 0) + continue; + + if (/^\s*#/.test(line)) + continue; + + const parts = line.split(/\s+/); + + if (parts.length < 4) + throw new RPCError(errs.DESERIALIZATION_ERROR, 'Malformed wallet.'); + + const secret = parseSecret(parts[0], this.network); + + keys.push(secret); + } + + for (const key of keys) + await wallet.importKey(0, key); + + if (rescan) + await this.wdb.rescan(0); + + return null; + } + + async importAddress(args, help) { + if (help || args.length < 1 || args.length > 4) { + throw new RPCError(errs.MISC_ERROR, + 'importaddress "address" ( "label" rescan p2sh )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let addr = valid.str(0, ''); + const rescan = valid.bool(2, false); + const p2sh = valid.bool(3, false); + + if (p2sh) { + let script = valid.buf(0); + + if (!script) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.'); + + script = Script.fromRaw(script); + script = Script.fromScripthash(script.hash160()); + + addr = script.getAddress(); + } else { + addr = parseAddress(addr, this.network); + } + + await wallet.importAddress(0, addr); + + if (rescan) + await this.wdb.rescan(0); + + return null; + } + + async importPubkey(args, help) { + if (help || args.length < 1 || args.length > 4) { + throw new RPCError(errs.MISC_ERROR, + 'importpubkey "pubkey" ( "label" rescan )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const data = valid.buf(0); + const rescan = valid.bool(2, false); + + if (!data) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + + const key = KeyRing.fromPublic(data, this.network); + + await wallet.importKey(0, key); + + if (rescan) + await this.wdb.rescan(0); + + return null; + } + + async keyPoolRefill(args, help) { + if (help || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'keypoolrefill ( newsize )'); + return null; + } + + async listAccounts(args, help) { + if (help || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'listaccounts ( minconf includeWatchonly)'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + const minconf = valid.u32(0, 0); + const watchOnly = valid.bool(1, false); + + const accounts = await wallet.getAccounts(); + const map = {}; + + for (const account of accounts) { + const balance = await wallet.getBalance(account); + let value = balance.unconfirmed; + + if (minconf > 0) + value = balance.confirmed; + + if (wallet.watchOnly !== watchOnly) + value = 0; + + map[account] = Amount.btc(value, true); + } + + return map; + } + + async listAddressGroupings(args, help) { + if (help) + throw new RPCError(errs.MISC_ERROR, 'listaddressgroupings'); + throw new Error('Not implemented.'); + } + + async listLockUnspent(args, help) { + if (help || args.length > 0) + throw new RPCError(errs.MISC_ERROR, 'listlockunspent'); + + const wallet = this.wallet; + const outpoints = wallet.getLocked(); + const out = []; + + for (const outpoint of outpoints) { + out.push({ + txid: outpoint.txid(), + vout: outpoint.index + }); + } + + return out; + } + + async listReceivedByAccount(args, help) { + if (help || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'listreceivedbyaccount ( minconf includeempty includeWatchonly )'); + } + + const valid = new Validator(args); + const minconf = valid.u32(0, 0); + const includeEmpty = valid.bool(1, false); + const watchOnly = valid.bool(2, false); + + return await this._listReceived(minconf, includeEmpty, watchOnly, true); + } + + async listReceivedByAddress(args, help) { + if (help || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'listreceivedbyaddress ( minconf includeempty includeWatchonly )'); + } + + const valid = new Validator(args); + const minconf = valid.u32(0, 0); + const includeEmpty = valid.bool(1, false); + const watchOnly = valid.bool(2, false); + + return await this._listReceived(minconf, includeEmpty, watchOnly, false); + } + + async _listReceived(minconf, empty, watchOnly, account) { + const wallet = this.wallet; + const paths = await wallet.getPaths(); + const height = this.wdb.state.height; + + const map = new Map(); + for (const path of paths) { + const addr = path.toAddress(); + map.set(path.hash, { + involvesWatchonly: wallet.watchOnly, + address: addr.toString(this.network), + account: path.name, + amount: 0, + confirmations: -1, + label: '' + }); + } + + const txs = await wallet.getHistory(); + + for (const wtx of txs) { + const conf = wtx.getDepth(height); + + if (conf < minconf) + continue; + + for (const output of wtx.tx.outputs) { + const addr = output.getAddress(); + + if (!addr) + continue; + + const hash = addr.getHash('hex'); + const entry = map.get(hash); + + if (entry) { + if (entry.confirmations === -1 || conf < entry.confirmations) + entry.confirmations = conf; + entry.address = addr.toString(this.network); + entry.amount += output.value; + } + } + } + + let out = []; + for (const entry of map.values()) + out.push(entry); + + if (account) { + const map = new Map(); + + for (const entry of out) { + const item = map.get(entry.account); + if (!item) { + map.set(entry.account, entry); + entry.address = undefined; + continue; + } + item.amount += entry.amount; + } + + out = []; + + for (const entry of map.values()) + out.push(entry); + } + + const result = []; + for (const entry of out) { + if (!empty && entry.amount === 0) + continue; + + if (entry.confirmations === -1) + entry.confirmations = 0; + + entry.amount = Amount.btc(entry.amount, true); + result.push(entry); + } + + return result; + } + + async listSinceBlock(args, help) { + const wallet = this.wallet; + const chainHeight = this.wdb.state.height; + const valid = new Validator(args); + const block = valid.rhash(0); + const minconf = valid.u32(1, 0); + const watchOnly = valid.bool(2, false); + + if (help) { + throw new RPCError(errs.MISC_ERROR, + 'listsinceblock ( "blockhash" target-confirmations includeWatchonly)'); + } + + if (wallet.watchOnly !== watchOnly) + return []; + + let height = -1; + if (block) { + const entry = await this.client.getEntry(block); + if (entry) + height = entry.height; + } + + if (height === -1) + height = this.chain.height; + + const txs = await wallet.getHistory(); + + const out = []; + let highest; + for (const wtx of txs) { + if (wtx.height < height) + continue; + + if (wtx.getDepth(chainHeight) < minconf) + continue; + + if (!highest || wtx.height > highest) + highest = wtx; + + const json = await this._toListTX(wtx); + + out.push(json); + } + + return { + transactions: out, + lastblock: highest && highest.block + ? util.revHex(highest.block) + : encoding.NULL_HASH + }; + } + + async _toListTX(wtx) { + const wallet = this.wallet; + const details = await wallet.toDetails(wtx); + + if (!details) + throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); + + let receive = true; + for (const member of details.inputs) { + if (member.path) { + receive = false; + break; + } + } + + let sent = 0; + let received = 0; + let sendMember, recMember, sendIndex, recIndex; + for (let i = 0; i < details.outputs.length; i++) { + const member = details.outputs[i]; + + if (member.path) { + if (member.path.branch === 1) + continue; + received += member.value; + recMember = member; + recIndex = i; + continue; + } + + sent += member.value; + sendMember = member; + sendIndex = i; + } + + let member, index; + if (receive) { + member = recMember; + index = recIndex; + } else { + member = sendMember; + index = sendIndex; + } + + // In the odd case where we send to ourselves. + if (!member) { + assert(!receive); + member = recMember; + index = recIndex; + } + + return { + account: member.path ? member.path.name : '', address: member.address ? member.address.toString(this.network) : null, - category: 'send', - amount: -(Amount.btc(member.value, true)), - fee: -(Amount.btc(details.fee, true)), - vout: i - }); - - sent += member.value; + category: receive ? 'receive' : 'send', + amount: Amount.btc(receive ? received : -sent, true), + label: member.path ? member.path.name : undefined, + vout: index, + confirmations: details.getDepth(), + blockhash: details.block ? util.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.time, + txid: util.revHex(details.hash), + walletconflicts: [], + time: details.mtime, + timereceived: details.mtime, + 'bip125-replaceable': 'no' + }; } - return { - amount: Amount.btc(receive ? received : -sent, true), - confirmations: details.confirmations, - blockhash: details.block ? util.revHex(details.block) : null, - blockindex: details.index, - blocktime: details.time, - txid: util.revHex(details.hash), - walletconflicts: [], - time: details.mtime, - timereceived: details.mtime, - 'bip125-replaceable': 'no', - details: det, - hex: details.tx.toRaw().toString('hex') - }; -}; + async listTransactions(args, help) { + if (help || args.length > 4) { + throw new RPCError(errs.MISC_ERROR, + 'listtransactions ( "account" count from includeWatchonly)'); + } -RPC.prototype.getTransaction = async function getTransaction(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'gettransaction "txid" ( includeWatchonly )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const hash = valid.rhash(0); - const watchOnly = valid.bool(1, false); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter'); - - const wtx = await wallet.getTX(hash); - - if (!wtx) - throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); - - return await this._toWalletTX(wtx, watchOnly); -}; - -RPC.prototype.abandonTransaction = async function abandonTransaction(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'abandontransaction "txid"'); - - const wallet = this.wallet; - const valid = new Validator(args); - const hash = valid.rhash(0); - - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - - const result = await wallet.abandon(hash); - - if (!result) - throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.'); - - return null; -}; - -RPC.prototype.getUnconfirmedBalance = async function getUnconfirmedBalance(args, help) { - if (help || args.length > 0) - throw new RPCError(errs.MISC_ERROR, 'getunconfirmedbalance'); - - const wallet = this.wallet; - const balance = await wallet.getBalance(); - - return Amount.btc(balance.unconfirmed, true); -}; - -RPC.prototype.getWalletInfo = async function getWalletInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getwalletinfo'); - - const wallet = this.wallet; - const balance = await wallet.getBalance(); - - return { - walletid: wallet.id, - walletversion: 6, - balance: Amount.btc(balance.unconfirmed, true), - unconfirmed_balance: Amount.btc(balance.unconfirmed, true), - txcount: balance.tx, - keypoololdest: 0, - keypoolsize: 0, - unlocked_until: wallet.master.until, - paytxfee: Amount.btc(this.wdb.feeRate, true) - }; -}; - -RPC.prototype.importPrivKey = async function importPrivKey(args, help) { - if (help || args.length < 1 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'importprivkey "bitcoinprivkey" ( "label" rescan )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const secret = valid.str(0); - const rescan = valid.bool(2, false); - - const key = parseSecret(secret, this.network); - - await wallet.importKey(0, key); - - if (rescan) - await this.wdb.rescan(0); - - return null; -}; - -RPC.prototype.importWallet = async function importWallet(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'importwallet "filename" ( rescan )'); - - const wallet = this.wallet; - const valid = new Validator(args); - const file = valid.str(0); - const rescan = valid.bool(1, false); - - if (fs.unsupported) - throw new RPCError(errs.INTERNAL_ERROR, 'FS not available.'); - - const data = await fs.readFile(file, 'utf8'); - const lines = data.split(/\n+/); - const keys = []; - - for (let line of lines) { - line = line.trim(); - - if (line.length === 0) - continue; - - if (/^\s*#/.test(line)) - continue; - - const parts = line.split(/\s+/); - - if (parts.length < 4) - throw new RPCError(errs.DESERIALIZATION_ERROR, 'Malformed wallet.'); - - const secret = parseSecret(parts[0], this.network); - - keys.push(secret); - } - - for (const key of keys) - await wallet.importKey(0, key); - - if (rescan) - await this.wdb.rescan(0); - - return null; -}; - -RPC.prototype.importAddress = async function importAddress(args, help) { - if (help || args.length < 1 || args.length > 4) { - throw new RPCError(errs.MISC_ERROR, - 'importaddress "address" ( "label" rescan p2sh )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - let addr = valid.str(0, ''); - const rescan = valid.bool(2, false); - const p2sh = valid.bool(3, false); - - if (p2sh) { - let script = valid.buf(0); - - if (!script) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.'); - - script = Script.fromRaw(script); - script = Script.fromScripthash(script.hash160()); - - addr = script.getAddress(); - } else { - addr = parseAddress(addr, this.network); - } - - await wallet.importAddress(0, addr); - - if (rescan) - await this.wdb.rescan(0); - - return null; -}; - -RPC.prototype.importPubkey = async function importPubkey(args, help) { - if (help || args.length < 1 || args.length > 4) { - throw new RPCError(errs.MISC_ERROR, - 'importpubkey "pubkey" ( "label" rescan )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const data = valid.buf(0); - const rescan = valid.bool(2, false); - - if (!data) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - - const key = KeyRing.fromPublic(data, this.network); - - await wallet.importKey(0, key); - - if (rescan) - await this.wdb.rescan(0); - - return null; -}; - -RPC.prototype.keyPoolRefill = async function keyPoolRefill(args, help) { - if (help || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'keypoolrefill ( newsize )'); - return null; -}; - -RPC.prototype.listAccounts = async function listAccounts(args, help) { - if (help || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'listaccounts ( minconf includeWatchonly)'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const minconf = valid.u32(0, 0); - const watchOnly = valid.bool(1, false); - - const accounts = await wallet.getAccounts(); - const map = {}; - - for (const account of accounts) { - const balance = await wallet.getBalance(account); - let value = balance.unconfirmed; - - if (minconf > 0) - value = balance.confirmed; + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const count = valid.u32(1, 10); + const from = valid.u32(2, 0); + const watchOnly = valid.bool(3, false); if (wallet.watchOnly !== watchOnly) - value = 0; + return []; - map[account] = Amount.btc(value, true); + if (name === '') + name = 'default'; + + const txs = await wallet.getHistory(); + + common.sortTX(txs); + + const end = from + count; + const to = Math.min(end, txs.length); + const out = []; + + for (let i = from; i < to; i++) { + const wtx = txs[i]; + const json = await this._toListTX(wtx); + out.push(json); + } + + return out; } - return map; -}; + async listUnspent(args, help) { + if (help || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'listunspent ( minconf maxconf ["address",...] )'); + } -RPC.prototype.listAddressGroupings = async function listAddressGroupings(args, help) { - if (help) - throw new RPCError(errs.MISC_ERROR, 'listaddressgroupings'); - throw new Error('Not implemented.'); -}; + const wallet = this.wallet; + const valid = new Validator(args); + const minDepth = valid.u32(0, 1); + const maxDepth = valid.u32(1, 9999999); + const addrs = valid.array(2); + const height = this.wdb.state.height; -RPC.prototype.listLockUnspent = async function listLockUnspent(args, help) { - if (help || args.length > 0) - throw new RPCError(errs.MISC_ERROR, 'listlockunspent'); + const map = new Set(); - const wallet = this.wallet; - const outpoints = wallet.getLocked(); - const out = []; + if (addrs) { + const valid = new Validator(addrs); + for (let i = 0; i < addrs.length; i++) { + const addr = valid.str(i, ''); + const hash = parseHash(addr, this.network); - for (const outpoint of outpoints) { - out.push({ - txid: outpoint.txid(), - vout: outpoint.index - }); - } + if (map.has(hash)) + throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address.'); - return out; -}; + map.add(hash); + } + } -RPC.prototype.listReceivedByAccount = async function listReceivedByAccount(args, help) { - if (help || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'listreceivedbyaccount ( minconf includeempty includeWatchonly )'); - } + const coins = await wallet.getCoins(); - const valid = new Validator(args); - const minconf = valid.u32(0, 0); - const includeEmpty = valid.bool(1, false); - const watchOnly = valid.bool(2, false); + common.sortCoins(coins); - return await this._listReceived(minconf, includeEmpty, watchOnly, true); -}; + const out = []; -RPC.prototype.listReceivedByAddress = async function listReceivedByAddress(args, help) { - if (help || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'listreceivedbyaddress ( minconf includeempty includeWatchonly )'); - } + for (const coin of coins) { + const depth = coin.getDepth(height); - const valid = new Validator(args); - const minconf = valid.u32(0, 0); - const includeEmpty = valid.bool(1, false); - const watchOnly = valid.bool(2, false); + if (depth < minDepth || depth > maxDepth) + continue; - return await this._listReceived(minconf, includeEmpty, watchOnly, false); -}; - -RPC.prototype._listReceived = async function _listReceived(minconf, empty, watchOnly, account) { - const wallet = this.wallet; - const paths = await wallet.getPaths(); - const height = this.wdb.state.height; - - const map = new Map(); - for (const path of paths) { - const addr = path.toAddress(); - map.set(path.hash, { - involvesWatchonly: wallet.watchOnly, - address: addr.toString(this.network), - account: path.name, - amount: 0, - confirmations: -1, - label: '' - }); - } - - const txs = await wallet.getHistory(); - - for (const wtx of txs) { - const conf = wtx.getDepth(height); - - if (conf < minconf) - continue; - - for (const output of wtx.tx.outputs) { - const addr = output.getAddress(); + const addr = coin.getAddress(); if (!addr) continue; - const hash = addr.getHash('hex'); - const entry = map.get(hash); + const hash = coin.getHash('hex'); - if (entry) { - if (entry.confirmations === -1 || conf < entry.confirmations) - entry.confirmations = conf; - entry.address = addr.toString(this.network); - entry.amount += output.value; + if (addrs) { + if (!hash || !map.has(hash)) + continue; } + + const ring = await wallet.getKey(hash); + + out.push({ + txid: coin.txid(), + vout: coin.index, + address: addr ? addr.toString(this.network) : null, + account: ring ? ring.name : undefined, + redeemScript: ring && ring.script + ? ring.script.toJSON() + : undefined, + scriptPubKey: coin.script.toJSON(), + amount: Amount.btc(coin.value, true), + confirmations: depth, + spendable: !wallet.isLocked(coin), + solvable: true + }); } + + return out; } - let out = []; - for (const entry of map.values()) - out.push(entry); + async lockUnspent(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'lockunspent unlock ([{"txid":"txid","vout":n},...])'); + } - if (account) { - const map = new Map(); + const wallet = this.wallet; + const valid = new Validator(args); + const unlock = valid.bool(0, false); + const outputs = valid.array(1); - for (const entry of out) { - const item = map.get(entry.account); - if (!item) { - map.set(entry.account, entry); - entry.address = undefined; + if (args.length === 1) { + if (unlock) + wallet.unlockCoins(); + return true; + } + + if (!outputs) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + + for (const output of outputs) { + const valid = new Validator(output); + const hash = valid.rhash('txid'); + const index = valid.u32('vout'); + + if (hash == null || index == null) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); + + const outpoint = new Outpoint(hash, index); + + if (unlock) { + wallet.unlockCoin(outpoint); continue; } - item.amount += entry.amount; + + wallet.lockCoin(outpoint); } - out = []; - - for (const entry of map.values()) - out.push(entry); - } - - const result = []; - for (const entry of out) { - if (!empty && entry.amount === 0) - continue; - - if (entry.confirmations === -1) - entry.confirmations = 0; - - entry.amount = Amount.btc(entry.amount, true); - result.push(entry); - } - - return result; -}; - -RPC.prototype.listSinceBlock = async function listSinceBlock(args, help) { - const wallet = this.wallet; - const chainHeight = this.wdb.state.height; - const valid = new Validator(args); - const block = valid.rhash(0); - const minconf = valid.u32(1, 0); - const watchOnly = valid.bool(2, false); - - if (help) { - throw new RPCError(errs.MISC_ERROR, - 'listsinceblock ( "blockhash" target-confirmations includeWatchonly)'); - } - - if (wallet.watchOnly !== watchOnly) - return []; - - let height = -1; - if (block) { - const entry = await this.client.getEntry(block); - if (entry) - height = entry.height; - } - - if (height === -1) - height = this.chain.height; - - const txs = await wallet.getHistory(); - - const out = []; - let highest; - for (const wtx of txs) { - if (wtx.height < height) - continue; - - if (wtx.getDepth(chainHeight) < minconf) - continue; - - if (!highest || wtx.height > highest) - highest = wtx; - - const json = await this._toListTX(wtx); - - out.push(json); - } - - return { - transactions: out, - lastblock: highest && highest.block - ? util.revHex(highest.block) - : encoding.NULL_HASH - }; -}; - -RPC.prototype._toListTX = async function _toListTX(wtx) { - const wallet = this.wallet; - const details = await wallet.toDetails(wtx); - - if (!details) - throw new RPCError(errs.WALLET_ERROR, 'TX not found.'); - - let receive = true; - for (const member of details.inputs) { - if (member.path) { - receive = false; - break; - } - } - - let sent = 0; - let received = 0; - let sendMember, recMember, sendIndex, recIndex; - for (let i = 0; i < details.outputs.length; i++) { - const member = details.outputs[i]; - - if (member.path) { - if (member.path.branch === 1) - continue; - received += member.value; - recMember = member; - recIndex = i; - continue; - } - - sent += member.value; - sendMember = member; - sendIndex = i; - } - - let member, index; - if (receive) { - member = recMember; - index = recIndex; - } else { - member = sendMember; - index = sendIndex; - } - - // In the odd case where we send to ourselves. - if (!member) { - assert(!receive); - member = recMember; - index = recIndex; - } - - return { - account: member.path ? member.path.name : '', - address: member.address - ? member.address.toString(this.network) - : null, - category: receive ? 'receive' : 'send', - amount: Amount.btc(receive ? received : -sent, true), - label: member.path ? member.path.name : undefined, - vout: index, - confirmations: details.getDepth(), - blockhash: details.block ? util.revHex(details.block) : null, - blockindex: details.index, - blocktime: details.time, - txid: util.revHex(details.hash), - walletconflicts: [], - time: details.mtime, - timereceived: details.mtime, - 'bip125-replaceable': 'no' - }; -}; - -RPC.prototype.listTransactions = async function listTransactions(args, help) { - if (help || args.length > 4) { - throw new RPCError(errs.MISC_ERROR, - 'listtransactions ( "account" count from includeWatchonly)'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - const count = valid.u32(1, 10); - const from = valid.u32(2, 0); - const watchOnly = valid.bool(3, false); - - if (wallet.watchOnly !== watchOnly) - return []; - - if (name === '') - name = 'default'; - - const txs = await wallet.getHistory(); - - common.sortTX(txs); - - const end = from + count; - const to = Math.min(end, txs.length); - const out = []; - - for (let i = from; i < to; i++) { - const wtx = txs[i]; - const json = await this._toListTX(wtx); - out.push(json); - } - - return out; -}; - -RPC.prototype.listUnspent = async function listUnspent(args, help) { - if (help || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'listunspent ( minconf maxconf ["address",...] )'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const minDepth = valid.u32(0, 1); - const maxDepth = valid.u32(1, 9999999); - const addrs = valid.array(2); - const height = this.wdb.state.height; - - const map = new Set(); - - if (addrs) { - const valid = new Validator(addrs); - for (let i = 0; i < addrs.length; i++) { - const addr = valid.str(i, ''); - const hash = parseHash(addr, this.network); - - if (map.has(hash)) - throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address.'); - - map.add(hash); - } - } - - const coins = await wallet.getCoins(); - - common.sortCoins(coins); - - const out = []; - - for (const coin of coins) { - const depth = coin.getDepth(height); - - if (depth < minDepth || depth > maxDepth) - continue; - - const addr = coin.getAddress(); - - if (!addr) - continue; - - const hash = coin.getHash('hex'); - - if (addrs) { - if (!hash || !map.has(hash)) - continue; - } - - const ring = await wallet.getKey(hash); - - out.push({ - txid: coin.txid(), - vout: coin.index, - address: addr ? addr.toString(this.network) : null, - account: ring ? ring.name : undefined, - redeemScript: ring && ring.script - ? ring.script.toJSON() - : undefined, - scriptPubKey: coin.script.toJSON(), - amount: Amount.btc(coin.value, true), - confirmations: depth, - spendable: !wallet.isLocked(coin), - solvable: true - }); - } - - return out; -}; - -RPC.prototype.lockUnspent = async function lockUnspent(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'lockunspent unlock ([{"txid":"txid","vout":n},...])'); - } - - const wallet = this.wallet; - const valid = new Validator(args); - const unlock = valid.bool(0, false); - const outputs = valid.array(1); - - if (args.length === 1) { - if (unlock) - wallet.unlockCoins(); return true; } - if (!outputs) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + async move(args, help) { + // Not implementing: stupid and deprecated. + throw new Error('Not implemented.'); + } - for (const output of outputs) { - const valid = new Validator(output); - const hash = valid.rhash('txid'); - const index = valid.u32('vout'); - - if (hash == null || index == null) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); - - const outpoint = new Outpoint(hash, index); - - if (unlock) { - wallet.unlockCoin(outpoint); - continue; + async sendFrom(args, help) { + if (help || args.length < 3 || args.length > 6) { + throw new RPCError(errs.MISC_ERROR, + 'sendfrom "fromaccount" "tobitcoinaddress"' + + ' amount ( minconf "comment" "comment-to" )'); } - wallet.lockCoin(outpoint); + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const str = valid.str(1); + const value = valid.ufixed(2, 8); + const minconf = valid.u32(3, 0); + + const addr = parseAddress(str, this.network); + + if (!addr || value == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + + if (name === '') + name = 'default'; + + const options = { + account: name, + depth: minconf, + outputs: [{ + address: addr, + value: value + }] + }; + + const tx = await wallet.send(options); + + return tx.txid(); } - return true; -}; + async sendMany(args, help) { + if (help || args.length < 2 || args.length > 5) { + throw new RPCError(errs.MISC_ERROR, + 'sendmany "fromaccount" {"address":amount,...}' + + ' ( minconf "comment" ["address",...] )'); + } -RPC.prototype.move = async function move(args, help) { - // Not implementing: stupid and deprecated. - throw new Error('Not implemented.'); -}; + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const sendTo = valid.obj(1); + const minconf = valid.u32(2, 1); + const subtract = valid.bool(4, false); -RPC.prototype.sendFrom = async function sendFrom(args, help) { - if (help || args.length < 3 || args.length > 6) { - throw new RPCError(errs.MISC_ERROR, - 'sendfrom "fromaccount" "tobitcoinaddress"' - + ' amount ( minconf "comment" "comment-to" )'); + if (name === '') + name = 'default'; + + if (!sendTo) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + + const to = new Validator(sendTo); + const uniq = new Set(); + const outputs = []; + + for (const key of Object.keys(sendTo)) { + const value = to.ufixed(key, 8); + const addr = parseAddress(key, this.network); + const hash = addr.getHash('hex'); + + if (value == null) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); + + if (uniq.has(hash)) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); + + uniq.add(hash); + + const output = new Output(); + output.value = value; + output.script.fromAddress(addr); + outputs.push(output); + } + + const options = { + outputs: outputs, + subtractFee: subtract, + account: name, + depth: minconf + }; + + const tx = await wallet.send(options); + + return tx.txid(); } - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - const str = valid.str(1); - const value = valid.ufixed(2, 8); - const minconf = valid.u32(3, 0); + async sendToAddress(args, help) { + if (help || args.length < 2 || args.length > 5) { + throw new RPCError(errs.MISC_ERROR, + 'sendtoaddress "bitcoinaddress" amount' + + ' ( "comment" "comment-to" subtractfeefromamount )'); + } - const addr = parseAddress(str, this.network); + const wallet = this.wallet; + const valid = new Validator(args); + const str = valid.str(0); + const value = valid.ufixed(1, 8); + const subtract = valid.bool(4, false); - if (!addr || value == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + const addr = parseAddress(str, this.network); - if (name === '') - name = 'default'; + if (!addr || value == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - const options = { - account: name, - depth: minconf, - outputs: [{ - address: addr, - value: value - }] - }; + const options = { + subtractFee: subtract, + outputs: [{ + address: addr, + value: value + }] + }; - const tx = await wallet.send(options); + const tx = await wallet.send(options); - return tx.txid(); -}; - -RPC.prototype.sendMany = async function sendMany(args, help) { - if (help || args.length < 2 || args.length > 5) { - throw new RPCError(errs.MISC_ERROR, - 'sendmany "fromaccount" {"address":amount,...}' - + ' ( minconf "comment" ["address",...] )'); + return tx.txid(); } - const wallet = this.wallet; - const valid = new Validator(args); - let name = valid.str(0); - const sendTo = valid.obj(1); - const minconf = valid.u32(2, 1); - const subtract = valid.bool(4, false); + async setAccount(args, help) { + if (help || args.length < 1 || args.length > 2) { + throw new RPCError(errs.MISC_ERROR, + 'setaccount "bitcoinaddress" "account"'); + } - if (name === '') - name = 'default'; - - if (!sendTo) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - - const to = new Validator(sendTo); - const uniq = new Set(); - const outputs = []; - - for (const key of Object.keys(sendTo)) { - const value = to.ufixed(key, 8); - const addr = parseAddress(key, this.network); - const hash = addr.getHash('hex'); - - if (value == null) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); - - if (uniq.has(hash)) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); - - uniq.add(hash); - - const output = new Output(); - output.value = value; - output.script.fromAddress(addr); - outputs.push(output); + // Impossible to implement in bcoin: + throw new Error('Not implemented.'); } - const options = { - outputs: outputs, - subtractFee: subtract, - account: name, - depth: minconf - }; + async setTXFee(args, help) { + const valid = new Validator(args); + const rate = valid.ufixed(0, 8); - const tx = await wallet.send(options); + if (help || args.length < 1 || args.length > 1) + throw new RPCError(errs.MISC_ERROR, 'settxfee amount'); - return tx.txid(); -}; + if (rate == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); -RPC.prototype.sendToAddress = async function sendToAddress(args, help) { - if (help || args.length < 2 || args.length > 5) { - throw new RPCError(errs.MISC_ERROR, - 'sendtoaddress "bitcoinaddress" amount' - + ' ( "comment" "comment-to" subtractfeefromamount )'); + this.wdb.feeRate = rate; + + return true; } - const wallet = this.wallet; - const valid = new Validator(args); - const str = valid.str(0); - const value = valid.ufixed(1, 8); - const subtract = valid.bool(4, false); + async signMessage(args, help) { + if (help || args.length !== 2) { + throw new RPCError(errs.MISC_ERROR, + 'signmessage "bitcoinaddress" "message"'); + } - const addr = parseAddress(str, this.network); + const wallet = this.wallet; + const valid = new Validator(args); + const b58 = valid.str(0, ''); + const str = valid.str(1, ''); - if (!addr || value == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + const addr = parseHash(b58, this.network); - const options = { - subtractFee: subtract, - outputs: [{ - address: addr, - value: value - }] - }; + const ring = await wallet.getKey(addr); - const tx = await wallet.send(options); + if (!ring) + throw new RPCError(errs.WALLET_ERROR, 'Address not found.'); - return tx.txid(); -}; + if (!wallet.master.key) + throw new RPCError(errs.WALLET_UNLOCK_NEEDED, 'Wallet is locked.'); -RPC.prototype.setAccount = async function setAccount(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'setaccount "bitcoinaddress" "account"'); + const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); + const hash = digest.hash256(msg); + + const sig = ring.sign(hash); + + return sig.toString('base64'); } - // Impossible to implement in bcoin: - throw new Error('Not implemented.'); -}; + async walletLock(args, help) { + const wallet = this.wallet; -RPC.prototype.setTXFee = async function setTXFee(args, help) { - const valid = new Validator(args); - const rate = valid.ufixed(0, 8); + if (help || (wallet.master.encrypted && args.length !== 0)) + throw new RPCError(errs.MISC_ERROR, 'walletlock'); - if (help || args.length < 1 || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'settxfee amount'); + if (!wallet.master.encrypted) + throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); - if (rate == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + await wallet.lock(); - this.wdb.feeRate = rate; - - return true; -}; - -RPC.prototype.signMessage = async function signMessage(args, help) { - if (help || args.length !== 2) { - throw new RPCError(errs.MISC_ERROR, - 'signmessage "bitcoinaddress" "message"'); + return null; } - const wallet = this.wallet; - const valid = new Validator(args); - const b58 = valid.str(0, ''); - const str = valid.str(1, ''); + async walletPassphraseChange(args, help) { + const wallet = this.wallet; - const addr = parseHash(b58, this.network); + if (help || (wallet.master.encrypted && args.length !== 2)) { + throw new RPCError(errs.MISC_ERROR, 'walletpassphrasechange' + + ' "oldpassphrase" "newpassphrase"'); + } - const ring = await wallet.getKey(addr); + const valid = new Validator(args); + const old = valid.str(0, ''); + const passphrase = valid.str(1, ''); - if (!ring) - throw new RPCError(errs.WALLET_ERROR, 'Address not found.'); + if (!wallet.master.encrypted) + throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); - if (!wallet.master.key) - throw new RPCError(errs.WALLET_UNLOCK_NEEDED, 'Wallet is locked.'); + if (old.length < 1 || passphrase.length < 1) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter'); - const msg = Buffer.from(MAGIC_STRING + str, 'utf8'); - const hash = digest.hash256(msg); + await wallet.setPassphrase(passphrase, old); - const sig = ring.sign(hash); - - return sig.toString('base64'); -}; - -RPC.prototype.walletLock = async function walletLock(args, help) { - const wallet = this.wallet; - - if (help || (wallet.master.encrypted && args.length !== 0)) - throw new RPCError(errs.MISC_ERROR, 'walletlock'); - - if (!wallet.master.encrypted) - throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); - - await wallet.lock(); - - return null; -}; - -RPC.prototype.walletPassphraseChange = async function walletPassphraseChange(args, help) { - const wallet = this.wallet; - - if (help || (wallet.master.encrypted && args.length !== 2)) { - throw new RPCError(errs.MISC_ERROR, 'walletpassphrasechange' - + ' "oldpassphrase" "newpassphrase"'); + return null; } - const valid = new Validator(args); - const old = valid.str(0, ''); - const passphrase = valid.str(1, ''); + async walletPassphrase(args, help) { + const wallet = this.wallet; + const valid = new Validator(args); + const passphrase = valid.str(0, ''); + const timeout = valid.u32(1); - if (!wallet.master.encrypted) - throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); + if (help || (wallet.master.encrypted && args.length !== 2)) { + throw new RPCError(errs.MISC_ERROR, + 'walletpassphrase "passphrase" timeout'); + } - if (old.length < 1 || passphrase.length < 1) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter'); + if (!wallet.master.encrypted) + throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); - await wallet.setPassphrase(passphrase, old); + if (passphrase.length < 1) + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter'); - return null; -}; + if (timeout == null) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter'); -RPC.prototype.walletPassphrase = async function walletPassphrase(args, help) { - const wallet = this.wallet; - const valid = new Validator(args); - const passphrase = valid.str(0, ''); - const timeout = valid.u32(1); + await wallet.unlock(passphrase, timeout); - if (help || (wallet.master.encrypted && args.length !== 2)) { - throw new RPCError(errs.MISC_ERROR, - 'walletpassphrase "passphrase" timeout'); + return null; } - if (!wallet.master.encrypted) - throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); + async importPrunedFunds(args, help) { + if (help || args.length < 2 || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'importprunedfunds "rawtransaction" "txoutproof" ( "label" )'); + } - if (passphrase.length < 1) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter'); + const valid = new Validator(args); + const txRaw = valid.buf(0); + const blockRaw = valid.buf(1); - if (timeout == null) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter'); + if (!txRaw || !blockRaw) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - await wallet.unlock(passphrase, timeout); + const tx = TX.fromRaw(txRaw); + const block = MerkleBlock.fromRaw(blockRaw); + const hash = block.hash('hex'); - return null; -}; + if (!block.verify()) + throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); -RPC.prototype.importPrunedFunds = async function importPrunedFunds(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'importprunedfunds "rawtransaction" "txoutproof" ( "label" )'); + if (!block.hasTX(tx.hash('hex'))) + throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); + + const height = await this.client.getEntry(hash); + + if (height === -1) + throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); + + const entry = { + hash: hash, + time: block.time, + height: height + }; + + if (!await this.wdb.addTX(tx, entry)) + throw new RPCError(errs.WALLET_ERROR, 'No tracked address for TX.'); + + return null; } - const valid = new Validator(args); - const txRaw = valid.buf(0); - const blockRaw = valid.buf(1); + async removePrunedFunds(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'removeprunedfunds "txid"'); - if (!txRaw || !blockRaw) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + const wallet = this.wallet; + const valid = new Validator(args); + const hash = valid.rhash(0); - const tx = TX.fromRaw(txRaw); - const block = MerkleBlock.fromRaw(blockRaw); - const hash = block.hash('hex'); + if (!hash) + throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - if (!block.verify()) - throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); + if (!await wallet.remove(hash)) + throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.'); - if (!block.hasTX(tx.hash('hex'))) - throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); + return null; + } - const height = await this.client.getEntry(hash); + async selectWallet(args, help) { + const valid = new Validator(args); + const id = valid.str(0); - if (height === -1) - throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.'); + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'selectwallet "id"'); - const entry = { - hash: hash, - time: block.time, - height: height - }; + const wallet = await this.wdb.get(id); - if (!await this.wdb.addTX(tx, entry)) - throw new RPCError(errs.WALLET_ERROR, 'No tracked address for TX.'); + if (!wallet) + throw new RPCError(errs.WALLET_ERROR, 'Wallet not found.'); - return null; -}; + this.wallet = wallet; -RPC.prototype.removePrunedFunds = async function removePrunedFunds(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'removeprunedfunds "txid"'); + return null; + } - const wallet = this.wallet; - const valid = new Validator(args); - const hash = valid.rhash(0); + async getMemoryInfo(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo'); - if (!hash) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + return util.memoryUsage(); + } - if (!await wallet.remove(hash)) - throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.'); + async setLogLevel(args, help) { + if (help || args.length !== 1) + throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"'); - return null; -}; + const valid = new Validator(args); + const level = valid.str(0, ''); -RPC.prototype.selectWallet = async function selectWallet(args, help) { - const valid = new Validator(args); - const id = valid.str(0); + this.logger.setLevel(level); - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'selectwallet "id"'); - - const wallet = await this.wdb.get(id); - - if (!wallet) - throw new RPCError(errs.WALLET_ERROR, 'Wallet not found.'); - - this.wallet = wallet; - - return null; -}; - -RPC.prototype.getMemoryInfo = async function getMemoryInfo(args, help) { - if (help || args.length !== 0) - throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo'); - - return util.memoryUsage(); -}; - -RPC.prototype.setLogLevel = async function setLogLevel(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"'); - - const valid = new Validator(args); - const level = valid.str(0, ''); - - this.logger.setLevel(level); - - return null; -}; + return null; + } +} /* * Helpers diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 34aea36b..4db37f22 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -62,22 +62,19 @@ function WalletDB(options) { this.db = LDB(this.options); this.rpc = new RPC(this); this.primary = null; - this.http = null; - if (!HTTPServer.unsupported) { - 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.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; diff --git a/test/http-test.js b/test/http-test.js index 0f7cc4a6..2af57f40 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -14,6 +14,8 @@ const MTX = require('../lib/primitives/mtx'); const HTTP = require('../lib/http'); const FullNode = require('../lib/node/fullnode'); const pkg = require('../lib/pkg'); +const Network = require('../lib/protocol/network'); +const network = Network.get('regtest'); const node = new FullNode({ network: 'regtest', @@ -24,8 +26,13 @@ const node = new FullNode({ plugins: [require('../lib/wallet/plugin')] }); +const client = new HTTP.Client({ + port: network.rpcPort, + apiKey: 'foo' +}); + const wallet = new HTTP.Wallet({ - network: 'regtest', + port: network.rpcPort, apiKey: 'foo' }); @@ -40,6 +47,7 @@ describe('HTTP', function() { it('should open node', async () => { consensus.COINBASE_MATURITY = 0; await node.open(); + await client.open(); }); it('should create wallet', async () => { @@ -48,7 +56,7 @@ describe('HTTP', function() { }); it('should get info', async () => { - const info = await wallet.client.getInfo(); + const info = await client.getInfo(); assert.strictEqual(info.network, node.network.type); assert.strictEqual(info.version, pkg.version); assert.typeOf(info.pool, 'object'); @@ -154,12 +162,12 @@ describe('HTTP', function() { }); it('should execute an rpc call', async () => { - const info = await wallet.client.rpc.execute('getblockchaininfo', []); + const info = await client.execute('getblockchaininfo', []); assert.strictEqual(info.blocks, 0); }); it('should execute an rpc call with bool parameter', async () => { - const info = await wallet.client.rpc.execute('getrawmempool', [true]); + const info = await client.execute('getrawmempool', [true]); assert.deepStrictEqual(info, {}); }); @@ -188,7 +196,7 @@ describe('HTTP', function() { }); it('should get a block template', async () => { - const json = await wallet.client.rpc.execute('getblocktemplate', []); + const json = await client.execute('getblocktemplate', []); assert.deepStrictEqual(json, { capabilities: ['proposal'], mutable: ['time', 'transactions', 'prevblock'], @@ -223,7 +231,7 @@ describe('HTTP', function() { const attempt = await node.miner.createBlock(); const block = attempt.toBlock(); const hex = block.toRaw().toString('hex'); - const json = await wallet.client.rpc.execute('getblocktemplate', [{ + const json = await client.execute('getblocktemplate', [{ mode: 'proposal', data: hex }]); @@ -231,7 +239,7 @@ describe('HTTP', function() { }); it('should validate an address', async () => { - const json = await wallet.client.rpc.execute('validateaddress', [ + const json = await client.execute('validateaddress', [ addr.toString(node.network) ]); assert.deepStrictEqual(json, { @@ -246,6 +254,7 @@ describe('HTTP', function() { it('should cleanup', async () => { consensus.COINBASE_MATURITY = 100; await wallet.close(); + await client.close(); await node.close(); }); });