From 5cbbdbfb2f86a1a906ec39e4cf3bce18bf8955bf Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 8 Mar 2017 16:54:04 -0800 Subject: [PATCH] refactor: config, plugins, and walletdb. --- lib/http/base.js | 1057 ++++++++++++++++------ lib/http/client.js | 47 +- lib/http/rpc.js | 1838 +++---------------------------------- lib/http/server.js | 1800 +++++-------------------------------- lib/http/wallet.js | 2 + lib/mining/miner.js | 6 +- lib/mining/minerblock.js | 24 +- lib/net/pool.js | 1 - lib/node/config.js | 54 +- lib/node/fullnode.js | 32 +- lib/node/node.js | 75 +- lib/node/spvnode.js | 33 +- lib/utils/index.js | 1 + lib/utils/util.js | 27 + lib/utils/validator.js | 428 +++++++++ lib/wallet/common.js | 3 + lib/wallet/http.js | 1087 ++++++++++++++++++++++ lib/wallet/rpc.js | 1847 ++++++++++++++++++++++++++++++++++++++ lib/wallet/wallet.js | 2 +- lib/wallet/walletdb.js | 114 +++ test/http-test.js | 10 +- test/node-test.js | 11 +- 22 files changed, 4825 insertions(+), 3674 deletions(-) create mode 100644 lib/utils/validator.js create mode 100644 lib/wallet/http.js create mode 100644 lib/wallet/rpc.js diff --git a/lib/http/base.js b/lib/http/base.js index 851c9b2a..2acd97b2 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -7,11 +7,19 @@ 'use strict'; +/* jshint -W069 */ + var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var URL = require('url'); +var StringDecoder = require('string_decoder').StringDecoder; var AsyncObject = require('../utils/asyncobject'); var util = require('../utils/util'); -var URL = require('url'); var co = require('../utils/co'); +var Validator = require('../utils/validator'); +var List = require('../utils/list'); +var fs = require('../utils/fs'); +var crypto = require('../crypto/crypto'); /** * HTTPBase @@ -27,11 +35,14 @@ function HTTPBase(options) { AsyncObject.call(this); - this.options = new HTTPBaseOptions(options); + this.config = new HTTPBaseOptions(options); + this.config.load(); this.server = null; this.io = null; + this.sockets = new List(); this.routes = new Routes(); + this.mounts = []; this.stack = []; this.hooks = []; @@ -47,13 +58,13 @@ util.inherits(HTTPBase, AsyncObject); HTTPBase.prototype._init = function _init() { var self = this; - var backend = this.options.getBackend(); - var options = this.options.toHTTP(); + var backend = this.config.getBackend(); + var options = this.config.toHTTP(); this.server = backend.createServer(options); this._initRouter(); - this._initIO(); + this._initSockets(); this.server.on('connection', function(socket) { socket.on('error', function(err) { @@ -89,13 +100,17 @@ HTTPBase.prototype._init = function _init() { HTTPBase.prototype._initRouter = function _initRouter() { var self = this; - this.server.on('request', co(function* (req, res) { + this.server.on('request', co(function* (hreq, hres) { + var req = new Request(hreq, hres, hreq.url); + var res = new Response(hreq, hres); + + req.on('error', nop); + try { + req.pause(); yield self.handleRequest(req, res); } catch (e) { - if (!res.sent) - res.error(e); - + res.error(e.statusCode || 500, e); self.emit('error', e); } })); @@ -112,15 +127,14 @@ HTTPBase.prototype._initRouter = function _initRouter() { HTTPBase.prototype.handleRequest = co(function* handleRequest(req, res) { var i, routes, route, params; - initRequest(req, res, this.options.keyLimit); + if (yield this.handleMounts(req, res)) + return; this.emit('request', req, res); if (yield this.handleStack(req, res)) return; - req.body = yield this.parseBody(req); - routes = this.routes.getHandlers(req.method); if (!routes) @@ -145,6 +159,129 @@ HTTPBase.prototype.handleRequest = co(function* handleRequest(req, res) { throw new Error('No routes found for path: ' + req.pathname); }); +/** + * CORS middleware. + * @returns {Function} + */ + +HTTPBase.prototype.cors = function cors() { + return co(function* (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'); + + if (req.method === 'OPTIONS') { + res.setStatus(200); + res.end(); + return; + } + }); +}; + +/** + * Basic auth middleware. + * @param {Object} options + * @returns {Function} + */ + +HTTPBase.prototype.basicAuth = function basicAuth(options) { + var user = options.username; + var pass = options.password; + var realm = options.realm; + + if (!Buffer.isBuffer(user)) { + assert(typeof user === 'string'); + user = new Buffer(user, 'utf8'); + } + + if (!Buffer.isBuffer(pass)) { + assert(typeof pass === 'string'); + pass = new Buffer(pass, 'utf8'); + } + + user = crypto.hash256(user); + pass = crypto.hash256(pass); + + // XXX + user = null; + + if (!realm) + realm = 'server'; + + assert(typeof realm === 'string'); + + function fail(res) { + res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); + res.setStatus(401); + res.end(); + } + + return co(function* (req, res) { + var auth = req.headers['authorization']; + var parts, username, password, digest; + + if (!auth) + return fail(res); + + parts = auth.split(' '); + + if (parts.length !== 2) + return fail(res); + + if (parts[0] !== 'Basic') + return fail(res); + + auth = new Buffer(parts[1], 'base64').toString('utf8'); + parts = auth.split(':'); + + username = parts.shift(); + password = parts.join(':'); + + if (user) { + digest = new Buffer(username, 'utf8'); + digest = crypto.hash256(digest); + + if (!crypto.ccmp(digest, user)) + return fail(res); + } + + digest = new Buffer(password, 'utf8'); + digest = crypto.hash256(digest); + + if (!crypto.ccmp(digest, pass)) + return fail(res); + + req.username = username; + }); +}; + +/** + * Body parser middleware. + * @param {Object} options + * @returns {Function} + */ + +HTTPBase.prototype.bodyParser = function bodyParser(options) { + var self = this; + var opt = new BodyParserOptions(options); + + return co(function* (req, res) { + if (req.hasBody) + return; + + try { + req.resume(); + req.body = yield self.parseBody(req, opt); + } finally { + req.pause(); + } + + req.hasBody = true; + }); +}; + /** * Parse request body. * @private @@ -152,7 +289,7 @@ HTTPBase.prototype.handleRequest = co(function* handleRequest(req, res) { * @returns {Promise} */ -HTTPBase.prototype.parseBody = co(function* parseBody(req) { +HTTPBase.prototype.parseBody = co(function* parseBody(req, opt) { var body = Object.create(null); var type = req.contentType; var data; @@ -160,20 +297,20 @@ HTTPBase.prototype.parseBody = co(function* parseBody(req) { if (req.method === 'GET') return body; - data = yield this.readBody(req, 'utf8'); + data = yield this.readBody(req, 'utf8', opt); if (!data) return body; - if (this.options.contentType) - type = this.options.contentType; + if (opt.contentType) + type = opt.contentType; switch (type) { case 'json': body = JSON.parse(data); break; case 'form': - body = parsePairs(data, this.options.keyLimit); + body = parsePairs(data, opt.keyLimit); break; default: break; @@ -189,10 +326,10 @@ HTTPBase.prototype.parseBody = co(function* parseBody(req) { * @returns {Promise} */ -HTTPBase.prototype.readBody = function readBody(req, enc) { +HTTPBase.prototype.readBody = function readBody(req, enc, opt) { var self = this; return new Promise(function(resolve, reject) { - return self._readBody(req, enc, resolve, reject); + return self._readBody(req, enc, opt, resolve, reject); }); }; @@ -205,9 +342,7 @@ HTTPBase.prototype.readBody = function readBody(req, enc) { * @param {Function} reject */ -HTTPBase.prototype._readBody = function _readBody(req, enc, resolve, reject) { - var self = this; - var StringDecoder = require('string_decoder').StringDecoder; +HTTPBase.prototype._readBody = function _readBody(req, enc, opt, resolve, reject) { var decode = new StringDecoder(enc); var hasData = false; var total = 0; @@ -218,7 +353,7 @@ HTTPBase.prototype._readBody = function _readBody(req, enc, resolve, reject) { timer = null; cleanup(); reject(new Error('Request body timed out.')); - }, 10 * 1000); + }, opt.timeout); function cleanup() { req.removeListener('data', onData); @@ -235,7 +370,7 @@ HTTPBase.prototype._readBody = function _readBody(req, enc, resolve, reject) { total += data.length; hasData = true; - if (total > self.options.bodyLimit) { + if (total > opt.bodyLimit) { reject(new Error('Request body overflow.')); return; } @@ -264,6 +399,38 @@ HTTPBase.prototype._readBody = function _readBody(req, enc, resolve, reject) { req.on('end', onEnd); }; +/** + * Handle mount stack. + * @private + * @param {HTTPRequest} req + * @param {HTTPResponse} res + * @returns {Promise} + */ + +HTTPBase.prototype.handleMounts = co(function* handleMounts(req, res) { + var url = req.url; + var i, route, server; + + for (i = 0; i < this.mounts.length; i++) { + route = this.mounts[i]; + 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); + + yield server.handleRequest(req, res); + + return true; + } + + return false; +}); + /** * Handle middleware stack. * @private @@ -317,11 +484,11 @@ HTTPBase.prototype.handleHooks = co(function* handleHooks(req, res) { * @private */ -HTTPBase.prototype._initIO = function _initIO() { +HTTPBase.prototype._initSockets = function _initSockets() { var self = this; var IOServer; - if (!this.options.sockets) + if (!this.config.sockets) return; try { @@ -339,11 +506,73 @@ HTTPBase.prototype._initIO = function _initIO() { this.io.attach(this.server); - this.io.on('connection', function(socket) { - self.emit('websocket', socket); + this.io.on('connection', function(ws) { + self.addSocket(ws); }); }; +/** + * Broadcast event to channel. + * @param {String} channel + * @param {String} type + * @param {...Object} args + */ + +HTTPBase.prototype.to = function to(channel) { + var args = new Array(arguments.length - 1); + var i, socket; + + for (i = 1; i < arguments.length; i++) + args[i - 1] = arguments[i]; + + for (socket = this.sockets.head; socket; socket = socket.next) { + if (socket.channels[channel]) + socket.emit.apply(socket, args); + } +}; + +/** + * Broadcast event to all connections. + * @param {String} channel + * @param {String} type + * @param {...Object} args + */ + +HTTPBase.prototype.all = function all() { + var socket; + + for (socket = this.sockets.head; socket; socket = socket.next) + socket.emit.apply(socket, arguments); +}; + +/** + * Initialize websockets. + * @private + */ + +HTTPBase.prototype.addSocket = function addSocket(ws) { + var self = this; + var socket = new WebSocket(ws); + var i, route; + + ws.on('error', function(err) { + self.emit('error', err); + }); + + ws.on('disconnect', function() { + assert(self.sockets.remove(socket)); + }); + + this.sockets.push(socket); + + for (i = 0; i < this.mounts.length; i++) { + route = this.mounts[i]; + route.handler.addSocket(ws); + } + + this.emit('socket', socket); +}; + /** * Open the server. * @alias HTTPBase#open @@ -351,7 +580,7 @@ HTTPBase.prototype._initIO = function _initIO() { */ HTTPBase.prototype._open = function open() { - return this.listen(this.options.port, this.options.host); + return this.listen(this.config.port, this.config.host); }; /** @@ -380,6 +609,21 @@ HTTPBase.prototype._close = function close() { }); }; +/** + * 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 @@ -392,7 +636,7 @@ HTTPBase.prototype.use = function use(path, handler, ctx) { handler = path; path = null; } - this.stack.push(new Route(ctx, path, handler)); + this.stack.push(new Route(ctx || this, path, handler)); }; /** @@ -407,7 +651,7 @@ HTTPBase.prototype.hook = function hook(path, handler, ctx) { handler = path; path = null; } - this.hooks.push(new Route(ctx, path, handler)); + this.hooks.push(new Route(ctx || this, path, handler)); }; /** @@ -418,7 +662,7 @@ HTTPBase.prototype.hook = function hook(path, handler, ctx) { */ HTTPBase.prototype.get = function get(path, handler, ctx) { - this.routes.get.push(new Route(ctx, path, handler)); + this.routes.get.push(new Route(ctx || this, path, handler)); }; /** @@ -429,7 +673,7 @@ HTTPBase.prototype.get = function get(path, handler, ctx) { */ HTTPBase.prototype.post = function post(path, handler, ctx) { - this.routes.post.push(new Route(ctx, path, handler)); + this.routes.post.push(new Route(ctx || this, path, handler)); }; /** @@ -440,7 +684,7 @@ HTTPBase.prototype.post = function post(path, handler, ctx) { */ HTTPBase.prototype.put = function put(path, handler, ctx) { - this.routes.put.push(new Route(ctx, path, handler)); + this.routes.put.push(new Route(ctx || this, path, handler)); }; /** @@ -451,7 +695,7 @@ HTTPBase.prototype.put = function put(path, handler, ctx) { */ HTTPBase.prototype.del = function del(path, handler, ctx) { - this.routes.del.push(new Route(ctx, path, handler)); + this.routes.del.push(new Route(ctx || this, path, handler)); }; /** @@ -501,18 +745,15 @@ function HTTPBaseOptions(options) { this.host = '127.0.0.1'; this.port = 8080; - this.sockets = false; + this.sockets = true; this.ssl = false; + this.keyFile = null; + this.certFile = null; this.key = null; this.cert = null; this.ca = null; - this.keyLimit = 100; - this.bodyLimit = 20 << 20; - - this.contentType = null; - if (options) this.fromOptions(options); } @@ -543,6 +784,28 @@ HTTPBaseOptions.prototype.fromOptions = function fromOptions(options) { this.sockets = options.sockets; } + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.keyFile = this.prefix + '/key.pem'; + this.certFile = this.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; @@ -558,31 +821,30 @@ HTTPBaseOptions.prototype.fromOptions = function fromOptions(options) { this.ca = options.ca; } - 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.ssl != null) { - assert(typeof options.ssl === 'boolean'); - assert(this.key, 'SSL specified with no provided key.'); - assert(this.cert, 'SSL specified with no provided cert.'); - this.ssl = options.ssl; - } - - if (options.contentType != null) { - assert(typeof options.contentType === 'string'); - this.contentType = options.contentType; + 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; }; +/** + * 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); +}; + /** * Instantiate http server options from object. * @param {Object} options @@ -620,6 +882,54 @@ HTTPBaseOptions.prototype.toHTTP = function toHTTP() { }; }; +/** + * 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 @@ -653,7 +963,9 @@ function Route(ctx, path, handler) { } } - assert(typeof handler === 'function'); + assert(handler); + assert(typeof handler === 'function' || typeof handler === 'object'); + this.handler = handler; } @@ -757,205 +1069,77 @@ Routes.prototype.getHandlers = function getHandlers(method) { } }; -/* - * Helpers +/** + * Request + * @constructor + * @ignore */ -function nop() {} +function Request(req, res, url) { + if (!(this instanceof Request)) + return new Request(req, res, url); -function initRequest(req, res, limit) { - var parsed; + EventEmitter.call(this); - req.on('error', nop); + 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; - assert(req.contentType == null); - assert(req.pathname == null); - assert(req.path == null); - assert(req.query == null); - assert(req.params == null); - assert(req.body == null); - - req.contentType = parseType(req.headers['content-type']); - req.pathname = ''; - req.path = []; - req.query = Object.create(null); - req.params = Object.create(null); - req.body = Object.create(null); - - assert(req.options == null); - assert(req.username == null); - assert(req.password == null); - assert(req.admin == null); - assert(req.wallet == null); - - req.options = Object.create(null); - req.username = null; - req.password = null; - req.admin = false; - req.wallet = null; - - assert(res.sent == null); - assert(res.send == null); - assert(res.error == null); - assert(res.redirect == null); - - res.sent = false; - res.send = makeSend(res); - res.error = makeSendError(req, res); - res.redirect = makeRedirect(res); - - parsed = parseURL(req.url, limit); - - req.url = parsed.url; - req.pathname = parsed.pathname; - req.path = parsed.parts; - req.query = parsed.query; - - return parsed; + if (req) + this.init(req, res, url); } -function makeSend(res) { - return function send(code, msg, type) { - return sendResponse(res, code, msg, type); - }; -} +util.inherits(Request, EventEmitter); -function sendResponse(res, code, msg, type) { - var len; +Request.prototype.init = function init(req, res, url) { + var self = this; - if (res.sent) - return; + assert(req); + assert(res); - assert(typeof code === 'number', 'Code must be a number.'); + 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']); - if (msg == null) - msg = { error: 'No message.' }; + req.on('error', function(err) { + self.emit('error', err); + }); - 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.'); - } + req.on('data', function(data) { + self.emit('data', data); + }); - if (!type) - type = typeof msg === 'string' ? 'txt' : 'bin'; + req.on('end', function() { + self.emit('end'); + }); - res.statusCode = code; - res.setHeader('Content-Type', getType(type)); - res.sent = true; + if (url != null) + this.parse(url); +}; - if (typeof msg === 'string') { - len = Buffer.byteLength(msg, 'utf8'); - res.setHeader('Content-Length', len + ''); - try { - res.write(msg, 'utf8'); - res.end(); - } catch (e) { - ; - } - return; - } - - if (Buffer.isBuffer(msg)) { - res.setHeader('Content-Length', msg.length + ''); - try { - res.write(msg); - res.end(); - } catch (e) { - ; - } - return; - } - - assert(false, 'Bad object passed to send.'); -} - -function makeSendError(req, res) { - return function error(err) { - return sendError(req, res, err); - }; -} - -function sendError(req, res, err) { - var code, msg; - - if (res.sent) - return; - - code = err.statusCode; - msg = err.message; - - if (!code) - code = 400; - - if (typeof msg !== 'string') - msg += ''; - - res.send(code, { error: msg }); - - try { - req.destroy(); - req.socket.destroy(); - } catch (e) { - ; - } -} - -function makeRedirect(res) { - return function redirect(code, url) { - if (!url) { - url = code; - code = 301; - } - res.statusCode = code; - res.setHeader('Location', url); - res.end(); - }; -} - -function parsePairs(str, limit) { - var parts = str.split('&'); - var data = Object.create(null); - var i, index, pair, key, value; - - assert(!limit || parts.length <= limit, 'Too many keys in querystring.'); - - for (i = 0; i < parts.length; i++) { - pair = parts[i]; - index = pair.indexOf('='); - - 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 parseURL(str, limit) { - var uri = URL.parse(str); - var parsed = new ParsedURL(str); +Request.prototype.parse = function parse(url) { + var uri = URL.parse(url); var pathname = uri.pathname; var query = Object.create(null); var trailing = false; - var path, parts, url; + var path, parts; if (pathname) { pathname = pathname.replace(/\/{2,}/g, '/'); @@ -1006,24 +1190,266 @@ function parseURL(str, limit) { } if (uri.query) - query = parsePairs(uri.query, limit); + query = parsePairs(uri.query, 100); - parsed.url = url; - parsed.pathname = pathname; - parsed.parts = parts; - parsed.query = query; - parsed.trailing = trailing; + this.url = url; + this.pathname = pathname; + this.path = parts; + this.query = query; + this.trailing = trailing; +}; - return parsed; +Request.prototype.rewrite = function rewrite(url) { + var req = new Request(); + req.init(this.req, this.res, url); + req.body = this.body; + req.hasBody = this.hasBody; + return req; +}; + +Request.prototype.valid = function valid() { + return new Validator([this.query, this.params, this.body]); +}; + +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); } -function ParsedURL(original) { - this.original = original; - this.url = null; - this.pathname = null; - this.parts = null; - this.query = null; - this.trailing = false; +util.inherits(Response, EventEmitter); + +Response.prototype.init = function init(req, res) { + var self = this; + + assert(req); + assert(res); + + res.on('error', function(err) { + self.emit('error', err); + }); + + res.on('drain', function() { + self.emit('drain'); + }); + + res.on('close', function() { + self.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) { + var msg; + + if (this.sent) + return; + + msg = err.message; + + if (!code) + code = 400; + + if (typeof msg !== 'string') + msg += ''; + + this.send(code, { + error: { + type: err.type || 'Error', + message: err.stack || msg, + code: err.code + } + }); + + try { + this.req.destroy(); + this.req.socket.destroy(); + } catch (e) { + ; + } +}; + +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) { + var len; + + 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') { + len = Buffer.byteLength(msg, 'utf8'); + this.setHeader('Content-Length', len + ''); + try { + this.write(msg, 'utf8'); + this.end(); + } catch (e) { + ; + } + return; + } + + if (Buffer.isBuffer(msg)) { + this.setHeader('Content-Length', msg.length + ''); + try { + this.write(msg); + this.end(); + } catch (e) { + ; + } + return; + } + + assert(false, 'Bad object passed to send.'); +}; + +/* + * Helpers + */ + +function nop() {} + +function parsePairs(str, limit) { + var parts = str.split('&'); + var data = Object.create(null); + var i, index, pair, key, value; + + if (parts.length > limit) + return data; + + assert(!limit || parts.length <= limit, 'Too many keys in querystring.'); + + for (i = 0; i < parts.length; i++) { + pair = parts[i]; + index = pair.indexOf('='); + + 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) { @@ -1041,6 +1467,8 @@ function getType(type) { 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': @@ -1050,7 +1478,7 @@ function getType(type) { case 'bin': return 'application/octet-stream'; default: - throw new Error('Unknown type: ' + type); + return type; } } @@ -1083,6 +1511,119 @@ function parseType(type) { } } +/** + * WebSocket + * @constructor + * @ignore + * @param {SocketIO.Socket} + */ + +function WebSocket(socket) { + if (!(this instanceof WebSocket)) + return new WebSocket(socket); + + EventEmitter.call(this); + + this.socket = socket; + this.remoteAddress = socket.conn.remoteAddress; + this.hooks = {}; + this.channels = {}; + this.auth = false; + this.prev = null; + this.next = null; + + this.init(); +} + +util.inherits(WebSocket, EventEmitter); + +WebSocket.prototype.init = function init() { + var self = this; + var socket = this.socket; + var onevent = socket.onevent.bind(socket); + + socket.onevent = function(packet) { + var result = onevent(packet); + self.onevent(packet); + return result; + }; + + socket.on('error', function(err) { + self.dispatch('error', err); + }); + + socket.on('disconnect', function() { + self.dispatch('disconnect'); + }); +}; + +WebSocket.prototype.onevent = co(function* onevent(packet) { + var socket = this.socket; + var args = (packet.data || []).slice(); + var type = args.shift() || ''; + var ack, result; + + if (typeof args[args.length - 1] === 'function') + ack = args.pop(); + else + ack = this.socket.ack(packet.id); + + this.dispatch(type, args); + + try { + result = yield this.fire(type, args); + } catch (e) { + ack({ + type: e.type || 'Error', + message: e.stack, + 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.'); + if (this.hooks[type]) + return; + this.hooks[type] = handler; +}; + +WebSocket.prototype.fire = co(function* fire(type, args) { + var handler = this.hooks[type]; + + if (!handler) + return; + + return yield handler(args); +}); + +WebSocket.prototype.join = function join(channel) { + this.channels[channel] = true; +}; + +WebSocket.prototype.leave = function leave(channel) { + delete this.channels[channel]; +}; + +WebSocket.prototype.dispatch = function dispatch() { + var emit = EventEmitter.prototype.emit; + return emit.apply(this, arguments); +}; + +WebSocket.prototype.emit = function emit() { + return this.socket.emit.apply(this.socket, arguments); +}; + +WebSocket.prototype.disconnect = function disconnect() { + return this.socket.disconnect(); +}; + /* * Expose */ diff --git a/lib/http/client.js b/lib/http/client.js index 018866aa..44ec7be0 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -108,8 +108,8 @@ HTTPClient.prototype._open = co(function* _open() { self.emit('balance', balance); }); - yield this._onConnect(); - yield this._sendAuth(); + yield this.onConnect(); + yield this.sendAuth(); }); /** @@ -134,7 +134,7 @@ HTTPClient.prototype._close = function close() { * @returns {Promise} */ -HTTPClient.prototype._onConnect = function _onConnect() { +HTTPClient.prototype.onConnect = function onConnect() { var self = this; return new Promise(function(resolve, reject) { self.socket.once('connect', resolve); @@ -147,12 +147,29 @@ HTTPClient.prototype._onConnect = function _onConnect() { * @returns {Promise} */ -HTTPClient.prototype._sendAuth = function _sendAuth() { +HTTPClient.prototype.sendAuth = function sendAuth() { var self = this; return new Promise(function(resolve, reject) { self.socket.emit('auth', self.apiKey, function(err) { if (err) - return reject(new Error(err.error)); + return reject(new Error(err.message)); + resolve(); + }); + }); +}; + +/** + * Wait for websocket auth. + * @private + * @returns {Promise} + */ + +HTTPClient.prototype.sendWalletAuth = function sendWalletAuth() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.emit('wallet auth', self.apiKey, function(err) { + if (err) + return reject(new Error(err.message)); resolve(); }); }); @@ -220,7 +237,7 @@ HTTPClient.prototype._request = co(function* _request(method, endpoint, json) { if (res.statusCode !== 200) { if (res.body.error) - throw new Error(res.body.error); + throw new Error(res.body.error.message); throw new Error('Status code: ' + res.statusCode); } @@ -369,7 +386,7 @@ HTTPClient.prototype.broadcast = function broadcast(tx) { HTTPClient.prototype.rescan = function rescan(height) { var options = { height: height }; - return this._post('/rescan', options); + return this._post('/_admin/rescan', options); }; /** @@ -380,7 +397,7 @@ HTTPClient.prototype.rescan = function rescan(height) { HTTPClient.prototype.reset = function reset(block) { var options = { block: block }; - return this._post('/reset', options); + return this._post('/_admin/reset', options); }; /** @@ -389,7 +406,7 @@ HTTPClient.prototype.reset = function reset(block) { */ HTTPClient.prototype.resend = function resend() { - return this._post('/resend', {}); + return this._post('/_admin/resend', {}); }; /** @@ -400,7 +417,7 @@ HTTPClient.prototype.resend = function resend() { HTTPClient.prototype.backup = function backup(path) { var options = { path: path }; - return this._post('/backup', options); + return this._post('/_admin/backup', options); }; /** @@ -417,7 +434,7 @@ HTTPClient.prototype.join = function join(id, token) { return new Promise(function(resolve, reject) { self.socket.emit('wallet join', id, token, function(err) { if (err) - return reject(new Error(err.error)); + return reject(new Error(err.message)); resolve(); }); }); @@ -437,7 +454,7 @@ HTTPClient.prototype.leave = function leave(id) { return new Promise(function(resolve, reject) { self.socket.emit('wallet leave', id, function(err) { if (err) - return reject(new Error(err.error)); + return reject(new Error(err.message)); resolve(); }); }); @@ -475,7 +492,7 @@ HTTPClient.prototype.getWallets = function getWallets() { */ HTTPClient.prototype.createWallet = function createWallet(options) { - return this._post('/wallet', options); + return this._put('/wallet/' + options.id, options); }; /** @@ -934,9 +951,9 @@ HTTPClient.prototype.createAccount = function createAccount(id, options) { if (typeof options === 'string') options = { account: options }; - path = '/wallet/' + id + '/account'; + path = '/wallet/' + id + '/account' + options.account; - return this._post(path, options); + return this._put(path, options); }; /** diff --git a/lib/http/rpc.js b/lib/http/rpc.js index d6b29b6d..86c73269 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -7,7 +7,6 @@ 'use strict'; var EventEmitter = require('events').EventEmitter; -var fs = require('../utils/fs'); var util = require('../utils/util'); var co = require('../utils/co'); var crypto = require('../crypto/crypto'); @@ -26,7 +25,6 @@ var Lock = require('../utils/lock'); var MerkleBlock = require('../primitives/merkleblock'); var MTX = require('../primitives/mtx'); var Network = require('../protocol/network'); -var Outpoint = require('../primitives/outpoint'); var Output = require('../primitives/output'); var TX = require('../primitives/tx'); var Logger = require('../node/logger'); @@ -57,9 +55,8 @@ function RPC(node) { this.pool = node.pool; this.fees = node.fees; this.miner = node.miner; - this.wallet = node.wallet; - this.walletdb = node.walletdb; this.logger = node.logger; + this.calls = {}; this.locker = new Lock(); @@ -77,6 +74,97 @@ util.inherits(RPC, EventEmitter); RPC.magic = 'Bitcoin Signed Message:\n'; +RPC.prototype.call = co(function* call(body, query) { + var cmds = body; + var out = []; + var array = true; + var i, cmd, result; + + if (!Array.isArray(cmds)) { + cmds = [cmds]; + array = false; + } + + for (i = 0; i < cmds.length; i++) { + cmd = cmds[i]; + + assert(cmd && typeof cmd === 'object', 'Command must be an object.'); + assert(typeof cmd.method === 'string', 'Method must be a string.'); + + if (!cmd.params) + cmd.params = []; + + assert(Array.isArray(cmd.params), 'Params must be an array.'); + + if (!cmd.id) + cmd.id = 0; + + assert(typeof cmd.id === 'number', 'ID must be a number.'); + } + + for (i = 0; i < cmds.length; i++) { + cmd = cmds[i]; + + 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'; + } + + try { + result = yield this.execute(cmd); + } catch (err) { + if (err.type === 'RPCError') { + out.push({ + result: null, + error: { + message: err.message, + code: -1 + }, + id: cmd.id + }); + continue; + } + + this.logger.error(err); + + out.push({ + result: null, + error: { + message: err.message, + code: 1 + }, + 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; +}); + RPC.prototype.execute = function execute(json, help) { switch (json.method) { case 'stop': @@ -214,128 +302,58 @@ RPC.prototype.execute = function execute(json, help) { case 'verifytxoutproof': return this.verifytxoutproof(json.params, help); - case 'fundrawtransaction': - return this.fundrawtransaction(json.params, help); - case 'resendwallettransactions': - return this.resendwallettransactions(json.params, help); - case 'abandontransaction': - return this.abandontransaction(json.params, help); - case 'addmultisigaddress': - return this.addmultisigaddress(json.params, help); - case 'addwitnessaddress': - return this.addwitnessaddress(json.params, help); - case 'backupwallet': - return this.backupwallet(json.params, help); - case 'dumpprivkey': - return this.dumpprivkey(json.params, help); - case 'dumpwallet': - return this.dumpwallet(json.params, help); - case 'encryptwallet': - return this.encryptwallet(json.params, help); - case 'getaccountaddress': - return this.getaccountaddress(json.params, help); - case 'getaccount': - return this.getaccount(json.params, help); - case 'getaddressesbyaccount': - return this.getaddressesbyaccount(json.params, help); - case 'getbalance': - return this.getbalance(json.params, help); - case 'getnewaddress': - return this.getnewaddress(json.params, help); - case 'getrawchangeaddress': - return this.getrawchangeaddress(json.params, help); - case 'getreceivedbyaccount': - return this.getreceivedbyaccount(json.params, help); - case 'getreceivedbyaddress': - return this.getreceivedbyaddress(json.params, help); - case 'gettransaction': - return this.gettransaction(json.params, help); - case 'getunconfirmedbalance': - return this.getunconfirmedbalance(json.params, help); - case 'getwalletinfo': - return this.getwalletinfo(json.params, help); - case 'importprivkey': - return this.importprivkey(json.params, help); - case 'importwallet': - return this.importwallet(json.params, help); - case 'importaddress': - return this.importaddress(json.params, help); - case 'importprunedfunds': - return this.importprunedfunds(json.params, help); - case 'importpubkey': - return this.importpubkey(json.params, help); - case 'keypoolrefill': - return this.keypoolrefill(json.params, help); - case 'listaccounts': - return this.listaccounts(json.params, help); - case 'listaddressgroupings': - return this.listaddressgroupings(json.params, help); - case 'listlockunspent': - return this.listlockunspent(json.params, help); - case 'listreceivedbyaccount': - return this.listreceivedbyaccount(json.params, help); - case 'listreceivedbyaddress': - return this.listreceivedbyaddress(json.params, help); - case 'listsinceblock': - return this.listsinceblock(json.params, help); - case 'listtransactions': - return this.listtransactions(json.params, help); - case 'listunspent': - return this.listunspent(json.params, help); - case 'lockunspent': - return this.lockunspent(json.params, help); - case 'move': - return this.move(json.params, help); - case 'sendfrom': - return this.sendfrom(json.params, help); - case 'sendmany': - return this.sendmany(json.params, help); - case 'sendtoaddress': - return this.sendtoaddress(json.params, help); - case 'setaccount': - return this.setaccount(json.params, help); - case 'settxfee': - return this.settxfee(json.params, help); - case 'signmessage': - return this.signmessage(json.params, help); - case 'walletlock': - return this.walletlock(json.params, help); - case 'walletpassphrasechange': - return this.walletpassphrasechange(json.params, help); - case 'walletpassphrase': - return this.walletpassphrase(json.params, help); - case 'removeprunedfunds': - return this.removeprunedfunds(json.params, help); - case 'getmemory': return this.getmemory(json.params, help); - case 'selectwallet': - return this.selectwallet(json.params, help); case 'setloglevel': return this.setloglevel(json.params, help); default: - return Promise.reject(new Error('Not found: ' + json.method + '.')); + return this.custom(json, help); } }; +/** + * Add a custom RPC call. + * @param {String} name + * @param {Function} func + * @param {Object?} ctx + */ + +RPC.prototype.add = function add(name, func, ctx) { + assert(!this.calls[name], 'Duplicate RPC call.'); + this.calls[name] = func.bind(ctx); +}; + +/** + * Execute a custom RPC call. + * @private + * @param {Object} json + * @param {Boolean} help + * @returns {Promise} + */ + +RPC.prototype.custom = co(function* custom(json, help) { + var call = this.calls[json.method]; + + if (!call) + throw new RPCError('Not found: ' + json.method + '.'); + + return yield call(json.params, help); +}); + /* * Overall control/query calls */ RPC.prototype.getinfo = co(function* getinfo(args, help) { - var balance; - if (help || args.length !== 0) throw new RPCError('getinfo'); - balance = yield this.wallet.getBalance(); - return { version: pkg.version, protocolversion: this.pool.options.version, walletversion: 0, - balance: Amount.btc(balance.unconfirmed, true), + balance: 0, blocks: this.chain.height, timeoffset: this.network.time.offset, connections: this.pool.peers.size(), @@ -344,7 +362,7 @@ RPC.prototype.getinfo = co(function* getinfo(args, help) { testnet: this.network.type !== Network.main, keypoololdest: 0, keypoolsize: 0, - unlocked_until: this.wallet.master.until, + unlocked_until: 0, paytxfee: Amount.btc(this.network.feeRate, true), relayfee: Amount.btc(this.network.minRelay, true), errors: '' @@ -2257,7 +2275,6 @@ RPC.prototype.sendrawtransaction = co(function* sendrawtransaction(args, help) { }); RPC.prototype.signrawtransaction = co(function* signrawtransaction(args, help) { - var wallet = this.wallet; var tx; if (help || args.length < 1 || args.length > 4) { @@ -2272,12 +2289,12 @@ RPC.prototype.signrawtransaction = co(function* signrawtransaction(args, help) { throw new RPCError('Invalid parameter'); tx = MTX.fromRaw(args[0], 'hex'); - tx.view = yield wallet.getCoinView(tx); + tx.view = yield this.mempool.getSpentView(tx); - return yield this._signrawtransaction(wallet, tx, args); + return yield this._signrawtransaction(tx, args); }); -RPC.prototype._signrawtransaction = co(function* signrawtransaction(wallet, tx, args) { +RPC.prototype._signrawtransaction = co(function* signrawtransaction(tx, args) { var type = Script.hashType.ALL; var keys = []; var keyMap = {}; @@ -2370,7 +2387,6 @@ RPC.prototype._signrawtransaction = co(function* signrawtransaction(wallet, tx, } yield tx.signAsync(keys, type); - yield wallet.sign(tx, { type: type }); return { hex: tx.toRaw().toString('hex'), @@ -2378,119 +2394,10 @@ RPC.prototype._signrawtransaction = co(function* signrawtransaction(wallet, tx, }; }); -RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args, help) { - var wallet = this.wallet; - var feeRate = this.feeRate; - var tx, options, changeAddress; - - if (help || args.length < 1 || args.length > 2) - throw new RPCError('fundrawtransaction "hexstring" ( options )'); - - if (!util.isHex(args[0])) - throw new RPCError('Invalid parameter.'); - - tx = MTX.fromRaw(args[0], 'hex'); - - if (tx.outputs.length === 0) - throw new RPCError('TX must have at least one output.'); - - if (args.length > 1) { - options = toObject(args[1]); - changeAddress = toString(options.changeAddress); - - if (changeAddress) - changeAddress = Address.fromBase58(changeAddress, this.network); - - feeRate = options.feeRate; - - if (feeRate != null) - feeRate = toSatoshi(feeRate); - } - - options = { - rate: feeRate, - changeAddress: changeAddress - }; - - yield wallet.fund(tx, options); - - return { - hex: tx.toRaw().toString('hex'), - changepos: tx.changeIndex, - fee: Amount.btc(tx.getFee(), true) - }; -}); - -RPC.prototype._createRedeem = co(function* _createRedeem(args, help) { - var wallet = this.wallet; - var i, m, n, keys, hash, script, key, ring; - - if (!util.isNumber(args[0]) - || !Array.isArray(args[1]) - || args[0] < 1 - || args[1].length < args[0] - || args[1].length > 16) { - throw new RPCError('Invalid parameter.'); - } - - m = args[0]; - n = args[1].length; - keys = args[1]; - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - - if (!util.isBase58(key)) { - if (!util.isHex(key)) - throw new RPCError('Invalid key.'); - keys[i] = new Buffer(key, 'hex'); - continue; - } - - hash = Address.getHash(key, 'hex'); - - if (!hash) - throw new RPCError('Invalid key.'); - - ring = yield wallet.getKey(hash); - - if (!ring) - throw new RPCError('Invalid key.'); - - keys[i] = ring.publicKey; - } - - try { - script = Script.fromMultisig(m, n, keys); - } catch (e) { - throw new RPCError('Invalid parameters.'); - } - - if (script.getSize() > consensus.MAX_SCRIPT_PUSH) - throw new RPCError('Redeem script exceeds size limit.'); - - return script; -}); - /* * Utility Functions */ -RPC.prototype.createmultisig = co(function* createmultisig(args, help) { - var script, address; - - if (help || args.length < 2 || args.length > 2) - throw new RPCError('createmultisig nrequired ["key",...]'); - - script = yield this._createRedeem(args); - address = script.getAddress(); - - return { - address: address.toBase58(this.network), - redeemScript: script.toJSON() - }; -}); - RPC.prototype.createwitnessaddress = co(function* createwitnessaddress(args, help) { var raw, script, program, address; @@ -2509,8 +2416,7 @@ RPC.prototype.createwitnessaddress = co(function* createwitnessaddress(args, hel }); RPC.prototype.validateaddress = co(function* validateaddress(args, help) { - var wallet = this.wallet; - var b58, address, json, path, script; + var b58, address, script; if (help || args.length !== 1) throw new RPCError('validateaddress "bitcoinaddress"'); @@ -2525,26 +2431,15 @@ RPC.prototype.validateaddress = co(function* validateaddress(args, help) { }; } - path = yield wallet.getPath(address); script = Script.fromAddress(address); - json = { + return { isvalid: true, address: address.toBase58(this.network), scriptPubKey: script.toJSON(), - ismine: path ? true : false, - iswatchonly: path ? wallet.watchOnly : false, - account: undefined, - hdkeypath: undefined + ismine: false, + iswatchonly: false }; - - if (!path) - return json; - - json.account = path.name; - json.hdkeypath = path.toPath(); - - return json; }); RPC.prototype.verifymessage = co(function* verifymessage(args, help) { @@ -2738,1446 +2633,6 @@ RPC.prototype.setmocktime = co(function* setmocktime(args, help) { return null; }); -/* - * Wallet - */ - -RPC.prototype.resendwallettransactions = co(function* resendwallettransactions(args, help) { - var wallet = this.wallet; - var hashes = []; - var i, tx, txs; - - if (help || args.length !== 0) - throw new RPCError('resendwallettransactions'); - - txs = yield wallet.resend(); - - for (i = 0; i < txs.length; i++) { - tx = txs[i]; - hashes.push(tx.txid()); - } - - return hashes; -}); - -RPC.prototype.addmultisigaddress = co(function* addmultisigaddress(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError('addmultisigaddress' - + ' nrequired ["key",...] ( "account" )'); - } - - // Impossible to implement in bcoin (no address book). - throw new Error('Not implemented.'); -}); - -RPC.prototype.addwitnessaddress = co(function* addwitnessaddress(args, help) { - if (help || args.length < 1 || args.length > 1) - throw new RPCError('addwitnessaddress "address"'); - - // Unlikely to be implemented. - throw new Error('Not implemented.'); -}); - -RPC.prototype.backupwallet = co(function* backupwallet(args, help) { - var dest; - - if (help || args.length !== 1) - throw new RPCError('backupwallet "destination"'); - - dest = toString(args[0]); - - yield this.walletdb.backup(dest); - - return null; -}); - -RPC.prototype.dumpprivkey = co(function* dumpprivkey(args, help) { - var wallet = this.wallet; - var hash, ring; - - if (help || args.length !== 1) - throw new RPCError('dumpprivkey "bitcoinaddress"'); - - hash = Address.getHash(toString(args[0]), 'hex'); - - if (!hash) - throw new RPCError('Invalid address.'); - - ring = yield wallet.getPrivateKey(hash); - - if (!ring) - throw new RPCError('Key not found.'); - - return ring.toSecret(); -}); - -RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) { - var wallet = this.wallet; - var i, file, time, address, fmt, str, out, hash, hashes, ring; - - if (help || args.length !== 1) - throw new RPCError('dumpwallet "filename"'); - - if (!args[0] || typeof args[0] !== 'string') - throw new RPCError('Invalid parameter.'); - - file = toString(args[0]); - time = util.date(); - 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),', - this.chain.height, this.chain.tip.rhash()), - util.fmt('# mined on %s', util.date(this.chain.tip.ts)), - util.fmt('# * File: %s', file), - '' - ]; - - hashes = yield wallet.getAddressHashes(); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - ring = yield wallet.getPrivateKey(hash); - - if (!ring) - continue; - - address = ring.getAddress('base58'); - fmt = '%s %s label= addr=%s'; - - if (ring.branch === 1) - fmt = '%s %s change=1 addr=%s'; - - str = util.fmt(fmt, ring.toSecret(), time, address); - - out.push(str); - } - - out.push(''); - out.push('# End of dump'); - out.push(''); - - out = out.join('\n'); - - if (fs.unsupported) - return out; - - yield fs.writeFile(file, out, 'utf8'); - - return out; -}); - -RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) { - var wallet = this.wallet; - var passphrase; - - if (!wallet.master.encrypted && (help || args.length !== 1)) - throw new RPCError('encryptwallet "passphrase"'); - - if (wallet.master.encrypted) - throw new RPCError('Already running with an encrypted wallet'); - - passphrase = toString(args[0]); - - if (passphrase.length < 1) - throw new RPCError('encryptwallet "passphrase"'); - - yield wallet.setPassphrase(passphrase); - - return 'wallet encrypted; we do not need to stop!'; -}); - -RPC.prototype.getaccountaddress = co(function* getaccountaddress(args, help) { - var wallet = this.wallet; - var account; - - if (help || args.length !== 1) - throw new RPCError('getaccountaddress "account"'); - - account = toString(args[0]); - - if (!account) - account = 'default'; - - account = yield wallet.getAccount(account); - - if (!account) - return ''; - - return account.receive.getAddress('base58'); -}); - -RPC.prototype.getaccount = co(function* getaccount(args, help) { - var wallet = this.wallet; - var hash, path; - - if (help || args.length !== 1) - throw new RPCError('getaccount "bitcoinaddress"'); - - hash = Address.getHash(args[0], 'hex'); - - if (!hash) - throw new RPCError('Invalid address.'); - - path = yield wallet.getPath(hash); - - if (!path) - return ''; - - return path.name; -}); - -RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args, help) { - var wallet = this.wallet; - var i, path, account, address, addrs, paths; - - if (help || args.length !== 1) - throw new RPCError('getaddressesbyaccount "account"'); - - account = toString(args[0]); - - if (!account) - account = 'default'; - - addrs = []; - - paths = yield wallet.getPaths(account); - - for (i = 0; i < paths.length; i++) { - path = paths[i]; - address = path.toAddress(); - addrs.push(address.toBase58(this.network)); - } - - return addrs; -}); - -RPC.prototype.getbalance = co(function* getbalance(args, help) { - var wallet = this.wallet; - var minconf = 0; - var account, value, balance; - - if (help || args.length > 3) - throw new RPCError('getbalance ( "account" minconf includeWatchonly )'); - - if (args.length >= 1) { - account = toString(args[0]); - - if (!account) - account = 'default'; - - if (account === '*') - account = null; - } - - if (args.length >= 2) - minconf = toNumber(args[1], 0); - - balance = yield wallet.getBalance(account); - - if (minconf) - value = balance.confirmed; - else - value = balance.unconfirmed; - - return Amount.btc(value, true); -}); - -RPC.prototype.getnewaddress = co(function* getnewaddress(args, help) { - var wallet = this.wallet; - var account, address; - - if (help || args.length > 1) - throw new RPCError('getnewaddress ( "account" )'); - - if (args.length === 1) - account = toString(args[0]); - - if (!account) - account = 'default'; - - address = yield wallet.createReceive(account); - - return address.getAddress('base58'); -}); - -RPC.prototype.getrawchangeaddress = co(function* getrawchangeaddress(args, help) { - var wallet = this.wallet; - var address; - - if (help || args.length > 1) - throw new RPCError('getrawchangeaddress'); - - address = yield wallet.createChange(); - - return address.getAddress('base58'); -}); - -RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args, help) { - var wallet = this.wallet; - var minconf = 0; - var total = 0; - var filter = {}; - var lastConf = -1; - var i, j, path, wtx, output, conf, hash, account, paths, txs; - - if (help || args.length < 1 || args.length > 2) - throw new RPCError('getreceivedbyaccount "account" ( minconf )'); - - account = toString(args[0]); - - if (!account) - account = 'default'; - - if (args.length === 2) - minconf = toNumber(args[1], 0); - - paths = yield wallet.getPaths(account); - - for (i = 0; i < paths.length; i++) { - path = paths[i]; - filter[path.hash] = true; - } - - txs = yield wallet.getHistory(account); - - for (i = 0; i < txs.length; i++) { - wtx = txs[i]; - - conf = wtx.getDepth(this.chain.height); - - if (conf < minconf) - continue; - - if (lastConf === -1 || conf < lastConf) - lastConf = conf; - - for (j = 0; j < wtx.tx.outputs.length; j++) { - output = wtx.tx.outputs[j]; - hash = output.getHash('hex'); - if (hash && filter[hash]) - total += output.value; - } - } - - return Amount.btc(total, true); -}); - -RPC.prototype.getreceivedbyaddress = co(function* getreceivedbyaddress(args, help) { - var wallet = this.wallet; - var minconf = 0; - var total = 0; - var i, j, hash, wtx, output, txs; - - if (help || args.length < 1 || args.length > 2) - throw new RPCError('getreceivedbyaddress "bitcoinaddress" ( minconf )'); - - hash = Address.getHash(toString(args[0]), 'hex'); - - if (!hash) - throw new RPCError('Invalid address'); - - if (args.length === 2) - minconf = toNumber(args[1], 0); - - txs = yield wallet.getHistory(); - - for (i = 0; i < txs.length; i++) { - wtx = txs[i]; - - if (wtx.getDepth(this.chain.height) < minconf) - continue; - - for (j = 0; j < wtx.tx.outputs.length; j++) { - output = wtx.tx.outputs[j]; - if (output.getHash('hex') === hash) - total += output.value; - } - } - - return Amount.btc(total, true); -}); - -RPC.prototype._toWalletTX = co(function* _toWalletTX(wtx) { - var wallet = this.wallet; - var details = yield wallet.toDetails(wtx); - var det = []; - var sent = 0; - var received = 0; - var receive = true; - var i, member; - - if (!details) - throw new RPCError('TX not found.'); - - for (i = 0; i < details.inputs.length; i++) { - member = details.inputs[i]; - if (member.path) { - receive = false; - break; - } - } - - for (i = 0; i < details.outputs.length; i++) { - member = details.outputs[i]; - - if (member.path) { - if (member.path.branch === 1) - continue; - - det.push({ - account: member.path.name, - address: member.address.toBase58(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: '', - address: member.address - ? member.address.toBase58(this.network) - : null, - category: 'send', - amount: -(Amount.btc(member.value, true)), - fee: -(Amount.btc(details.fee, true)), - vout: i - }); - - sent += member.value; - } - - return { - amount: Amount.btc(receive ? received : -sent, true), - confirmations: details.confirmations, - blockhash: details.block ? util.revHex(details.block) : null, - blockindex: details.index, - blocktime: details.ts, - txid: util.revHex(details.hash), - walletconflicts: [], - time: details.ps, - timereceived: details.ps, - 'bip125-replaceable': 'no', - details: det, - hex: details.tx.toRaw().toString('hex') - }; -}); - -RPC.prototype.gettransaction = co(function* gettransaction(args, help) { - var wallet = this.wallet; - var hash, wtx; - - if (help || args.length < 1 || args.length > 2) - throw new RPCError('gettransaction "txid" ( includeWatchonly )'); - - hash = toHash(args[0]); - - if (!hash) - throw new RPCError('Invalid parameter'); - - wtx = yield wallet.getTX(hash); - - if (!wtx) - throw new RPCError('TX not found.'); - - return yield this._toWalletTX(wtx); -}); - -RPC.prototype.abandontransaction = co(function* abandontransaction(args, help) { - var wallet = this.wallet; - var hash, result; - - if (help || args.length !== 1) - throw new RPCError('abandontransaction "txid"'); - - hash = toHash(args[0]); - - if (!hash) - throw new RPCError('Invalid parameter.'); - - result = yield wallet.abandon(hash); - - if (!result) - throw new RPCError('Transaction not in wallet.'); - - return null; -}); - -RPC.prototype.getunconfirmedbalance = co(function* getunconfirmedbalance(args, help) { - var wallet = this.wallet; - var balance; - - if (help || args.length > 0) - throw new RPCError('getunconfirmedbalance'); - - balance = yield wallet.getBalance(); - - return Amount.btc(balance.unconfirmed, true); -}); - -RPC.prototype.getwalletinfo = co(function* getwalletinfo(args, help) { - var wallet = this.wallet; - var balance; - - if (help || args.length !== 0) - throw new RPCError('getwalletinfo'); - - balance = yield wallet.getBalance(); - - return { - walletid: wallet.id, - walletversion: 6, - balance: Amount.btc(balance.unconfirmed, true), - unconfirmed_balance: Amount.btc(balance.unconfirmed, true), - txcount: wallet.txdb.state.tx, - keypoololdest: 0, - keypoolsize: 0, - unlocked_until: wallet.master.until, - paytxfee: this.feeRate != null - ? Amount.btc(this.feeRate, true) - : 0 - }; -}); - -RPC.prototype.importprivkey = co(function* importprivkey(args, help) { - var wallet = this.wallet; - var secret, label, rescan, key; - - if (help || args.length < 1 || args.length > 3) - throw new RPCError('importprivkey "bitcoinprivkey" ( "label" rescan )'); - - secret = toString(args[0]); - - if (args.length > 1) - label = toString(args[1]); - - if (args.length > 2) - rescan = toBool(args[2]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - key = KeyRing.fromSecret(secret, this.network); - - yield wallet.importKey(0, key); - - if (rescan) - yield this.walletdb.rescan(0); - - return null; -}); - -RPC.prototype.importwallet = co(function* importwallet(args, help) { - var wallet = this.wallet; - var file, keys, lines, line, parts; - var i, secret, time, label, addr; - var data, key, rescan; - - if (help || args.length !== 1) - throw new RPCError('importwallet "filename" ( rescan )'); - - if (fs.unsupported) - throw new RPCError('FS not available.'); - - file = toString(args[0]); - - if (args.length > 1) - rescan = toBool(args[1]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - data = yield fs.readFile(file, 'utf8'); - - lines = data.split(/\n+/); - keys = []; - - for (i = 0; i < lines.length; i++) { - line = lines[i].trim(); - - if (line.length === 0) - continue; - - if (/^\s*#/.test(line)) - continue; - - parts = line.split(/\s+/); - - if (parts.length < 4) - throw new RPCError('Malformed wallet.'); - - secret = KeyRing.fromSecret(parts[0], this.network); - - time = +parts[1]; - label = parts[2]; - addr = parts[3]; - - keys.push(secret); - } - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - yield wallet.importKey(0, key); - } - - if (rescan) - yield this.walletdb.rescan(0); - - return null; -}); - -RPC.prototype.importaddress = co(function* importaddress(args, help) { - var wallet = this.wallet; - var addr, label, rescan, p2sh; - - if (help || args.length < 1 || args.length > 4) - throw new RPCError('importaddress "address" ( "label" rescan p2sh )'); - - addr = toString(args[0]); - - if (args.length > 1) - label = toString(args[1]); - - if (args.length > 2) - rescan = toBool(args[2]); - - if (args.length > 3) - p2sh = toBool(args[3]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - addr = Address.fromBase58(addr, this.network); - - yield wallet.importAddress(0, addr); - - if (rescan) - yield this.walletdb.rescan(0); - - return null; -}); - -RPC.prototype.importpubkey = co(function* importpubkey(args, help) { - var wallet = this.wallet; - var pubkey, label, rescan, key; - - if (help || args.length < 1 || args.length > 4) - throw new RPCError('importpubkey "pubkey" ( "label" rescan )'); - - pubkey = toString(args[0]); - - if (!util.isHex(pubkey)) - throw new RPCError('Invalid parameter.'); - - if (args.length > 1) - label = toString(args[1]); - - if (args.length > 2) - rescan = toBool(args[2]); - - if (rescan && this.chain.options.prune) - throw new RPCError('Cannot rescan when pruned.'); - - pubkey = new Buffer(pubkey, 'hex'); - - key = KeyRing.fromPublic(pubkey, this.network); - - yield wallet.importKey(0, key); - - if (rescan) - yield this.walletdb.rescan(0); - - return null; -}); - -RPC.prototype.keypoolrefill = co(function* keypoolrefill(args, help) { - if (help || args.length > 1) - throw new RPCError('keypoolrefill ( newsize )'); - return null; -}); - -RPC.prototype.listaccounts = co(function* listaccounts(args, help) { - var wallet = this.wallet; - var i, map, accounts, account, balance; - - if (help || args.length > 2) - throw new RPCError('listaccounts ( minconf includeWatchonly)'); - - map = {}; - accounts = yield wallet.getAccounts(); - - for (i = 0; i < accounts.length; i++) { - account = accounts[i]; - balance = yield wallet.getBalance(account); - map[account] = Amount.btc(balance.unconfirmed, true); - } - - return map; -}); - -RPC.prototype.listaddressgroupings = co(function* listaddressgroupings(args, help) { - if (help) - throw new RPCError('listaddressgroupings'); - throw new Error('Not implemented.'); -}); - -RPC.prototype.listlockunspent = co(function* listlockunspent(args, help) { - var wallet = this.wallet; - var i, outpoints, outpoint, out; - - if (help || args.length > 0) - throw new RPCError('listlockunspent'); - - outpoints = wallet.getLocked(); - out = []; - - for (i = 0; i < outpoints.length; i++) { - outpoint = outpoints[i]; - out.push({ - txid: outpoint.txid(), - vout: outpoint.index - }); - } - - return out; -}); - -RPC.prototype.listreceivedbyaccount = co(function* listreceivedbyaccount(args, help) { - var minconf = 0; - var includeEmpty = false; - - if (help || args.length > 3) { - throw new RPCError('listreceivedbyaccount' - + ' ( minconf includeempty includeWatchonly )'); - } - - if (args.length > 0) - minconf = toNumber(args[0], 0); - - if (args.length > 1) - includeEmpty = toBool(args[1], false); - - return yield this._listReceived(minconf, includeEmpty, true); -}); - -RPC.prototype.listreceivedbyaddress = co(function* listreceivedbyaddress(args, help) { - var minconf = 0; - var includeEmpty = false; - - if (help || args.length > 3) { - throw new RPCError('listreceivedbyaddress' - + ' ( minconf includeempty includeWatchonly )'); - } - - if (args.length > 0) - minconf = toNumber(args[0], 0); - - if (args.length > 1) - includeEmpty = toBool(args[1], false); - - return yield this._listReceived(minconf, includeEmpty, false); -}); - -RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account) { - var wallet = this.wallet; - var out = []; - var result = []; - var map = {}; - var paths = yield wallet.getPaths(); - var i, j, path, wtx, output, conf, hash; - var entry, address, keys, key, item, txs; - - for (i = 0; i < paths.length; i++) { - path = paths[i]; - address = path.toAddress(); - map[path.hash] = { - involvesWatchonly: wallet.watchOnly, - address: address.toBase58(this.network), - account: path.name, - amount: 0, - confirmations: -1, - label: '', - }; - } - - txs = yield wallet.getHistory(); - - for (i = 0; i < txs.length; i++) { - wtx = txs[i]; - - conf = wtx.getDepth(this.chain.height); - - if (conf < minconf) - continue; - - for (j = 0; j < wtx.tx.outputs.length; j++) { - output = wtx.tx.outputs[j]; - address = output.getAddress(); - - if (!address) - continue; - - hash = address.getHash('hex'); - entry = map[hash]; - - if (entry) { - if (entry.confirmations === -1 || conf < entry.confirmations) - entry.confirmations = conf; - entry.address = address.toBase58(this.network); - entry.amount += output.value; - } - } - } - - keys = Object.keys(map); - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - entry = map[key]; - out.push(entry); - } - - if (account) { - map = {}; - - for (i = 0; i < out.length; i++) { - entry = out[i]; - item = map[entry.account]; - if (!item) { - map[entry.account] = entry; - entry.address = undefined; - continue; - } - item.amount += entry.amount; - } - - out = []; - keys = Object.keys(map); - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - entry = map[key]; - out.push(entry); - } - } - - for (i = 0; i < out.length; i++) { - entry = out[i]; - - 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 = co(function* listsinceblock(args, help) { - var wallet = this.wallet; - var minconf = 0; - var out = []; - var i, block, highest, height; - var txs, wtx, json; - - if (help) { - throw new RPCError('listsinceblock' - + ' ( "blockhash" target-confirmations includeWatchonly)'); - } - - if (args.length > 0) { - block = toHash(args[0]); - if (!block) - throw new RPCError('Invalid parameter.'); - } - - if (args.length > 1) - minconf = toNumber(args[1], 0); - - height = yield this.chain.db.getHeight(block); - - if (height === -1) - height = this.chain.height; - - txs = yield wallet.getHistory(); - - for (i = 0; i < txs.length; i++) { - wtx = txs[i]; - - if (wtx.height < height) - continue; - - if (wtx.getDepth(this.chain.height) < minconf) - continue; - - if (!highest || wtx.height > highest) - highest = wtx; - - json = yield this._toListTX(wtx); - - out.push(json); - } - - return { - transactions: out, - lastblock: highest && highest.block - ? util.revHex(highest.block) - : encoding.NULL_HASH - }; -}); - -RPC.prototype._toListTX = co(function* _toListTX(wtx) { - var wallet = this.wallet; - var sent = 0; - var received = 0; - var receive = true; - var sendMember, recMember, sendIndex, recIndex; - var i, member, index; - var details = yield wallet.toDetails(wtx); - - if (!details) - throw new RPCError('TX not found.'); - - for (i = 0; i < details.inputs.length; i++) { - member = details.inputs[i]; - if (member.path) { - receive = false; - break; - } - } - - for (i = 0; i < details.outputs.length; i++) { - 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; - } - - 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.toBase58(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.ts, - txid: util.revHex(details.hash), - walletconflicts: [], - time: details.ps, - timereceived: details.ps, - 'bip125-replaceable': 'no' - }; -}); - -RPC.prototype.listtransactions = co(function* listtransactions(args, help) { - var wallet = this.wallet; - var account = null; - var count = 10; - var i, txs, wtx, json; - - if (help || args.length > 4) { - throw new RPCError( - 'listtransactions ( "account" count from includeWatchonly)'); - } - - if (args.length > 0) { - account = toString(args[0]); - if (!account) - account = 'default'; - } - - if (args.length > 1) { - count = toNumber(args[1], 10); - if (count < 0) - count = 10; - } - - txs = yield wallet.getHistory(); - - sortTX(txs); - - for (i = 0; i < txs.length; i++) { - wtx = txs[i]; - json = yield this._toListTX(wtx); - txs[i] = json; - } - - return txs; -}); - -RPC.prototype.listunspent = co(function* listunspent(args, help) { - var wallet = this.wallet; - var minDepth = 1; - var maxDepth = 9999999; - var out = []; - var i, addresses, addrs, depth, address, hash, coins, coin, ring; - - if (help || args.length > 3) { - throw new RPCError('listunspent' - + ' ( minconf maxconf ["address",...] )'); - } - - if (args.length > 0) - minDepth = toNumber(args[0], 1); - - if (args.length > 1) - maxDepth = toNumber(args[1], maxDepth); - - if (args.length > 2) - addrs = toArray(args[2]); - - if (addrs) { - addresses = {}; - for (i = 0; i < addrs.length; i++) { - address = toString(addrs[i]); - hash = Address.getHash(address, 'hex'); - - if (!hash) - throw new RPCError('Invalid address.'); - - if (addresses[hash]) - throw new RPCError('Duplicate address.'); - - addresses[hash] = true; - } - } - - coins = yield wallet.getCoins(); - - sortCoins(coins); - - for (i = 0; i < coins.length; i++ ) { - coin = coins[i]; - depth = coin.getDepth(this.chain.height); - - if (!(depth >= minDepth && depth <= maxDepth)) - continue; - - address = coin.getAddress(); - - if (!address) - continue; - - hash = coin.getHash('hex'); - - if (addresses) { - if (!hash || !addresses[hash]) - continue; - } - - ring = yield wallet.getKey(hash); - - out.push({ - txid: coin.txid(), - vout: coin.index, - address: address ? address.toBase58(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 = co(function* lockunspent(args, help) { - var wallet = this.wallet; - var i, unlock, outputs, output, outpoint; - - if (help || args.length < 1 || args.length > 2) { - throw new RPCError('lockunspent' - + ' unlock ([{"txid":"txid","vout":n},...])'); - } - - unlock = toBool(args[0]); - - if (args.length === 1) { - if (unlock) - wallet.unlockCoins(); - return true; - } - - outputs = toArray(args[1]); - - if (!outputs) - throw new RPCError('Invalid parameter.'); - - for (i = 0; i < outputs.length; i++) { - output = outputs[i]; - - if (!output || typeof output !== 'object') - throw new RPCError('Invalid parameter.'); - - outpoint = new Outpoint(); - outpoint.hash = toHash(output.txid); - outpoint.index = toNumber(output.vout); - - if (!outpoint.hash) - throw new RPCError('Invalid parameter.'); - - if (outpoint.index < 0) - throw new RPCError('Invalid parameter.'); - - if (unlock) { - wallet.unlockCoin(outpoint); - continue; - } - - wallet.lockCoin(outpoint); - } - - return true; -}); - -RPC.prototype.move = co(function* move(args, help) { - // Not implementing: stupid and deprecated. - throw new Error('Not implemented.'); -}); - -RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) { - var wallet = this.wallet; - var tx, options; - - options = { - account: account, - subtractFee: subtractFee, - rate: this.feeRate, - outputs: [{ - address: address, - value: amount - }] - }; - - tx = yield wallet.send(options); - - return tx.txid(); -}); - -RPC.prototype.sendfrom = co(function* sendfrom(args, help) { - var account, address, amount; - - if (help || args.length < 3 || args.length > 6) { - throw new RPCError('sendfrom' - + ' "fromaccount" "tobitcoinaddress"' - + ' amount ( minconf "comment" "comment-to" )'); - } - - account = toString(args[0]); - address = Address.fromBase58(toString(args[1]), this.network); - amount = toSatoshi(args[2]); - - if (!account) - account = 'default'; - - return yield this._send(account, address, amount, false); -}); - -RPC.prototype.sendmany = co(function* sendmany(args, help) { - var wallet = this.wallet; - var minconf = 1; - var outputs = []; - var uniq = {}; - var account, sendTo, comment, subtractFee; - var i, keys, tx, key, value, address; - var hash, output, options; - - if (help || args.length < 2 || args.length > 5) { - throw new RPCError('sendmany' - + ' "fromaccount" {"address":amount,...}' - + ' ( minconf "comment" ["address",...] )'); - } - - account = toString(args[0]); - sendTo = toObject(args[1]); - - if (!account) - account = 'default'; - - if (!sendTo) - throw new RPCError('Invalid parameter.'); - - if (args.length > 2) - minconf = toNumber(args[2], 1); - - if (args.length > 3) - comment = toString(args[3]); - - if (args.length > 4) { - subtractFee = args[4]; - if (typeof subtractFee !== 'boolean') { - if (!util.isNumber(subtractFee)) - throw new RPCError('Invalid parameter.'); - } - } - - keys = Object.keys(sendTo); - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - value = toSatoshi(sendTo[key]); - address = Address.fromBase58(key, this.network); - hash = address.getHash('hex'); - - if (uniq[hash]) - throw new RPCError('Invalid parameter.'); - - uniq[hash] = true; - - output = new Output(); - output.value = value; - output.script.fromAddress(address); - outputs.push(output); - } - - options = { - outputs: outputs, - subtractFee: subtractFee, - account: account, - depth: minconf - }; - - tx = yield wallet.send(options); - - return tx.txid(); -}); - -RPC.prototype.sendtoaddress = co(function* sendtoaddress(args, help) { - var address, amount, subtractFee; - - if (help || args.length < 2 || args.length > 5) { - throw new RPCError('sendtoaddress' - + ' "bitcoinaddress" amount' - + ' ( "comment" "comment-to"' - + ' subtractfeefromamount )'); - } - - address = Address.fromBase58(toString(args[0]), this.network); - amount = toSatoshi(args[1]); - subtractFee = toBool(args[4]); - - return yield this._send(null, address, amount, subtractFee); -}); - -RPC.prototype.setaccount = co(function* setaccount(args, help) { - if (help || args.length < 1 || args.length > 2) - throw new RPCError('setaccount "bitcoinaddress" "account"'); - - // Impossible to implement in bcoin: - throw new Error('Not implemented.'); -}); - -RPC.prototype.settxfee = co(function* settxfee(args, help) { - if (help || args.length < 1 || args.length > 1) - throw new RPCError('settxfee amount'); - - this.feeRate = toSatoshi(args[0]); - - return true; -}); - -RPC.prototype.signmessage = co(function* signmessage(args, help) { - var wallet = this.wallet; - var address, msg, sig, ring; - - if (help || args.length !== 2) - throw new RPCError('signmessage "bitcoinaddress" "message"'); - - address = toString(args[0]); - msg = toString(args[1]); - - address = Address.getHash(address, 'hex'); - - if (!address) - throw new RPCError('Invalid address.'); - - ring = yield wallet.getKey(address); - - if (!ring) - throw new RPCError('Address not found.'); - - if (!wallet.master.key) - throw new RPCError('Wallet is locked.'); - - msg = new Buffer(RPC.magic + msg, 'utf8'); - msg = crypto.hash256(msg); - - sig = ring.sign(msg); - - return sig.toString('base64'); -}); - -RPC.prototype.walletlock = co(function* walletlock(args, help) { - var wallet = this.wallet; - - if (help || (wallet.master.encrypted && args.length !== 0)) - throw new RPCError('walletlock'); - - if (!wallet.master.encrypted) - throw new RPCError('Wallet is not encrypted.'); - - yield wallet.lock(); - - return null; -}); - -RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, help) { - var wallet = this.wallet; - var old, new_; - - if (help || (wallet.master.encrypted && args.length !== 2)) { - throw new RPCError('walletpassphrasechange' - + ' "oldpassphrase" "newpassphrase"'); - } - - if (!wallet.master.encrypted) - throw new RPCError('Wallet is not encrypted.'); - - old = toString(args[0]); - new_ = toString(args[1]); - - if (old.length < 1 || new_.length < 1) - throw new RPCError('Invalid parameter'); - - yield wallet.setPassphrase(old, new_); - - return null; -}); - -RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) { - var wallet = this.wallet; - var passphrase, timeout; - - if (help || (wallet.master.encrypted && args.length !== 2)) - throw new RPCError('walletpassphrase "passphrase" timeout'); - - if (!wallet.master.encrypted) - throw new RPCError('Wallet is not encrypted.'); - - passphrase = toString(args[0]); - timeout = toNumber(args[1]); - - if (passphrase.length < 1) - throw new RPCError('Invalid parameter'); - - if (timeout < 0) - throw new RPCError('Invalid parameter'); - - yield wallet.unlock(passphrase, timeout); - - return null; -}); - -RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) { - var tx, block, hash, label, height; - - if (help || args.length < 2 || args.length > 3) { - throw new RPCError('importprunedfunds' - + ' "rawtransaction" "txoutproof" ( "label" )'); - } - - tx = args[0]; - block = args[1]; - - if (!util.isHex(tx) || !util.isHex(block)) - throw new RPCError('Invalid parameter.'); - - tx = TX.fromRaw(tx, 'hex'); - block = MerkleBlock.fromRaw(block, 'hex'); - hash = block.hash('hex'); - - if (args.length === 3) - label = toString(args[2]); - - if (!block.verify()) - throw new RPCError('Invalid proof.'); - - if (!block.hasTX(tx.hash('hex'))) - throw new RPCError('Invalid proof.'); - - height = yield this.chain.db.getHeight(hash); - - if (height === -1) - throw new RPCError('Invalid proof.'); - - block = { - hash: hash, - ts: block.ts, - height: height - }; - - if (!(yield this.walletdb.addTX(tx, block))) - throw new RPCError('No tracked address for TX.'); - - return null; -}); - -RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args, help) { - var wallet = this.wallet; - var hash; - - if (help || args.length !== 1) - throw new RPCError('removeprunedfunds "txid"'); - - hash = toHash(args[0]); - - if (!hash) - throw new RPCError('Invalid parameter.'); - - if (!(yield wallet.remove(hash))) - throw new RPCError('Transaction not in wallet.'); - - return null; -}); - RPC.prototype.getmemory = co(function* getmemory(args, help) { var mem; @@ -4197,23 +2652,6 @@ RPC.prototype.getmemory = co(function* getmemory(args, help) { }; }); -RPC.prototype.selectwallet = co(function* selectwallet(args, help) { - var id, wallet; - - if (help || args.length !== 1) - throw new RPCError('selectwallet "id"'); - - id = toString(args[0]); - wallet = yield this.walletdb.get(id); - - if (!wallet) - throw new RPCError('Wallet not found.'); - - this.wallet = wallet; - - return null; -}); - RPC.prototype.setloglevel = co(function* setloglevel(args, help) { var name, level; @@ -4301,20 +2739,6 @@ function reverseEndian(data) { } } -function sortTX(txs) { - return txs.sort(function(a, b) { - return a.ps - b.ps; - }); -} - -function sortCoins(coins) { - return coins.sort(function(a, b) { - a = a.height === -1 ? 0x7fffffff : a.height; - b = b.height === -1 ? 0x7fffffff : b.height; - return a - b; - }); -} - /* * Expose */ diff --git a/lib/http/server.js b/lib/http/server.js index d7422099..79dcf2fb 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -7,30 +7,21 @@ 'use strict'; -/* jshint -W069 */ -/* jshint noyield: true */ - var assert = require('assert'); var EventEmitter = require('events').EventEmitter; var HTTPBase = require('./base'); var util = require('../utils/util'); var co = require('../utils/co'); -var IP = require('../utils/ip'); var base58 = require('../utils/base58'); var Amount = require('../btc/amount'); -var Address = require('../primitives/address'); var Bloom = require('../utils/bloom'); var TX = require('../primitives/tx'); -var KeyRing = require('../primitives/keyring'); var Outpoint = require('../primitives/outpoint'); -var HD = require('../hd/hd'); -var Script = require('../script/script'); var crypto = require('../crypto/crypto'); var Network = require('../protocol/network'); -var fs = require('../utils/fs'); +var Validator = require('../utils/validator'); var pkg = require('../pkg'); -var cob = co.cob; -var RPC; +var RPC = require('./rpc'); /** * HTTPServer @@ -46,11 +37,11 @@ function HTTPServer(options) { if (!(this instanceof HTTPServer)) return new HTTPServer(options); - EventEmitter.call(this); + options = new HTTPOptions(options); - this.options = new HTTPOptions(options); - this.options.load(); + HTTPBase.call(this, options); + this.options = options; this.network = this.options.network; this.logger = this.options.logger; this.node = this.options.node; @@ -60,26 +51,22 @@ function HTTPServer(options) { this.pool = this.node.pool; this.fees = this.node.fees; this.miner = this.node.miner; - this.wallet = this.node.wallet; - this.walletdb = this.node.walletdb; + this.rpc = new RPC(this.node); - this.server = new HTTPBase(this.options); - this.rpc = null; - - this._init(); + this.init(); } -util.inherits(HTTPServer, EventEmitter); +util.inherits(HTTPServer, HTTPBase); /** * Initialize routes. * @private */ -HTTPServer.prototype._init = function _init() { +HTTPServer.prototype.init = function init() { var self = this; - this.server.on('request', function(req, res) { + this.on('request', function(req, res) { if (req.method === 'POST' && req.pathname === '/') return; @@ -87,536 +74,45 @@ HTTPServer.prototype._init = function _init() { req.method, req.pathname, req.socket.remoteAddress); }); - this.server.on('listening', function(address) { - self.logger.info('HTTP server listening on %s (port=%d).', + this.on('listening', function(address) { + self.logger.info('Node HTTP server listening on %s (port=%d).', address.address, address.port); }); - this.use(co(function* (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'); - - if (req.method === 'OPTIONS') { - res.statusCode = 200; - res.sent = true; - res.end(); - return; - } - - res.setHeader('X-Bcoin-Version', pkg.version); - res.setHeader('X-Bcoin-Network', this.network.type); - })); - - this.use(co(function* (req, res) { - var auth = req.headers['authorization']; - var parts; - - if (!auth) { - req.username = null; - req.password = null; - return; - } - - parts = auth.split(' '); - assert(parts.length === 2, 'Invalid auth token.'); - assert(parts[0] === 'Basic', 'Invalid auth token.'); - - auth = new Buffer(parts[1], 'base64').toString('utf8'); - parts = auth.split(':'); - - req.username = parts.shift(); - req.password = parts.join(':'); - })); - - this.use(co(function* (req, res) { - var hash; - - if (this.options.noAuth) { - req.admin = true; - return; - } - - hash = hash256(req.password); - - // Regular API key gives access to everything. - if (crypto.ccmp(hash, this.options.apiHash)) { - req.admin = true; - return; - } - - // If they're hitting the wallet services, - // they can use the less powerful API key. - if (isWalletPath(req)) { - if (crypto.ccmp(hash, this.options.serviceHash)) - return; - } - - res.setHeader('WWW-Authenticate', 'Basic realm="node"'); - - if (req.method === 'POST' - && req.pathname === '/') { - res.send(401, { - result: null, - error: { - message: 'Bad auth.', - code: 1 - }, - id: req.body.id - }); - return; - } - - res.send(401, { error: 'Bad API key.' }); - })); - - this.hook(co(function* (req, res) { - var i, params, options, censored, output, address; - - if (req.method === 'POST' && req.pathname === '/') { - req.options = Object.create(null); - return; - } - - params = Object.create(null); - options = Object.create(null); - censored = Object.create(null); - - softMerge(params, req.params, true); - softMerge(params, req.query, true); - softMerge(params, req.body); - softMerge(censored, params); - - this.logger.debug('Params:'); - - // Censor sensitive data from logs. - if (censored.passphrase != null) - censored.passphrase = ''; - - if (censored.old != null) - censored.old = ''; - - if (censored.privateKey != null) - censored.privateKey = ''; - - if (censored.accountKey != null) - censored.accountKey = ''; - - if (censored.master != null) - censored.master = ''; - - if (censored.mnemonic != null) - censored.mnemonic = ''; - - if (censored.token != null) - censored.token = ''; - - this.logger.debug(censored); - - if (params.id) { - enforce(typeof params.id === 'string', 'ID must be a string.'); - options.id = params.id; - } - - if (params.block != null) { - if (typeof params.block === 'number') { - assert(util.isUInt32(params.block), 'Height must be a number.'); - options.height = params.block; - } else { - enforce(typeof params.block === 'string', 'Hash must be a string.'); - if (params.block.length !== 64) { - options.height = Number(params.block); - enforce(util.isUInt32(options.height), 'Height must be a number.'); - } else { - options.hash = util.revHex(params.block); - } - } - } - - if (params.hash) { - enforce(typeof params.hash === 'string', 'Hash must be a string.'); - enforce(params.hash.length === 64, 'Hash must be a string.'); - options.hash = util.revHex(params.hash); - } - - if (params.index != null) { - options.index = Number(params.index); - enforce(util.isUInt32(options.index), 'Index must be a number.'); - } - - if (params.height != null) { - options.height = Number(params.height); - enforce(util.isUInt32(options.height), 'Height must be a number.'); - } - - if (params.start != null) { - options.start = Number(params.start); - enforce(util.isUInt32(options.start), 'Start must be a number.'); - } - - if (params.end != null) { - options.end = Number(params.end); - enforce(util.isUInt32(options.end), 'End must be a number.'); - } - - if (params.limit != null) { - options.limit = Number(params.limit); - enforce(util.isUInt32(options.limit), 'Limit must be a number.'); - } - - if (params.age != null) { - options.age = Number(params.age); - enforce(util.isUInt32(options.age), 'Age must be a number.'); - } - - if (params.confirmations != null) { - options.depth = Number(params.confirmations); - enforce(util.isNumber(options.depth), - 'Confirmations must be a number.'); - } - - if (params.depth != null) { - options.depth = Number(params.depth); - enforce(util.isNumber(options.depth), - 'Depth must be a number.'); - } - - if (params.fee) - options.fee = Amount.value(params.fee); - - if (params.hardFee) - options.hardFee = Amount.value(params.hardFee); - - if (params.maxFee) - options.maxFee = Amount.value(params.maxFee); - - if (params.rate) - options.rate = Amount.value(params.rate); - - if (params.m != null) { - options.m = Number(params.m); - enforce(util.isUInt32(options.m), 'm must be a number.'); - } - - if (params.n != null) { - options.n = Number(params.n); - enforce(util.isUInt32(options.n), 'n must be a number.'); - } - - if (params.blocks != null) { - options.blocks = Number(params.blocks); - enforce(util.isUInt32(options.blocks), 'Blocks must be a number.'); - } - - if (params.selection != null) { - options.selection = params.selection; - enforce(typeof options.selection === 'string', - 'selection must be a string.'); - } - - if (params.smart != null) { - if (typeof params.smart === 'string') { - options.smart = Boolean(params.smart); - } else { - options.smart = params.smart; - enforce(typeof options.smart === 'boolean', - 'smart must be a boolean.'); - } - } - - if (params.subtractFee != null) { - if (typeof params.subtractFee === 'number') { - options.subtractFee = params.subtractFee; - enforce(util.isUInt32(options.subtractFee), - 'subtractFee must be a number.'); - } else { - options.subtractFee = params.subtractFee; - enforce(typeof options.subtractFee === 'boolean', - 'subtractFee must be a boolean.'); - } - } - - if (params.watchOnly != null) { - enforce(typeof params.watchOnly === 'boolean', - 'watchOnly must be a boolean.'); - options.watchOnly = params.watchOnly; - } - - if (params.accountKey) { - enforce(typeof params.accountKey === 'string', - 'accountKey must be a string.'); - options.accountKey = HD.fromBase58(params.accountKey, this.network); - } - - if (params.timeout != null) { - options.timeout = Number(params.timeout); - enforce(util.isNumber(options.timeout), 'Timeout must be a number.'); - } - - if (params.witness != null) { - enforce(typeof params.witness === 'boolean', - 'witness must be a boolean.'); - options.witness = params.witness; - } - - if (params.outputs) { - enforce(Array.isArray(params.outputs), 'Outputs must be an array.'); - options.outputs = []; - for (i = 0; i < params.outputs.length; i++) { - output = params.outputs[i]; - - enforce(output && typeof output === 'object', - 'Output must be an object.'); - - if (output.address) { - enforce(typeof output.address === 'string', - 'Address must be a string.'); - output.address = Address.fromBase58(output.address, this.network); - } else if (output.script) { - enforce(typeof output.script === 'string', - 'Script must be a string.'); - output.script = Script.fromRaw(output.script, 'hex'); - } else { - enforce(false, 'No address or script present.'); - } - - options.outputs.push({ - address: output.address || null, - script: output.script || null, - value: Amount.value(output.value) - }); - } - } - - if (params.address) { - if (Array.isArray(options.address)) { - options.address = []; - for (i = 0; i < params.address.length; i++) { - address = params.address[i]; - enforce(typeof address === 'string', 'Address must be a string.'); - address = Address.fromBase58(address, this.network); - options.address.push(address); - } - } else { - enforce(typeof params.address === 'string', - 'Address must be a string.'); - options.address = Address.fromBase58(params.address, this.network); - } - } - - if (params.tx) { - if (typeof params.tx === 'object') { - options.tx = TX.fromJSON(params.tx); - } else { - enforce(typeof params.tx === 'string', - 'TX must be a hex string.'); - options.tx = TX.fromRaw(params.tx, 'hex'); - } - } - - if (params.account != null) { - if (typeof params.account === 'number') { - options.account = params.account; - enforce(util.isUInt32(options.account), 'Account must be a number.'); - } else { - enforce(typeof params.account === 'string', - 'Account must be a string.'); - options.account = params.account; - } - } - - if (params.type) { - enforce(typeof params.type === 'string', 'Type must be a string.'); - options.type = params.type; - } - - if (params.name) { - enforce(typeof params.name === 'string', 'Name must be a string.'); - options.name = params.name; - } - - if (params.privateKey) { - enforce(typeof params.privateKey === 'string', 'Key must be a string.'); - options.privateKey = KeyRing.fromSecret(params.privateKey, this.network); - } - - if (params.publicKey) { - enforce(typeof params.publicKey === 'string', 'Key must be a string.'); - options.publicKey = new Buffer(params.publicKey, 'hex'); - options.publicKey = KeyRing.fromKey(options.publicKey, this.network); - } - - if (params.master) { - enforce(typeof params.master === 'string', 'Key must be a string.'); - options.master = HD.fromBase58(params.master, this.network); - } - - if (params.mnemonic) { - enforce(typeof params.mnemonic === 'string', 'Key must be a string.'); - options.master = HD.fromMnemonic(params.mnemonic, this.network); - } - - if (params.old) { - enforce(typeof params.old === 'string', 'Passphrase must be a string.'); - enforce(params.old.length > 0, 'Passphrase must be a string.'); - options.old = params.old; - } - - if (params.passphrase) { - enforce(typeof params.passphrase === 'string', - 'Passphrase must be a string.'); - enforce(params.passphrase.length > 0, 'Passphrase must be a string.'); - options.passphrase = params.passphrase; - } - - if (params.token) { - enforce(util.isHex(params.token), 'Wallet token must be a hex string.'); - enforce(params.token.length === 64, 'Wallet token must be 32 bytes.'); - options.token = new Buffer(params.token, 'hex'); - } - - if (params.path) { - enforce(typeof params.path === 'string', 'Passphrase must be a string.'); - options.path = params.path; - } - - req.options = options; - })); - - this.hook(co(function* (req, res) { - var options = req.options; - var wallet; - - if (req.path.length < 2 || req.path[0] !== 'wallet') - return; - - if (!this.options.walletAuth) { - wallet = yield this.walletdb.get(options.id); - - if (!wallet) { - res.send(404); - return; - } - - req.wallet = wallet; - - return; - } - - try { - wallet = yield this.walletdb.auth(options.id, options.token); - } catch (err) { - this.logger.info('Auth failure for %s: %s.', - req.options.id, err.message); - res.send(403, { error: err.message }); - return; - } - - if (!wallet) { - res.send(404); - return; - } - - req.wallet = wallet; - - this.logger.info('Successful auth for %s.', options.id); + this.initRouter(); + this.initSockets(); +}; + +/** + * Initialize routes. + * @private + */ + +HTTPServer.prototype.initRouter = function initRouter() { + this.use(this.cors()); + + if (!this.options.noAuth) { + this.use(this.basicAuth({ + username: 'bitcoinrpc', + password: this.options.apiKey, + realm: 'node' + })); + } + + this.use(this.bodyParser({ + contentType: 'json' })); // JSON RPC this.post('/', co(function* (req, res) { - var out = []; - var cmds = req.body; - var array = true; - var i, cmd, json; + var json = yield this.rpc.call(req.body, req.query); - if (!this.rpc) { - RPC = require('./rpc'); - this.rpc = new RPC(this.node); - } + json = JSON.stringify(json); + json += '\n'; - if (!Array.isArray(cmds)) { - cmds = [cmds]; - array = false; - } + res.setHeader('X-Long-Polling', '/?longpoll=1'); - for (i = 0; i < cmds.length; i++) { - cmd = cmds[i]; - - enforce(cmd && typeof cmd === 'object', 'Command must be an object.'); - enforce(typeof cmd.method === 'string', 'Method must be a string.'); - - if (!cmd.params) - cmd.params = []; - - enforce(Array.isArray(cmd.params), 'Params must be an array.'); - - enforce(!cmd.id || typeof cmd.id !== 'object', 'Invalid ID.'); - } - - for (i = 0; i < cmds.length; i++) { - cmd = cmds[i]; - - if (cmd.method === 'getwork') { - res.setHeader('X-Long-Polling', '/?longpoll=1'); - if (req.query.longpoll) - cmd.method = 'getworklp'; - } - - 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); - } - } - - try { - json = yield this.rpc.execute(cmd); - } catch (err) { - if (err.type === 'RPCError') { - out.push({ - result: null, - error: { - message: err.message, - code: -1 - }, - id: cmd.id - }); - continue; - } - - this.logger.error(err); - - out.push({ - result: null, - error: { - message: err.message, - code: 1 - }, - id: cmd.id - }); - - continue; - } - - out.push({ - result: json != null ? json : null, - error: null, - id: cmd.id - }); - } - - if (!array) - out = out[0]; - - out = JSON.stringify(out); - out += '\n'; - - res.send(200, out, 'json'); + res.send(200, json, 'json'); })); this.get('/', co(function* (req, res) { @@ -653,17 +149,18 @@ HTTPServer.prototype._init = function _init() { adjusted: this.network.now(), offset: this.network.time.offset }, - memory: getMemory() + memory: util.memoryUsage() }); })); // UTXO by address this.get('/coin/address/:address', co(function* (req, res) { - var address = req.options.address; + var valid = req.valid(); + var address = valid.str('address'); var result = []; var i, coins, coin; - enforce(req.options.address, 'Address is required.'); + enforce(address, 'Address is required.'); enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); coins = yield this.node.getCoinsByAddress(address); @@ -678,12 +175,13 @@ HTTPServer.prototype._init = function _init() { // UTXO by id this.get('/coin/:hash/:index', co(function* (req, res) { - var hash = req.options.hash; - var index = req.options.index; + var valid = req.valid(); + var hash = valid.hash('hash'); + var index = valid.num('index'); var coin; - enforce(req.options.hash, 'Hash is required.'); - enforce(req.options.index != null, 'Index is required.'); + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); coin = yield this.node.getCoin(hash, index); @@ -698,11 +196,12 @@ HTTPServer.prototype._init = function _init() { // Bulk read UTXOs this.post('/coin/address', co(function* (req, res) { - var address = req.options.address; + var valid = req.valid(); + var address = valid.array('addresses'); var result = []; var i, coins, coin; - enforce(req.options.address, 'Address is required.'); + enforce(address, 'Address is required.'); enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.'); coins = yield this.node.getCoinsByAddress(address); @@ -717,10 +216,11 @@ HTTPServer.prototype._init = function _init() { // TX by hash this.get('/tx/:hash', co(function* (req, res) { - var hash = req.options.hash; + var valid = req.valid(); + var hash = valid.hash('hash'); var meta, view; - enforce(req.options.hash, 'Hash is required.'); + enforce(hash, 'Hash is required.'); enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); meta = yield this.node.getMeta(hash); @@ -737,11 +237,12 @@ HTTPServer.prototype._init = function _init() { // TX by address this.get('/tx/address/:address', co(function* (req, res) { - var address = req.options.address; + var valid = req.valid(); + var address = valid.str('address'); var result = []; var i, metas, meta, view; - enforce(req.options.address, 'Address is required.'); + enforce(address, 'Address is required.'); enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.'); metas = yield this.node.getMetaByAddress(address); @@ -757,7 +258,8 @@ HTTPServer.prototype._init = function _init() { // Bulk read TXs this.post('/tx/address', co(function* (req, res) { - var address = req.options.address; + var valid = req.valid(); + var address = valid.array('address'); var result = []; var i, metas, meta, view; @@ -777,10 +279,12 @@ HTTPServer.prototype._init = function _init() { // Block by hash/height this.get('/block/:block', co(function* (req, res) { - var hash = req.options.hash || req.options.height; + var valid = req.valid(); + var hash = valid.get('block'); var block, view, height; - enforce(hash != null, 'Hash or height required.'); + enforce(typeof hash === 'number' || typeof hash === 'string', + 'Hash or height required.'); enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.'); block = yield this.chain.db.getBlock(hash); @@ -821,13 +325,17 @@ HTTPServer.prototype._init = function _init() { // Broadcast TX this.post('/broadcast', co(function* (req, res) { - enforce(req.options.tx, 'TX is required.'); + var valid = req.valid(); + var tx = valid.buf('tx'); + enforce(tx, 'TX is required.'); yield this.node.sendTX(req.options.tx); res.send(200, { success: true }); })); // Estimate fee this.get('/fee', function(req, res) { + var valid = req.valid(); + var blocks = valid.num('blocks'); var fee; if (!this.fees) { @@ -835,524 +343,22 @@ HTTPServer.prototype._init = function _init() { return; } - fee = this.fees.estimateFee(req.options.blocks); + fee = this.fees.estimateFee(blocks); res.send(200, { rate: Amount.btc(fee) }); }); // Reset chain this.post('/reset', co(function* (req, res) { - var options = req.options; - var hash = options.hash || options.height; + var valid = req.valid(); + var height = valid.num('height'); - enforce(hash != null, 'Hash or height is required.'); + enforce(height != null, 'Hash or height is required.'); - yield this.chain.reset(hash); + yield this.chain.reset(height); res.send(200, { success: true }); })); - - // Rescan - this.post('/rescan', co(function* (req, res) { - var options = req.options; - var height = options.height; - - res.send(200, { success: true }); - - yield this.walletdb.rescan(height); - })); - - // Resend - this.post('/resend', co(function* (req, res) { - yield this.walletdb.resend(); - res.send(200, { success: true }); - })); - - // Backup WalletDB - this.post('/backup', co(function* (req, res) { - var options = req.options; - var path = options.path; - - enforce(path, 'Path is required.'); - - yield this.walletdb.backup(path); - - res.send(200, { success: true }); - })); - - // List wallets - this.get('/wallets', co(function* (req, res) { - var wallets = yield this.walletdb.getWallets(); - res.send(200, wallets); - })); - - // Get wallet - this.get('/wallet/:id', function(req, res) { - res.send(200, req.wallet.toJSON()); - }); - - // Get wallet master key - this.get('/wallet/:id/master', function(req, res) { - if (!req.admin) { - res.send(403, { error: 'Admin access required.' }); - return; - } - - res.send(200, req.wallet.master.toJSON(true)); - }); - - // Create wallet - this.post('/wallet/:id?', co(function* (req, res) { - var wallet = yield this.walletdb.create(req.options); - res.send(200, wallet.toJSON()); - })); - - // List accounts - this.get('/wallet/:id/account', co(function* (req, res) { - var accounts = yield req.wallet.getAccounts(); - res.send(200, accounts); - })); - - // Get account - this.get('/wallet/:id/account/:account', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var account; - - enforce(acct != null, 'Account is required.'); - - account = yield req.wallet.getAccount(acct); - - if (!account) { - res.send(404); - return; - } - - res.send(200, account.toJSON()); - })); - - // Create account - this.post('/wallet/:id/account/:account?', co(function* (req, res) { - var options = req.options; - var passphrase = options.passphrase; - var account; - - if (typeof options.account === 'string') { - options.name = options.account; - options.account = null; - } - - account = yield req.wallet.createAccount(options, passphrase); - - if (!account) { - res.send(404); - return; - } - - res.send(200, account.toJSON()); - })); - - // Change passphrase - this.post('/wallet/:id/passphrase', co(function* (req, res) { - var options = req.options; - var old = options.old; - var new_ = options.passphrase; - enforce(old || new_, 'Passphrase is required.'); - yield req.wallet.setPassphrase(old, new_); - res.send(200, { success: true }); - })); - - // Unlock wallet - this.post('/wallet/:id/unlock', co(function* (req, res) { - var options = req.options; - var passphrase = options.passphrase; - var timeout = options.timeout; - enforce(passphrase, 'Passphrase is required.'); - yield req.wallet.unlock(passphrase, timeout); - res.send(200, { success: true }); - })); - - // Lock wallet - this.post('/wallet/:id/lock', co(function* (req, res) { - yield req.wallet.lock(); - res.send(200, { success: true }); - })); - - // Import key - this.post('/wallet/:id/import', co(function* (req, res) { - var options = req.options; - var acct = req.options.name || req.options.account; - var key = options.privateKey || options.publicKey; - - if (key) { - yield req.wallet.importKey(acct, key); - res.send(200, { success: true }); - return; - } - - if (options.address) { - enforce(options.address instanceof Address, 'Address is required.'); - yield req.wallet.importAddress(acct, options.address); - res.send(200, { success: true }); - return; - } - - enforce(false, 'Key or address is required.'); - })); - - // Generate new token - this.post('/wallet/:id/retoken', co(function* (req, res) { - var options = req.options; - var token = yield req.wallet.retoken(options.passphrase); - res.send(200, { token: token.toString('hex') }); - })); - - // Send TX - this.post('/wallet/:id/send', co(function* (req, res) { - var options = req.options; - var passphrase = options.passphrase; - var tx = yield req.wallet.send(options, passphrase); - var details = yield req.wallet.getDetails(tx.hash('hex')); - res.send(200, details.toJSON()); - })); - - // Create TX - this.post('/wallet/:id/create', co(function* (req, res) { - var options = req.options; - var passphrase = options.passphrase; - var tx = yield req.wallet.createTX(options); - yield req.wallet.sign(tx, passphrase); - res.send(200, tx.getJSON(this.network)); - })); - - // Sign TX - this.post('/wallet/:id/sign', co(function* (req, res) { - var options = req.options; - var passphrase = options.passphrase; - var tx = req.options.tx; - enforce(tx, 'TX is required.'); - yield req.wallet.sign(tx, passphrase); - res.send(200, tx.getJSON(this.network)); - })); - - // Zap Wallet TXs - this.post('/wallet/:id/zap', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var age = options.age; - enforce(age, 'Age is required.'); - yield req.wallet.zap(acct, age); - res.send(200, { success: true }); - })); - - // Abandon Wallet TX - this.del('/wallet/:id/tx/:hash', co(function* (req, res) { - var hash = req.options.hash; - enforce(hash, 'Hash is required.'); - yield req.wallet.abandon(hash); - res.send(200, { success: true }); - })); - - // List blocks - this.get('/wallet/:id/block', co(function* (req, res) { - var heights = yield req.wallet.getBlocks(); - res.send(200, heights); - })); - - // Get Block Record - this.get('/wallet/:id/block/:height', co(function* (req, res) { - var height = req.options.height; - var block; - - enforce(height != null, 'Height is required.'); - - block = yield req.wallet.getBlock(height); - - if (!block) { - res.send(404); - return; - } - - res.send(200, block.toJSON()); - })); - - // Add key - this.put('/wallet/:id/shared-key', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var key = options.accountKey; - enforce(key, 'Key is required.'); - yield req.wallet.addSharedKey(acct, key); - res.send(200, { success: true }); - })); - - // Remove key - this.del('/wallet/:id/shared-key', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var key = options.accountKey; - enforce(key, 'Key is required.'); - yield req.wallet.removeSharedKey(acct, key); - res.send(200, { success: true }); - })); - - // Get key by address - this.get('/wallet/:id/key/:address', co(function* (req, res) { - var options = req.options; - var address = options.address; - var key; - - enforce(address instanceof Address, 'Address is required.'); - - key = yield req.wallet.getKey(address); - - if (!key) { - res.send(404); - return; - } - - res.send(200, key.toJSON()); - })); - - // Get private key - this.get('/wallet/:id/wif/:address', co(function* (req, res) { - var options = req.options; - var address = options.address; - var passphrase = options.passphrase; - var key; - - enforce(address instanceof Address, 'Address is required.'); - - key = yield req.wallet.getPrivateKey(address, passphrase); - - if (!key) { - res.send(404); - return; - } - - res.send(200, { privateKey: key.toSecret() }); - })); - - // Create address - this.post('/wallet/:id/address', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var address = yield req.wallet.createReceive(acct); - res.send(200, address.toJSON()); - })); - - // Create change address - this.post('/wallet/:id/change', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var address = yield req.wallet.createChange(acct); - res.send(200, address.toJSON()); - })); - - // Create nested address - this.post('/wallet/:id/nested', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var address = yield req.wallet.createNested(acct); - res.send(200, address.toJSON()); - })); - - // Wallet Balance - this.get('/wallet/:id/balance', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var balance = yield req.wallet.getBalance(acct); - - if (!balance) { - res.send(404); - return; - } - - res.send(200, balance.toJSON()); - })); - - // Wallet UTXOs - this.get('/wallet/:id/coin', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var coins = yield req.wallet.getCoins(acct); - var result = []; - var i, coin; - - sortCoins(coins); - - for (i = 0; i < coins.length; i++) { - coin = coins[i]; - result.push(coin.getJSON(this.network)); - } - - res.send(200, result); - })); - - // Locked coins - this.get('/wallet/:id/coin/locked', co(function* (req, res) { - var locked = this.wallet.getLocked(); - var result = []; - var i, outpoint; - - for (i = 0; i < locked.length; i++) { - outpoint = locked[i]; - result.push(outpoint.toJSON()); - } - - res.send(200, result); - })); - - // Lock coin - this.put('/wallet/:id/coin/locked', co(function* (req, res) { - var options = req.options.hash; - var outpoint; - - enforce(options.hash, 'Hash is required.'); - enforce(options.index != null, 'Index is required.'); - - outpoint = new Outpoint(options.hash, options.index); - - this.wallet.lockCoin(outpoint); - })); - - // Unlock coin - this.del('/wallet/:id/coin/locked', co(function* (req, res) { - var options = req.options.hash; - var outpoint; - - enforce(options.hash, 'Hash is required.'); - enforce(options.index != null, 'Index is required.'); - - outpoint = new Outpoint(options.hash, options.index); - - this.wallet.unlockCoin(outpoint); - })); - - // Wallet Coin - this.get('/wallet/:id/coin/:hash/:index', co(function* (req, res) { - var hash = req.options.hash; - var index = req.options.index; - var coin; - - enforce(hash, 'Hash is required.'); - enforce(index != null, 'Index is required.'); - - coin = yield req.wallet.getCoin(hash, index); - - if (!coin) { - res.send(404); - return; - } - - res.send(200, coin.getJSON(this.network)); - })); - - // Wallet TXs - this.get('/wallet/:id/tx/history', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var txs = yield req.wallet.getHistory(acct); - var result = []; - var i, details, item; - - sortTX(txs); - - details = yield req.wallet.toDetails(txs); - - for (i = 0; i < details.length; i++) { - item = details[i]; - result.push(item.toJSON()); - } - - res.send(200, result); - })); - - // Wallet Pending TXs - this.get('/wallet/:id/tx/unconfirmed', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var txs = yield req.wallet.getPending(acct); - var result = []; - var i, details, item; - - sortTX(txs); - - details = yield req.wallet.toDetails(txs); - - for (i = 0; i < details.length; i++) { - item = details[i]; - result.push(item.toJSON()); - } - - res.send(200, result); - })); - - // Wallet TXs within time range - this.get('/wallet/:id/tx/range', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var txs = yield req.wallet.getRange(acct, options); - var details = yield req.wallet.toDetails(txs); - var result = []; - var i, item; - - for (i = 0; i < details.length; i++) { - item = details[i]; - result.push(item.toJSON()); - } - - res.send(200, result); - })); - - // Last Wallet TXs - this.get('/wallet/:id/tx/last', co(function* (req, res) { - var options = req.options; - var acct = options.name || options.account; - var limit = options.limit; - var txs = yield req.wallet.getLast(acct, limit); - var details = yield req.wallet.toDetails(txs); - var result = []; - var i, item; - - for (i = 0; i < details.length; i++) { - item = details[i]; - result.push(item.toJSON()); - } - - res.send(200, result); - })); - - // Wallet TX - this.get('/wallet/:id/tx/:hash', co(function* (req, res) { - var hash = req.options.hash; - var tx, details; - - enforce(hash, 'Hash is required.'); - - tx = yield req.wallet.getTX(hash); - - if (!tx) { - res.send(404); - return; - } - - details = yield req.wallet.toDetails(tx); - - res.send(200, details.toJSON()); - })); - - // Resend - this.post('/wallet/:id/resend', co(function* (req, res) { - yield req.wallet.resend(); - res.send(200, { success: true }); - })); - - this.server.on('error', function(err) { - self.emit('error', err); - }); - - this._initIO(); }; /** @@ -1360,420 +366,197 @@ HTTPServer.prototype._init = function _init() { * @private */ -HTTPServer.prototype._initIO = function _initIO() { +HTTPServer.prototype.initSockets = function initSockets() { var self = this; - if (!this.server.io) + if (!this.io) return; - this.server.on('websocket', function(ws) { - var socket = new ClientSocket(self, ws); + this.on('socket', function(ws) { + self.handleSocket(ws); + }); +}; - socket.start(); +/** + * Handle new websocket. + * @private + * @param {WebSocket} socket + */ - socket.on('error', function(err) { - self.emit('error', err); - }); +HTTPServer.prototype.handleSocket = function handleSocket(ws) { + var self = this; + var socket = new ClientSocket(this, ws); - socket.on('disconnect', function() { - socket.destroy(); - }); + socket.start(); - socket.on('auth', cob(function* (args) { - var key = args[0]; - var hash, api, service; - - if (socket.auth) - throw { error: 'Already authed.' }; - - socket.stop(); - - if (!self.options.noAuth) { - hash = hash256(key); - api = crypto.ccmp(hash, self.options.apiHash); - service = crypto.ccmp(hash, self.options.serviceHash); - - if (!api && !service) - throw { error: 'Bad key.' }; - - socket.api = api; - } - - socket.auth = true; - - self.logger.info('Successful auth from %s.', socket.host); - - self.emit('websocket', socket); - })); - - socket.emit('version', { - version: pkg.version, - network: self.network.type - }); + socket.on('disconnect', function() { + socket.destroy(); }); - this.on('websocket', function(socket) { - socket.on('wallet join', cob(function* (args) { - var id = args[0]; - var token = args[1]; - var wallet; + socket.hook('auth', function(args) { + var valid = new Validator([args]); + var key = valid.str(0); + var hash; - if (typeof id !== 'string') - throw { error: 'Invalid parameter.' }; + if (socket.auth) + throw new Error('Already authed.'); - if (!self.options.walletAuth) { - socket.join(id); - return; - } + socket.stop(); - if (!util.isHex256(token)) - throw { error: 'Invalid parameter.' }; - - token = new Buffer(token, 'hex'); - - if (socket.api && id === '!all') { - socket.join(id); - return; - } - - try { - wallet = yield self.walletdb.auth(id, token); - } catch (e) { - self.logger.info('Wallet auth failure for %s: %s.', id, e.message); - throw { error: 'Bad token.' }; - } - - if (!wallet) - throw { error: 'Wallet does not exist.' }; - - self.logger.info('Successful wallet auth for %s.', id); - - socket.join(id); - })); - - socket.on('wallet leave', cob(function* (args) { - var id = args[0]; - - if (typeof id !== 'string') - throw { error: 'Invalid parameter.' }; - - socket.leave(id); - })); - - socket.on('options', cob(function* (args) { - var options = args[0]; - try { - socket.setOptions(options); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('watch chain', cob(function* (args) { - if (!socket.api) - throw { error: 'Not authorized.' }; - - try { - socket.watchChain(); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('unwatch chain', cob(function* (args) { - if (!socket.api) - throw { error: 'Not authorized.' }; - - try { - socket.unwatchChain(); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('set filter', cob(function* (args) { - var data = args[0]; - var filter; - - if (!util.isHex(data) && !Buffer.isBuffer(data)) - throw { error: 'Invalid parameter.' }; - - if (!socket.api) - throw { error: 'Not authorized.' }; - - try { - filter = Bloom.fromRaw(data, 'hex'); - socket.setFilter(filter); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('get tip', cob(function* (args) { - return socket.frameEntry(self.chain.tip); - })); - - socket.on('get entry', cob(function* (args) { - var block = args[0]; - var entry; - - if (typeof block === 'string') { - if (!util.isHex256(block)) - throw { error: 'Invalid parameter.' }; - block = util.revHex(block); - } else { - if (!util.isUInt32(block)) - throw { error: 'Invalid parameter.' }; - } - - try { - entry = yield self.chain.db.getEntry(block); - if (!(yield entry.isMainChain())) - entry = null; - } catch (e) { - throw { error: e.message }; - } - - if (!entry) - return null; - - return socket.frameEntry(entry); - })); - - socket.on('add filter', cob(function* (args) { - var chunks = args[0]; - - if (!Array.isArray(chunks)) - throw { error: 'Invalid parameter.' }; - - if (!socket.api) - throw { error: 'Not authorized.' }; - - try { - socket.addFilter(chunks); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('reset filter', cob(function* (args) { - if (!socket.api) - throw { error: 'Not authorized.' }; - - try { - socket.resetFilter(); - } catch (e) { - throw { error: e.message }; - } - })); - - socket.on('estimate fee', cob(function* (args) { - var blocks = args[0]; - var rate; - - if (blocks != null && !util.isNumber(blocks)) - throw { error: 'Invalid parameter.' }; - - if (!self.fees) { - rate = self.network.feeRate; - rate = Amount.btc(rate); - return rate; - } - - rate = self.fees.estimateFee(blocks); - rate = Amount.btc(rate); - - return rate; - })); - - socket.on('send', cob(function* (args) { - var data = args[0]; - var tx; - - if (!util.isHex(data) && !Buffer.isBuffer(data)) - throw { error: 'Invalid parameter.' }; - - try { - tx = TX.fromRaw(data, 'hex'); - } catch (e) { - throw { error: 'Invalid parameter.' }; - } - - self.node.send(tx); - })); - - socket.on('rescan', cob(function* (args) { - var start = args[0]; - - if (!util.isHex256(start) && !util.isUInt32(start)) - throw { error: 'Invalid parameter.' }; - - if (!socket.api) - throw { error: 'Not authorized.' }; - - if (typeof start === 'string') - start = util.revHex(start); - - try { - yield socket.scan(start); - } catch (e) { - throw { error: e.message }; - } - })); - }); - - this.walletdb.on('tx', function(id, tx, details) { - var json = details.toJSON(); - self.server.io.to(id).emit('wallet tx', json); - self.server.io.to('!all').emit('wallet tx', id, json); - }); - - this.walletdb.on('confirmed', function(id, tx, details) { - var json = details.toJSON(); - self.server.io.to(id).emit('wallet confirmed', json); - self.server.io.to('!all').emit('wallet confirmed', id, json); - }); - - this.walletdb.on('unconfirmed', function(id, tx, details) { - var json = details.toJSON(); - self.server.io.to(id).emit('wallet unconfirmed', json); - self.server.io.to('!all').emit('wallet unconfirmed', id, json); - }); - - this.walletdb.on('conflict', function(id, tx, details) { - var json = details.toJSON(); - self.server.io.to(id).emit('wallet conflict', json); - self.server.io.to('!all').emit('wallet conflict', id, json); - }); - - this.walletdb.on('balance', function(id, balance) { - var json = balance.toJSON(); - self.server.io.to(id).emit('wallet balance', json); - self.server.io.to('!all').emit('wallet balance', id, json); - }); - - this.walletdb.on('address', function(id, receive) { - var json = []; - var i, address; - - for (i = 0; i < receive.length; i++) { - address = receive[i]; - json.push(address.toJSON()); + if (!self.options.noAuth) { + hash = hash256(key); + if (!crypto.ccmp(hash, self.options.apiHash)) + throw new Error('Bad key.'); } - self.server.io.to(id).emit('wallet address', json); - self.server.io.to('!all').emit('wallet address', id, json); + socket.auth = true; + + self.logger.info('Successful auth from %s.', socket.host); + self.handleAuth(socket); + + return null; + }); + + socket.emit('version', { + version: pkg.version, + network: self.network.type }); }; /** - * Open the server, wait for socket. - * @returns {Promise} + * Handle new auth'd websocket. + * @private + * @param {WebSocket} socket */ -HTTPServer.prototype.open = co(function* open() { - yield this.server.open(); +HTTPServer.prototype.handleAuth = function handleAuth(socket) { + var self = this; - this.logger.info('HTTP server loaded.'); + socket.hook('options', function (args) { + var options = args[0]; + socket.setOptions(options); + }); - if (this.options.noAuth) { - this.logger.warning('WARNING: Your http server is open to the world.'); - return; - } + socket.hook('watch chain', function(args) { + if (!socket.auth) + throw new Error('Not authorized.'); - this.logger.info('HTTP API key: %s', this.options.apiKey); - this.logger.info('HTTP Service API key: %s', this.options.serviceKey); + socket.watchChain(); + }); - this.options.apiKey = null; - this.options.serviceKey = null; -}); + socket.hook('unwatch chain', function(args) { + if (!socket.auth) + throw new Error('Not authorized.'); -/** - * Close the server, wait for server socket to close. - * @returns {Promise} - */ + socket.unwatchChain(); + }); -HTTPServer.prototype.close = function close() { - return this.server.close(); -}; + socket.hook('set filter', function(args) { + var data = args[0]; + var filter; -/** - * Add a middleware to the stack. - * @param {String?} path - * @param {Function} handler - */ + if (!util.isHex(data) && !Buffer.isBuffer(data)) + throw new Error('Invalid parameter.'); -HTTPServer.prototype.use = function use(path, handler) { - if (!handler) { - handler = path; - path = null; - } - return this.server.use(path, handler, this); -}; + if (!socket.auth) + throw new Error('Not authorized.'); -/** - * Add a hook to the stack. - * @param {String?} path - * @param {Function} handler - */ + filter = Bloom.fromRaw(data, 'hex'); + socket.setFilter(filter); + }); -HTTPServer.prototype.hook = function hook(path, handler) { - if (!handler) { - handler = path; - path = null; - } - return this.server.hook(path, handler, this); -}; + socket.hook('get tip', function(args) { + return socket.frameEntry(self.chain.tip); + }); -/** - * Add a GET route. - * @param {String} path - * @param {Function} handler - */ + socket.hook('get entry', co(function* (args) { + var block = args[0]; + var entry; -HTTPServer.prototype.get = function get(path, handler) { - return this.server.get(path, handler, this); -}; + if (typeof block === 'string') { + if (!util.isHex256(block)) + throw new Error('Invalid parameter.'); + block = util.revHex(block); + } else { + if (!util.isUInt32(block)) + throw new Error('Invalid parameter.'); + } -/** - * Add a POST route. - * @param {String} path - * @param {Function} handler - */ + entry = yield self.chain.db.getEntry(block); -HTTPServer.prototype.post = function post(path, handler) { - return this.server.post(path, handler, this); -}; + if (!(yield entry.isMainChain())) + entry = null; -/** - * Add a PUT route. - * @param {String} path - * @param {Function} handler - */ + if (!entry) + return null; -HTTPServer.prototype.put = function put(path, handler) { - return this.server.put(path, handler, this); -}; + return socket.frameEntry(entry); + })); -/** - * Add a DEL route. - * @param {String} path - * @param {Function} handler - */ + socket.hook('add filter', function(args) { + var chunks = args[0]; -HTTPServer.prototype.del = function del(path, handler) { - return this.server.del(path, handler, this); -}; + if (!Array.isArray(chunks)) + throw new Error('Invalid parameter.'); -/** - * Listen on port and host. - * @param {Number} port - * @param {String} host - * @returns {Promise} - */ + if (!socket.auth) + throw new Error('Not authorized.'); -HTTPServer.prototype.listen = function listen(port, host) { - return this.server.listen(port, host); + socket.addFilter(chunks); + }); + + socket.hook('reset filter', function(args) { + if (!socket.auth) + throw new Error('Not authorized.'); + + socket.resetFilter(); + }); + + socket.hook('estimate fee', function(args) { + var blocks = args[0]; + var rate; + + if (blocks != null && !util.isNumber(blocks)) + throw new Error('Invalid parameter.'); + + if (!self.fees) { + rate = self.network.feeRate; + rate = Amount.btc(rate); + return rate; + } + + rate = self.fees.estimateFee(blocks); + rate = Amount.btc(rate); + + return rate; + }); + + socket.hook('send', function(args) { + var data = args[0]; + var tx; + + if (!util.isHex(data) && !Buffer.isBuffer(data)) + throw new Error('Invalid parameter.'); + + tx = TX.fromRaw(data, 'hex'); + + self.node.send(tx); + }); + + socket.hook('rescan', function(args) { + var start = args[0]; + + if (!util.isHex256(start) && !util.isUInt32(start)) + throw new Error('Invalid parameter.'); + + if (!socket.auth) + throw new Error('Not authorized.'); + + if (typeof start === 'string') + start = util.revHex(start); + + return socket.scan(start); + }); }; /** @@ -1792,18 +575,15 @@ function HTTPOptions(options) { this.node = null; this.apiKey = base58.encode(crypto.randomBytes(20)); this.apiHash = hash256(this.apiKey); - this.serviceKey = this.apiKey; - this.serviceHash = this.apiHash; this.noAuth = false; this.walletAuth = false; - this.sockets = true; + + this.prefix = null; this.host = '127.0.0.1'; - this.port = this.network.rpcPort; + this.port = 8080; + this.ssl = false; this.keyFile = null; this.certFile = null; - this.key = null; - this.cert = null; - this.contentType = 'json'; this.fromOptions(options); } @@ -1838,17 +618,6 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { 'API key must be under 200 bytes.'); this.apiKey = options.apiKey; this.apiHash = hash256(this.apiKey); - this.serviceKey = this.apiKey; - this.serviceHash = this.apiHash; - } - - if (options.serviceKey != null) { - assert(typeof options.serviceKey === 'string', - 'API key must be a string.'); - assert(options.serviceKey.length <= 200, - 'API key must be under 200 bytes.'); - this.serviceKey = options.serviceKey; - this.serviceHash = hash256(this.serviceKey); } if (options.noAuth != null) { @@ -1861,9 +630,16 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { this.walletAuth = options.walletAuth; } + if (options.prefix != null) { + assert(typeof options.prefix === 'string'); + this.prefix = options.prefix; + this.keyFile = this.prefix + '/key.pem'; + this.certFile = this.prefix + '/cert.pem'; + } + if (options.host != null) { assert(typeof options.host === 'string'); - this.host = IP.normalize(options.host); + this.host = options.host; } if (options.port != null) { @@ -1872,13 +648,6 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { this.port = options.port; } - if (options.prefix != null) { - assert(typeof options.prefix === 'string'); - this.prefix = options.prefix; - this.keyFile = this.prefix + '/key.pem'; - this.certFile = this.prefix + '/cert.pem'; - } - if (options.ssl != null) { assert(typeof options.ssl === 'boolean'); this.ssl = options.ssl; @@ -1896,7 +665,7 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { // Allow no-auth implicitly // if we're listening locally. - if (!options.apiKey && !options.serviceKey && options.noAuth == null) { + if (!options.apiKey) { if (this.host === '127.0.0.1' || this.host === '::1') this.noAuth = true; } @@ -1904,22 +673,6 @@ HTTPOptions.prototype.fromOptions = function fromOptions(options) { return this; }; -/** - * Load key and cert file. - * @private - */ - -HTTPOptions.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); -}; - /** * Instantiate http options from object. * @param {Object} options @@ -1946,11 +699,10 @@ function ClientSocket(server, socket) { this.server = server; this.socket = socket; - this.host = socket.conn.remoteAddress; + this.host = socket.remoteAddress; this.timeout = null; this.auth = false; this.filter = null; - this.api = false; this.raw = false; this.watching = false; @@ -1962,32 +714,15 @@ function ClientSocket(server, socket) { this.logger = this.server.logger; this.events = []; - this._init(); + this.init(); } util.inherits(ClientSocket, EventEmitter); -ClientSocket.prototype._init = function _init() { +ClientSocket.prototype.init = function init() { var self = this; var socket = this.socket; var emit = EventEmitter.prototype.emit; - var onevent = socket.onevent.bind(socket); - - socket.onevent = function(packet) { - var result = onevent(packet); - var args = packet.data || []; - var event = args.shift(); - var ack; - - if (typeof args[args.length - 1] === 'function') - ack = args.pop(); - else - ack = self.socket.ack(packet.id); - - emit.call(self, event, args, ack); - - return result; - }; socket.on('error', function(err) { emit.call(self, 'error', err); @@ -2242,6 +977,10 @@ ClientSocket.prototype.leave = function leave(id) { this.socket.leave(id); }; +ClientSocket.prototype.hook = function hook(type, handler) { + this.socket.hook(type, handler); +}; + ClientSocket.prototype.emit = function emit() { this.socket.emit.apply(this.socket, arguments); }; @@ -2275,20 +1014,11 @@ ClientSocket.prototype.destroy = function() { function hash256(data) { if (typeof data !== 'string') return new Buffer(0); + if (data.length > 200) return new Buffer(0); - return crypto.hash256(new Buffer(data, 'utf8')); -} -function softMerge(a, b, soft) { - var keys = Object.keys(b); - var i, key, value; - for (i = 0; i < keys.length; i++) { - key = keys[i]; - value = b[key]; - if (!soft || value) - a[key] = value; - } + return crypto.hash256(new Buffer(data, 'utf8')); } function enforce(value, msg) { @@ -2301,46 +1031,6 @@ function enforce(value, msg) { } } -function sortTX(txs) { - return txs.sort(function(a, b) { - return a.ps - b.ps; - }); -} - -function sortCoins(coins) { - return coins.sort(function(a, b) { - a = a.height === -1 ? 0x7fffffff : a.height; - b = b.height === -1 ? 0x7fffffff : b.height; - return a - b; - }); -} - -function isWalletPath(req) { - if (req.path.length >= 1 && req.path[0] === 'wallet') - return true; - - if (req.method === 'GET' && req.pathname === '/') - return true; - - return false; -} - -function getMemory() { - var mem; - - if (!process.memoryUsage) - return {}; - - mem = process.memoryUsage(); - - return { - rss: util.mb(mem.rss), - jsHeap: util.mb(mem.heapUsed), - jsHeapTotal: util.mb(mem.heapTotal), - nativeHeap: util.mb(mem.rss - mem.heapTotal) - }; -} - /** * TimedCB * @constructor diff --git a/lib/http/wallet.js b/lib/http/wallet.js index 09756d44..e2bf6294 100644 --- a/lib/http/wallet.js +++ b/lib/http/wallet.js @@ -115,6 +115,7 @@ HTTPWallet.prototype.open = co(function* open(options) { assert(this.id, 'No ID provided.'); yield this.client.open(); + yield this.client.sendWalletAuth(); yield this.client.join(this.id, this.token); }); @@ -128,6 +129,7 @@ HTTPWallet.prototype.create = co(function* create(options) { var wallet; yield this.client.open(); + yield this.client.sendWalletAuth(); wallet = yield this.client.createWallet(options); diff --git a/lib/mining/miner.js b/lib/mining/miner.js index 214e390a..9a91942e 100644 --- a/lib/mining/miner.js +++ b/lib/mining/miner.js @@ -107,6 +107,9 @@ Miner.prototype._open = co(function* open() { this.logger.info('Miner loaded (flags=%s).', this.options.coinbaseFlags.toString('utf8')); + + if (this.addresses.length === 0) + this.logger.warning('No reward address is set for miner!'); }); /** @@ -387,7 +390,8 @@ Miner.prototype.addAddress = function addAddress(address) { */ Miner.prototype.getAddress = function getAddress() { - assert(this.addresses.length !== 0, 'No address passed in for miner.'); + if (this.addresses.length === 0) + return; return this.addresses[Math.random() * this.addresses.length | 0]; }; diff --git a/lib/mining/minerblock.js b/lib/mining/minerblock.js index 3eec53df..1ba90a4f 100644 --- a/lib/mining/minerblock.js +++ b/lib/mining/minerblock.js @@ -14,6 +14,7 @@ var util = require('../utils/util'); var co = require('../utils/co'); var StaticWriter = require('../utils/staticwriter'); var Network = require('../protocol/network'); +var Address = require('../primitives/address'); var TX = require('../primitives/tx'); var Block = require('../primitives/block'); var Input = require('../primitives/input'); @@ -232,8 +233,10 @@ MinerBlock.prototype._init = function _init() { input.script.compile(); // Setup output script (variable size). - output.script.clear(); - output.script.fromAddress(this.address); + if (this.address) { + output.script.clear(); + output.script.fromAddress(this.address); + } // Update commitments. this.refresh(); @@ -345,6 +348,23 @@ MinerBlock.prototype.extraNonce = function extraNonce() { return bw.render(); }; +/** + * Set the reward output address. + * @param {Address} address + */ + +MinerBlock.prototype.setAddress = function setAddress(address) { + var output = this.coinbase.outputs[0]; + + this.address = Address(address); + + output.script.clear(); + output.script.fromAddress(this.address); + + // Update commitments. + this.refresh(); +}; + /** * Add a transaction to the block. Rebuilds the merkle tree, * updates coinbase and commitment. diff --git a/lib/net/pool.js b/lib/net/pool.js index 7a94578d..343f0772 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -268,7 +268,6 @@ Pool.prototype.resetChain = function resetChain() { Pool.prototype._close = co(function* close() { yield this.disconnect(); - yield this.hosts.close(); }); /** diff --git a/lib/node/config.js b/lib/node/config.js index eb2e23af..a2e42531 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -243,8 +243,8 @@ Config.prototype.str = function str(key, fallback) { if (value === null) return fallback; - assert(typeof value === 'string', - 'Passed in config option is of wrong type.'); + if (typeof value !== 'string') + throw new Error(key + ' must be a string.'); return value; }; @@ -266,15 +266,18 @@ Config.prototype.num = function num(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(typeof value === 'number', - 'Passed in config option is of wrong type.'); + if (typeof value !== 'number') + throw new Error(key + ' must be a string.'); return value; } + if (!/^\d+$/.test(value)) + throw new Error(key + ' must be a number.'); + value = parseInt(value, 10); if (!isFinite(value)) - return fallback; + throw new Error(key + ' must be a number.'); return value; }; @@ -296,8 +299,8 @@ Config.prototype.bool = function bool(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(typeof value === 'boolean', - 'Passed in config option is of wrong type.'); + if (typeof value !== 'boolean') + throw new Error(key + ' must be a boolean.'); return value; } @@ -307,7 +310,7 @@ Config.prototype.bool = function bool(key, fallback) { if (value === 'false' || value === '0') return false; - return fallback; + throw new Error(key + ' must be a boolean.'); }; /** @@ -319,6 +322,7 @@ Config.prototype.bool = function bool(key, fallback) { Config.prototype.buf = function buf(key, fallback) { var value = this.get(key); + var data; if (fallback === undefined) fallback = null; @@ -327,12 +331,17 @@ Config.prototype.buf = function buf(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(Buffer.isBuffer(value), - 'Passed in config option is of wrong type.'); + if (!Buffer.isBuffer(value)) + throw new Error(key + ' must be a buffer.'); return value; } - return new Buffer(value, 'hex'); + data = new Buffer(value, 'hex'); + + if (data.length !== value.length / 2) + throw new Error(key + ' must be a hex string.'); + + return data; }; /** @@ -352,8 +361,8 @@ Config.prototype.list = function list(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(Array.isArray(value), - 'Passed in config option is of wrong type.'); + if (!Array.isArray(value)) + throw new Error(key + ' must be an array.'); return value; } @@ -377,8 +386,8 @@ Config.prototype.obj = function obj(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(value && typeof value === 'object', - 'Passed in config option is of wrong type.'); + if (!value || typeof value !== 'object') + throw new Error(key + ' must be an object.'); return value; } @@ -388,8 +397,8 @@ Config.prototype.obj = function obj(key, fallback) { ; } - assert(value && typeof value === 'object', - 'Passed in config option is of wrong type.'); + if (!value || typeof value !== 'object') + throw new Error(key + ' must be an object.'); return value; }; @@ -411,13 +420,12 @@ Config.prototype.func = function func(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(value && typeof value === 'function', - 'Passed in config option is of wrong type.'); + if (!value || typeof value !== 'function') + throw new Error(key + ' must be a function.'); return value; } - assert(false, - 'Passed in config option is of wrong type.'); + throw new Error(key + ' must be a function.'); }; /** @@ -486,8 +494,8 @@ Config.prototype.boolpath = function boolpath(key, fallback) { return fallback; if (typeof value !== 'string') { - assert(typeof value === 'boolean' || typeof value === 'string', - 'Passed in config option is of wrong type.'); + if (typeof value !== 'boolean' && typeof value !== 'string') + throw new Error(key + ' must be a boolean or string.'); return value; } diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 3fcee1ef..15cd8f89 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -15,12 +15,11 @@ var Fees = require('../mempool/fees'); var Mempool = require('../mempool/mempool'); var Pool = require('../net/pool'); var Miner = require('../mining/miner'); -var WalletDB = require('../wallet/walletdb'); var HTTPServer = require('../http/server'); /** * Respresents a fullnode complete with a - * chain, mempool, miner, wallet, etc. + * chain, mempool, miner, etc. * @alias module:node.FullNode * @extends Node * @constructor @@ -30,7 +29,6 @@ var HTTPServer = require('../http/server'); * @property {Mempool} mempool * @property {Pool} pool * @property {Miner} miner - * @property {WalletDB} walletdb * @property {HTTPServer} http * @emits FullNode#block * @emits FullNode#tx @@ -126,22 +124,6 @@ function FullNode(options) { reservedSigops: this.config.num('reserved-sigops') }); - // Wallet database needs access to fees. - this.walletdb = new WalletDB({ - network: this.network, - logger: this.logger, - client: this.client, - db: this.config.str('db'), - prefix: this.config.prefix, - maxFiles: this.config.num('walletdb-max-files'), - cacheSize: this.config.mb('walletdb-cache-size'), - witness: false, - checkpoints: this.config.bool('checkpoints'), - startHeight: this.config.num('start-height'), - wipeNoReally: this.config.bool('wipe-no-really'), - verify: false - }); - // HTTP needs access to the node. if (!HTTPServer.unsupported) { this.http = new HTTPServer({ @@ -180,7 +162,6 @@ FullNode.prototype._init = function _init() { this.mempool.on('error', onError); this.pool.on('error', onError); this.miner.on('error', onError); - this.walletdb.on('error', onError); if (this.http) this.http.on('error', onError); @@ -217,6 +198,8 @@ FullNode.prototype._init = function _init() { } self.emit('reset', tip); })); + + this.loadPlugins(); }; /** @@ -231,10 +214,8 @@ FullNode.prototype._open = co(function* open() { yield this.mempool.open(); yield this.miner.open(); yield this.pool.open(); - yield this.walletdb.open(); - // Ensure primary wallet. - yield this.openWallet(); + yield this.openPlugins(); if (this.http) yield this.http.open(); @@ -252,11 +233,8 @@ FullNode.prototype._close = co(function* close() { if (this.http) yield this.http.close(); - yield this.wallet.destroy(); + yield this.closePlugins(); - this.wallet = null; - - yield this.walletdb.close(); yield this.pool.close(); yield this.miner.close(); yield this.mempool.close(); diff --git a/lib/node/node.js b/lib/node/node.js index 2e432b4b..d0841a64 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -13,7 +13,6 @@ var util = require('../utils/util'); var co = require('../utils/co'); var Network = require('../protocol/network'); var Logger = require('./logger'); -var NodeClient = require('./nodeclient'); var workerPool = require('../workers/workerpool').pool; var ec = require('../crypto/ec'); var native = require('../utils/native'); @@ -48,8 +47,6 @@ function Node(options) { this.mempool = null; this.pool = null; this.miner = null; - this.walletdb = null; - this.wallet = null; this.http = null; this.client = null; @@ -90,15 +87,15 @@ Node.prototype.init = function init() { var self = this; this.initOptions(); - this.loadPlugins(); - - // Local client for walletdb - this.client = new NodeClient(this); this.hook('preopen', function() { return self.handlePreopen(); }); + this.hook('preopen', function() { + return self.handlePreclose(); + }); + this.hook('open', function() { return self.handleOpen(); }); @@ -117,6 +114,7 @@ Node.prototype.handlePreopen = co(function* handlePreopen() { var self = this; yield fs.mkdirp(this.config.prefix); + yield this.logger.open(); this.bind(this.network.time, 'offset', function(offset) { @@ -173,8 +171,14 @@ Node.prototype.handleOpen = co(function* handleOpen() { this.logger.warning('Warning: worker pool is disabled.'); this.logger.warning('Verification will be slow.'); } +}); - yield this.openPlugins(); +/** + * Open node. Bind all events. + * @private + */ + +Node.prototype.handlePreclose = co(function* handlePreclose() { }); /** @@ -185,8 +189,6 @@ Node.prototype.handleOpen = co(function* handleOpen() { Node.prototype.handleClose = co(function* handleClose() { var i, bound; - yield this.closePlugins(); - this.startTime = -1; for (i = 0; i < this.bound.length; i++) { @@ -264,36 +266,6 @@ Node.prototype.uptime = function uptime() { return util.now() - this.startTime; }; -/** - * Open and ensure primary wallet. - * @returns {Promise} - */ - -Node.prototype.openWallet = co(function* openWallet() { - var options, wallet; - - assert(!this.wallet); - - options = { - id: 'primary', - passphrase: this.config.str('passphrase') - }; - - wallet = yield this.walletdb.ensure(options); - - this.logger.info( - 'Loaded wallet with id=%s wid=%d address=%s', - wallet.id, wallet.wid, wallet.getAddress()); - - if (this.miner && this.miner.addresses.length === 0) - this.miner.addAddress(wallet.getAddress()); - - this.wallet = wallet; - - if (this.http && this.http.rpc && !this.http.rpc.wallet) - this.http.rpc.wallet = wallet; -}); - /** * Attach a plugin. * @param {Object} plugin @@ -312,25 +284,24 @@ Node.prototype.use = function use(plugin) { assert(typeof instance.open === 'function', '`open` must be a function.'); assert(typeof instance.close === 'function', '`close` must be a function.'); - if (plugin.name) { - assert(typeof plugin.name === 'string', '`name` must be a string.'); + if (plugin.id) { + assert(typeof plugin.id === 'string', '`name` must be a string.'); // Reserved names - switch (plugin.name) { + switch (plugin.id) { case 'logger': case 'chain': case 'fees': case 'mempool': case 'miner': case 'pool': - case 'walletdb': - assert(false, plugin.name + ' is already added.'); + assert(false, plugin.id + ' is already added.'); break; } - assert(!this.plugins[plugin.name], plugin.name + ' is already added.'); + assert(!this.plugins[plugin.id], plugin.id + ' is already added.'); - this.plugins[plugin.name] = instance; + this.plugins[plugin.id] = instance; } this.stack.push(instance); @@ -366,9 +337,6 @@ Node.prototype.require = function require(name) { case 'pool': assert(this.pool, 'pool is not loaded.'); return this.pool; - case 'walletdb': - assert(this.walletdb, 'walletdb is not loaded.'); - return this.walletdb; } plugin = this.plugins[name]; @@ -392,9 +360,16 @@ Node.prototype.loadPlugins = function loadPlugins() { for (i = 0; i < plugins.length; i++) { name = plugins[i]; + assert(typeof name === 'string', 'Plugin name must be a string.'); + + // Temporary until we separate walletdb out. + if (name === 'walletdb') + name = __dirname + '/../wallet/walletdb'; + plugin = loader(name); + this.use(plugin); } }; diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 4652280e..2d2c552f 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -13,12 +13,11 @@ var Lock = require('../utils/lock'); var Node = require('./node'); var Chain = require('../blockchain/chain'); var Pool = require('../net/pool'); -var WalletDB = require('../wallet/walletdb'); var HTTPServer = require('../http/server'); /** * Create an spv node which only maintains - * a chain, a pool, and a wallet database. + * a chain, a pool, and an http server. * @alias module:node.SPVNode * @extends Node * @constructor @@ -27,11 +26,9 @@ var HTTPServer = require('../http/server'); * @param {Buffer?} options.sslCert * @param {Number?} options.httpPort * @param {String?} options.httpHost - * @param {Object?} options.wallet - Primary {@link Wallet} options. * @property {Boolean} loaded * @property {Chain} chain * @property {Pool} pool - * @property {WalletDB} walletdb * @property {HTTPServer} http * @emits SPVNode#block * @emits SPVNode#tx @@ -74,22 +71,6 @@ function SPVNode(options) { listen: false }); - this.walletdb = new WalletDB({ - network: this.network, - logger: this.logger, - client: this.client, - db: this.config.str('db'), - prefix: this.config.prefix, - maxFiles: this.config.num('max-files'), - cacheSize: this.config.mb('cache-size'), - witness: false, - checkpoints: this.config.bool('checkpoints'), - startHeight: this.config.num('start-height'), - wipeNoReally: this.config.bool('wipe-no-really'), - verify: true, - spv: true - }); - if (!HTTPServer.unsupported) { this.http = new HTTPServer({ network: this.network, @@ -129,7 +110,6 @@ SPVNode.prototype._init = function _init() { // Bind to errors this.chain.on('error', onError); this.pool.on('error', onError); - this.walletdb.on('error', onError); if (this.http) this.http.on('error', onError); @@ -165,6 +145,8 @@ SPVNode.prototype._init = function _init() { this.chain.on('reset', function(tip) { self.emit('reset', tip); }); + + this.loadPlugins(); }; /** @@ -177,10 +159,8 @@ SPVNode.prototype._init = function _init() { SPVNode.prototype._open = co(function* open(callback) { yield this.chain.open(); yield this.pool.open(); - yield this.walletdb.open(); - // Ensure primary wallet. - yield this.openWallet(); + yield this.openPlugins(); if (this.http) yield this.http.open(); @@ -198,11 +178,8 @@ SPVNode.prototype._close = co(function* close() { if (this.http) yield this.http.close(); - yield this.wallet.destroy(); + yield this.closePlugins(); - this.wallet = null; - - yield this.walletdb.close(); yield this.pool.close(); yield this.chain.close(); }); diff --git a/lib/utils/index.js b/lib/utils/index.js index c1d28d6f..a15c4d93 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -31,3 +31,4 @@ exports.BufferReader = require('./reader'); exports.StaticWriter = require('./staticwriter'); exports.util = require('./util'); exports.BufferWriter = require('./writer'); +exports.Validator = require('./validator'); diff --git a/lib/utils/util.js b/lib/utils/util.js index 350cacd3..16607dc5 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -1037,3 +1037,30 @@ util.promisify = function promisify(func) { return Promise.resolve(result); }; }; + +/** + * Get memory usage info. + * @returns {Object} + */ + +util.memoryUsage = function memoryUsage() { + var mem; + + if (!process.memoryUsage) { + return { + total: 0, + jsHeap: 0, + jsHeapTotal: 0, + nativeHeap: 0 + }; + } + + mem = process.memoryUsage(); + + return { + total: util.mb(mem.rss), + jsHeap: util.mb(mem.heapUsed), + jsHeapTotal: util.mb(mem.heapTotal), + nativeHeap: util.mb(mem.rss - mem.heapTotal) + }; +}; diff --git a/lib/utils/validator.js b/lib/utils/validator.js new file mode 100644 index 00000000..0ecd3b32 --- /dev/null +++ b/lib/utils/validator.js @@ -0,0 +1,428 @@ +'use strict'; + +var assert = require('assert'); + +/** + * Validator + * @alias module:utils.Validator + * @constructor + * @param {Object} options + */ + +function Validator(data) { + if (!(this instanceof Validator)) + return new Validator(data); + + this.data = []; + + if (data) + this.init(data); +} + +/** + * Test whether a config option is present. + * @param {String} key + * @returns {Boolean} + */ + +Validator.prototype.init = function init(data) { + var i, obj; + + assert(data && typeof data === 'object'); + + if (!Array.isArray(data)) + data = [data]; + + for (i = 0; i < data.length; i++) { + obj = data[i]; + assert(obj && typeof obj === 'object'); + this.data.push(obj); + } +}; + +/** + * Test whether a config option is present. + * @param {String} key + * @returns {Boolean} + */ + +Validator.prototype.has = function has(key) { + var i, map, value; + + assert(typeof key === 'string' || typeof key === 'number', + 'Key must be a string.'); + + for (i = 0; i < this.data.length; i++) { + map = this.data[i]; + value = map[key]; + if (value != null) + return true; + } + + return false; +}; + +/** + * Get a config option. + * @param {String} key + * @param {Object?} fallback + * @returns {Object|null} + */ + +Validator.prototype.get = function get(key, fallback) { + var i, keys, value, map; + + if (fallback === undefined) + fallback = null; + + if (Array.isArray(key)) { + keys = key; + for (i = 0; i < keys.length; i++) { + key = keys[i]; + value = this.get(key); + if (value !== null) + return value; + } + return fallback; + } + + assert(typeof key === 'string' || typeof key === 'number', + 'Key must be a string.'); + + for (i = 0; i < this.data.length; i++) { + map = this.data[i]; + value = map[key]; + if (value != null) + return value; + } + + return fallback; +}; + +/** + * Get a config option (as a string). + * @param {String} key + * @param {Object?} fallback + * @returns {String|null} + */ + +Validator.prototype.str = function str(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') + throw new Error(key + ' must be a string.'); + + return value; +}; + +/** + * Get a config option (as a number). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + +Validator.prototype.num = function num(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'number') + throw new Error(key + ' must be a number.'); + return value; + } + + if (!/^\d+$/.test(value)) + throw new Error(key + ' must be a number.'); + + value = parseInt(value, 10); + + if (!isFinite(value)) + throw new Error(key + ' must be a number.'); + + return value; +}; + +/** + * Get a config option (as a number). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + +Validator.prototype.amt = function amt(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'number') + throw new Error(key + ' must be a number.'); + return value; + } + + if (!/^\d+(\.\d{0,8})?$/.test(value)) + throw new Error(key + ' must be a number.'); + + value = parseFloat(value); + + if (!isFinite(value)) + throw new Error(key + ' must be a number.'); + + return value * 1e8; +}; + +/** + * Get a config option (as a number). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + +Validator.prototype.hash = function hash(key, fallback) { + var value = this.get(key); + var out = ''; + var i; + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (!Buffer.isBuffer(value)) + throw new Error(key + ' must be a buffer.'); + if (value.length !== 32) + throw new Error(key + ' must be a buffer.'); + return value.toString('hex'); + } + + if (value.length !== 64) + throw new Error(key + ' must be a hex string.'); + + if (!/^[0-9a-f]+$/i.test(value)) + throw new Error(key + ' must be a hex string.'); + + for (i = 0; i < value.length; i += 2) + out = value.slice(i, i + 2) + out; + + return out; +}; + +/** + * Get a config option (as a number). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + +Validator.prototype.numstr = function numstr(key, fallback) { + var value = this.get(key); + var num; + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'number') + throw new Error(key + ' must be a number or string.'); + return value; + } + + num = parseInt(value, 10); + + if (!isFinite(num)) + return value; + + return num; +}; + +/** + * Get a config option (as a boolean). + * @param {String} key + * @param {Object?} fallback + * @returns {Boolean|null} + */ + +Validator.prototype.bool = function bool(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + assert(typeof value === 'boolean', + 'Passed in config option is of wrong type.'); + return value; + } + + if (value === 'true' || value === '1') + return true; + + if (value === 'false' || value === '0') + return false; + + throw new Error(key + ' must be a boolean.'); +}; + +/** + * Get a config option (as a buffer). + * @param {String} key + * @param {Object?} fallback + * @returns {Buffer|null} + */ + +Validator.prototype.buf = function buf(key, fallback) { + var value = this.get(key); + var data; + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + assert(Buffer.isBuffer(value), + 'Passed in config option is of wrong type.'); + return value; + } + + data = new Buffer(value, 'hex'); + + if (data.length !== value.length / 2) + throw new Error(key + ' must be a hex string.'); + + return data; +}; + +/** + * Get a config option (as an array of strings). + * @param {String} key + * @param {Object?} fallback + * @returns {String[]|null} + */ + +Validator.prototype.array = function array(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (!Array.isArray(value)) + throw new Error(key + ' must be a list/array.'); + return value; + } + + return value.trim().split(/\s*,\s*/); +}; + +/** + * Get a config option (as an object). + * @param {String} key + * @param {Object?} fallback + * @returns {Object|null} + */ + +Validator.prototype.obj = function obj(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (!value || typeof value !== 'object') + throw new Error(key + ' must be an object.'); + return value; + } + + try { + value = JSON.parse(value); + } catch (e) { + ; + } + + if (!value || typeof value !== 'object') + throw new Error(key + ' must be an object.'); + + return value; +}; + +/** + * Get a config option (as an object). + * @param {String} key + * @param {Object?} fallback + * @returns {Object|null} + */ + +Validator.prototype.next = function next(key, fallback) { + var value = this.obj(key, fallback); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + return new Validator(value); +}; + +/** + * Get a config option (as a function). + * @param {String} key + * @param {Object?} fallback + * @returns {Function|null} + */ + +Validator.prototype.func = function func(key, fallback) { + var value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'function') + throw new Error(key + ' must be a function.'); + return value; + } + + throw new Error(key + ' must be a function.'); +}; + +/* + * Expose + */ + +module.exports = Validator; diff --git a/lib/wallet/common.js b/lib/wallet/common.js index b439d283..ba85dd92 100644 --- a/lib/wallet/common.js +++ b/lib/wallet/common.js @@ -23,6 +23,9 @@ common.isName = function isName(key) { if (typeof key !== 'string') return false; + if (key === '_admin') + return false; + if (key === '__proto__') return false; diff --git a/lib/wallet/http.js b/lib/wallet/http.js new file mode 100644 index 00000000..13ff7202 --- /dev/null +++ b/lib/wallet/http.js @@ -0,0 +1,1087 @@ +/*! + * server.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'; + +var assert = require('assert'); +var HTTPBase = require('../http/base'); +var util = require('../utils/util'); +var co = require('../utils/co'); +var base58 = require('../utils/base58'); +var TX = require('../primitives/tx'); +var Outpoint = require('../primitives/outpoint'); +var Script = require('../script/script'); +var crypto = require('../crypto/crypto'); +var Network = require('../protocol/network'); +var Validator = require('../utils/validator'); +var RPC = require('./rpc'); + +/** + * HTTPServer + * @alias module:http.Server + * @constructor + * @param {Object} options + * @see HTTPBase + * @emits HTTPServer#websocket + */ + +function HTTPServer(options) { + if (!(this instanceof HTTPServer)) + return new HTTPServer(options); + + options = new HTTPOptions(options); + + HTTPBase.call(this, options); + + this.options = options; + this.network = this.options.network; + this.logger = this.options.logger; + this.walletdb = this.options.walletdb; + + this.server = new HTTPBase(this.options); + this.rpc = new RPC(this.walletdb); + + this.init(); +} + +util.inherits(HTTPServer, HTTPBase); + +/** + * Attach to server. + * @private + * @param {HTTPServer} server + */ + +HTTPServer.prototype.attach = function attach(server) { + server.mount('/wallet', this); +}; + +/** + * Initialize routes. + * @private + */ + +HTTPServer.prototype.init = function init() { + var self = this; + + this.on('request', function(req, res) { + if (req.method === 'POST' && req.pathname === '/') + return; + + self.logger.debug('Request for method=%s path=%s (%s).', + req.method, req.pathname, req.socket.remoteAddress); + }); + + this.on('listening', function(address) { + self.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({ + username: 'walletrpc', + password: this.options.apiKey, + realm: 'wallet' + })); + } + + this.use(this.bodyParser({ + contentType: 'json' + })); + + this.hook(co(function* (req, res) { + var valid = req.valid(); + var id, token, wallet; + + if (req.path.length === 0) + return; + + if (req.path[0] === '_admin') + return; + + if (req.method === 'PUT' && req.path.length === 1) + return; + + id = valid.str('id'); + token = valid.buf('token'); + + if (!this.options.walletAuth) { + wallet = yield this.walletdb.get(id); + + if (!wallet) { + res.send(404); + return; + } + + req.wallet = wallet; + + return; + } + + try { + wallet = yield this.walletdb.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); + })); + + // JSON RPC + this.post('/', co(function* (req, res) { + var json = yield this.rpc.call(req.body, req.query); + + json = JSON.stringify(json); + json += '\n'; + + res.send(200, json, 'json'); + })); + + // Rescan + this.post('/_admin/rescan', co(function* (req, res) { + var valid = req.valid(); + var height = valid.num('height'); + + res.send(200, { success: true }); + + yield this.walletdb.rescan(height); + })); + + // Resend + this.post('/_admin/resend', co(function* (req, res) { + yield this.walletdb.resend(); + res.send(200, { success: true }); + })); + + // Backup WalletDB + this.post('/_admin/backup', co(function* (req, res) { + var valid = req.valid(); + var path = valid.str('path'); + + enforce(path, 'Path is required.'); + + yield this.walletdb.backup(path); + + res.send(200, { success: true }); + })); + + // List wallets + this.get('/_admin/wallets', co(function* (req, res) { + var wallets = yield this.walletdb.getWallets(); + res.send(200, wallets); + })); + + // Get wallet + this.get('/:id', function(req, res) { + res.send(200, req.wallet.toJSON()); + }); + + // Get wallet master key + this.get('/:id/master', function(req, res) { + if (!req.admin) { + res.send(403, { error: 'Admin access required.' }); + return; + } + + res.send(200, req.wallet.master.toJSON(true)); + }); + + // Create wallet + this.put('/:id', co(function* (req, res) { + var valid = req.valid(); + var wallet; + + wallet = yield this.walletdb.create({ + id: valid.str('id'), + type: valid.str('type'), + m: valid.num('m'), + n: valid.num('n'), + passphrase: valid.str('passphrase'), + master: valid.str('master'), + mnemonic: valid.str('mnemonic'), + accountKey: valid.str('accountKey'), + watchOnly: valid.bool('watchOnly') + }); + + res.send(200, wallet.toJSON()); + })); + + // List accounts + this.get('/:id/account', co(function* (req, res) { + var accounts = yield req.wallet.getAccounts(); + res.send(200, accounts); + })); + + // Get account + this.get('/:id/account/:account', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var account = yield req.wallet.getAccount(acct); + + if (!account) { + res.send(404); + return; + } + + res.send(200, account.toJSON()); + })); + + // Create account + this.put('/:id/account/:account', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var options, account; + + options = { + name: valid.str('account'), + witness: valid.bool('witness'), + watchOnly: valid.bool('watchOnly'), + type: valid.str('type'), + m: valid.num('m'), + n: valid.num('n'), + lookahead: valid.num('lookahead') + }; + + account = yield req.wallet.createAccount(options, passphrase); + + if (!account) { + res.send(404); + return; + } + + res.send(200, account.toJSON()); + })); + + // Change passphrase + this.post('/:id/passphrase', co(function* (req, res) { + var valid = req.valid(); + var old = valid.str('old'); + var new_ = valid.str('new'); + enforce(old || new_, 'Passphrase is required.'); + yield req.wallet.setPassphrase(old, new_); + res.send(200, { success: true }); + })); + + // Unlock wallet + this.post('/:id/unlock', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var timeout = valid.num('timeout'); + enforce(passphrase, 'Passphrase is required.'); + yield req.wallet.unlock(passphrase, timeout); + res.send(200, { success: true }); + })); + + // Lock wallet + this.post('/:id/lock', co(function* (req, res) { + yield req.wallet.lock(); + res.send(200, { success: true }); + })); + + // Import key + this.post('/:id/import', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var pub = valid.str('publicKey'); + var priv = valid.str('privateKey'); + var address = valid.str('address'); + + if (pub) { + yield req.wallet.importKey(acct, pub); + res.send(200, { success: true }); + return; + } + + if (priv) { + yield req.wallet.importKey(acct, priv); + res.send(200, { success: true }); + return; + } + + if (address) { + yield req.wallet.importAddress(acct, address); + res.send(200, { success: true }); + return; + } + + enforce(false, 'Key or address is required.'); + })); + + // Generate new token + this.post('/:id/retoken', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var token = yield req.wallet.retoken(passphrase); + res.send(200, { token: token.toString('hex') }); + })); + + // Send TX + this.post('/:id/send', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var outputs = valid.array('outputs'); + var i, options, tx, details, output, script; + + options = { + rate: valid.amt('rate'), + blocks: valid.num('blocks'), + maxFee: valid.amt('maxFee'), + selection: valid.str('selection'), + smart: valid.bool('smart'), + subtractFee: valid.bool('subtractFee'), + depth: valid.num(['confirmations', 'depth']), + outputs: [] + }; + + for (i = 0; i < outputs.length; i++) { + output = outputs[i]; + valid = new Validator(output); + script = null; + + if (valid.has('script')) { + script = valid.buf('script'); + script = Script.fromRaw(script); + } + + options.outputs.push({ + script: script, + address: valid.str('address'), + value: valid.amt('value') + }); + } + + tx = yield req.wallet.send(options, passphrase); + + details = yield req.wallet.getDetails(tx.hash('hex')); + + res.send(200, details.toJSON()); + })); + + // Create TX + this.post('/:id/create', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var outputs = valid.array('outputs'); + var i, options, tx, output, script; + + options = { + rate: valid.amt('rate'), + maxFee: valid.amt('maxFee'), + selection: valid.str('selection'), + smart: valid.bool('smart'), + subtractFee: valid.bool('subtractFee'), + depth: valid.num(['confirmations', 'depth']), + outputs: [] + }; + + for (i = 0; i < outputs.length; i++) { + output = outputs[i]; + valid = new Validator(output); + script = null; + + if (valid.has('script')) { + script = valid.buf('script'); + script = Script.fromRaw(script); + } + + options.outputs.push({ + script: script, + address: valid.str('address'), + value: valid.amt('value') + }); + } + + tx = yield req.wallet.createTX(options); + yield req.wallet.sign(tx, passphrase); + res.send(200, tx.getJSON(this.network)); + })); + + // Sign TX + this.post('/:id/sign', co(function* (req, res) { + var valid = req.valid(); + var passphrase = valid.str('passphrase'); + var raw = valid.buf('tx'); + var tx; + + enforce(raw, 'TX is required.'); + + tx = TX.fromRaw(raw); + + yield req.wallet.sign(tx, passphrase); + + res.send(200, tx.getJSON(this.network)); + })); + + // Zap Wallet TXs + this.post('/:id/zap', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var age = valid.num('age'); + enforce(age, 'Age is required.'); + yield req.wallet.zap(acct, age); + res.send(200, { success: true }); + })); + + // Abandon Wallet TX + this.del('/:id/tx/:hash', co(function* (req, res) { + var valid = req.valid(); + var hash = valid.hash('hash'); + enforce(hash, 'Hash is required.'); + yield req.wallet.abandon(hash); + res.send(200, { success: true }); + })); + + // List blocks + this.get('/:id/block', co(function* (req, res) { + var heights = yield req.wallet.getBlocks(); + res.send(200, heights); + })); + + // Get Block Record + this.get('/:id/block/:height', co(function* (req, res) { + var valid = req.valid(); + var height = valid.num('height'); + var block; + + enforce(height != null, 'Height is required.'); + + block = yield req.wallet.getBlock(height); + + if (!block) { + res.send(404); + return; + } + + res.send(200, block.toJSON()); + })); + + // Add key + this.put('/:id/shared-key', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var key = valid.str('accountKey'); + enforce(key, 'Key is required.'); + yield req.wallet.addSharedKey(acct, key); + res.send(200, { success: true }); + })); + + // Remove key + this.del('/:id/shared-key', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var key = valid.str('accountKey'); + enforce(key, 'Key is required.'); + yield req.wallet.removeSharedKey(acct, key); + res.send(200, { success: true }); + })); + + // Get key by address + this.get('/:id/key/:address', co(function* (req, res) { + var valid = req.valid(); + var address = valid.str('address'); + var key; + + enforce(address, 'Address is required.'); + + key = yield req.wallet.getKey(address); + + if (!key) { + res.send(404); + return; + } + + res.send(200, key.toJSON()); + })); + + // Get private key + this.get('/:id/wif/:address', co(function* (req, res) { + var valid = req.valid(); + var address = valid.str('address'); + var passphrase = valid.str('passphrase'); + var key; + + enforce(address, 'Address is required.'); + + key = yield req.wallet.getPrivateKey(address, passphrase); + + if (!key) { + res.send(404); + return; + } + + res.send(200, { privateKey: key.toSecret() }); + })); + + // Create address + this.post('/:id/address', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var address = yield req.wallet.createReceive(acct); + res.send(200, address.toJSON()); + })); + + // Create change address + this.post('/:id/change', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var address = yield req.wallet.createChange(acct); + res.send(200, address.toJSON()); + })); + + // Create nested address + this.post('/:id/nested', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var address = yield req.wallet.createNested(acct); + res.send(200, address.toJSON()); + })); + + // Wallet Balance + this.get('/:id/balance', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var balance = yield req.wallet.getBalance(acct); + + if (!balance) { + res.send(404); + return; + } + + res.send(200, balance.toJSON()); + })); + + // Wallet UTXOs + this.get('/:id/coin', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var coins = yield req.wallet.getCoins(acct); + var result = []; + var i, coin; + + sortCoins(coins); + + for (i = 0; i < coins.length; i++) { + coin = coins[i]; + result.push(coin.getJSON(this.network)); + } + + res.send(200, result); + })); + + // Locked coins + this.get('/:id/locked', co(function* (req, res) { + var locked = this.wallet.getLocked(); + var result = []; + var i, outpoint; + + for (i = 0; i < locked.length; i++) { + outpoint = locked[i]; + result.push(outpoint.toJSON()); + } + + res.send(200, result); + })); + + // Lock coin + this.put('/:id/locked/:hash/:index', co(function* (req, res) { + var valid = req.valid(); + var hash = valid.hash('hash'); + var index = valid.num('index'); + var outpoint; + + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); + + outpoint = new Outpoint(hash, index); + + this.wallet.lockCoin(outpoint); + })); + + // Unlock coin + this.del('/:id/locked/:hash/:index', co(function* (req, res) { + var valid = req.valid(); + var hash = valid.hash('hash'); + var index = valid.num('index'); + var outpoint; + + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); + + outpoint = new Outpoint(hash, index); + + this.wallet.unlockCoin(outpoint); + })); + + // Wallet Coin + this.get('/:id/coin/:hash/:index', co(function* (req, res) { + var valid = req.valid(); + var hash = valid.hash('hash'); + var index = valid.num('index'); + var coin; + + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); + + coin = yield 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', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var txs = yield req.wallet.getHistory(acct); + var result = []; + var i, details, item; + + sortTX(txs); + + details = yield req.wallet.toDetails(txs); + + for (i = 0; i < details.length; i++) { + item = details[i]; + result.push(item.toJSON()); + } + + res.send(200, result); + })); + + // Wallet Pending TXs + this.get('/:id/tx/unconfirmed', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var txs = yield req.wallet.getPending(acct); + var result = []; + var i, details, item; + + sortTX(txs); + + details = yield req.wallet.toDetails(txs); + + for (i = 0; i < details.length; i++) { + item = details[i]; + result.push(item.toJSON()); + } + + res.send(200, result); + })); + + // Wallet TXs within time range + this.get('/:id/tx/range', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var result = []; + var i, options, txs, details, item; + + options = { + start: valid.num('start'), + end: valid.num('end'), + limit: valid.num('limit'), + reverse: valid.bool('reverse') + }; + + txs = yield req.wallet.getRange(acct, options); + + details = yield req.wallet.toDetails(txs); + + for (i = 0; i < details.length; i++) { + item = details[i]; + result.push(item.toJSON()); + } + + res.send(200, result); + })); + + // Last Wallet TXs + this.get('/:id/tx/last', co(function* (req, res) { + var valid = req.valid(); + var acct = valid.str('account'); + var limit = valid.num('limit'); + var txs = yield req.wallet.getLast(acct, limit); + var details = yield req.wallet.toDetails(txs); + var result = []; + var i, item; + + for (i = 0; i < details.length; i++) { + item = details[i]; + result.push(item.toJSON()); + } + + res.send(200, result); + })); + + // Wallet TX + this.get('/:id/tx/:hash', co(function* (req, res) { + var valid = req.valid(); + var hash = valid.hash('hash'); + var tx, details; + + enforce(hash, 'Hash is required.'); + + tx = yield req.wallet.getTX(hash); + + if (!tx) { + res.send(404); + return; + } + + details = yield req.wallet.toDetails(tx); + + res.send(200, details.toJSON()); + })); + + // Resend + this.post('/:id/resend', co(function* (req, res) { + yield req.wallet.resend(); + res.send(200, { success: true }); + })); + + this.initSockets(); +}; + +/** + * Initialize websockets. + * @private + */ + +HTTPServer.prototype.initSockets = function initSockets() { + var self = this; + + if (!this.io) + return; + + this.on('socket', function(socket) { + self.handleSocket(socket); + }); + + this.walletdb.on('tx', function(id, tx, details) { + var json = details.toJSON(); + self.to(id, 'wallet tx', json); + self.to('!all', 'wallet tx', id, json); + }); + + this.walletdb.on('confirmed', function(id, tx, details) { + var json = details.toJSON(); + self.to(id, 'wallet confirmed', json); + self.to('!all', 'wallet confirmed', id, json); + }); + + this.walletdb.on('unconfirmed', function(id, tx, details) { + var json = details.toJSON(); + self.to(id, 'wallet unconfirmed', json); + self.to('!all', 'wallet unconfirmed', id, json); + }); + + this.walletdb.on('conflict', function(id, tx, details) { + var json = details.toJSON(); + self.to(id, 'wallet conflict', json); + self.to('!all', 'wallet conflict', id, json); + }); + + this.walletdb.on('balance', function(id, balance) { + var json = balance.toJSON(); + self.to(id, 'wallet balance', json); + self.to('!all', 'wallet balance', id, json); + }); + + this.walletdb.on('address', function(id, receive) { + var json = []; + var i, address; + + for (i = 0; i < receive.length; i++) { + address = receive[i]; + json.push(address.toJSON()); + } + + self.to(id, 'wallet address', json); + self.to('!all', 'wallet address', id, json); + }); +}; + +/** + * Handle new websocket. + * @private + * @param {WebSocket} socket + */ + +HTTPServer.prototype.handleSocket = function handleSocket(socket) { + var self = this; + + socket.hook('wallet auth', function(args) { + var valid = new Validator([args]); + var key = valid.str(0); + var hash; + + if (socket.auth) + throw new Error('Already authed.'); + + if (!self.options.noAuth) { + hash = hash256(key); + if (!crypto.ccmp(hash, self.options.apiHash)) + throw new Error('Bad key.'); + } + + socket.auth = true; + + self.logger.info('Successful auth from %s.', socket.host); + + self.handleAuth(socket); + + return null; + }); +}; + +/** + * Handle new auth'd websocket. + * @private + * @param {WebSocket} socket + */ + +HTTPServer.prototype.handleAuth = function handleAuth(socket) { + var self = this; + + socket.hook('wallet join', co(function* (args) { + var valid = new Validator([args]); + var id = valid.str(0); + var token = valid.buf(1); + var wallet; + + if (!id) + throw new Error('Invalid parameter.'); + + if (!self.options.walletAuth) { + socket.join(id); + return; + } + + if (!token) + throw new Error('Invalid parameter.'); + + try { + wallet = yield self.walletdb.auth(id, token); + } catch (e) { + self.logger.info('Wallet auth failure for %s: %s.', id, e.message); + throw new Error('Bad token.'); + } + + if (!wallet) + throw new Error('Wallet does not exist.'); + + self.logger.info('Successful wallet auth for %s.', id); + + socket.join(id); + + return null; + })); + + socket.hook('wallet leave', function(args) { + var valid = new Validator([args]); + var id = valid.str(0); + + if (!id) + throw new Error('Invalid parameter.'); + + socket.leave(id); + + return null; + }); +}; + +/** + * 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.walletdb = null; + this.apiKey = base58.encode(crypto.randomBytes(20)); + this.apiHash = hash256(this.apiKey); + 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); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {HTTPOptions} + */ + +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 <= 200, + 'API key must be under 200 bytes.'); + this.apiKey = options.apiKey; + this.apiHash = hash256(this.apiKey); + } + + 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 = this.prefix + '/key.pem'; + this.certFile = this.prefix + '/cert.pem'; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(typeof options.port === 'number', 'Port must be a number.'); + assert(options.port > 0 && options.port <= 0xffff); + 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 + */ + +function hash256(data) { + if (typeof data !== 'string') + return new Buffer(0); + + if (data.length > 200) + return new Buffer(0); + + return crypto.hash256(new Buffer(data, 'utf8')); +} + +function enforce(value, msg) { + var err; + + if (!value) { + err = new Error(msg); + err.statusCode = 400; + throw err; + } +} + +function sortTX(txs) { + return txs.sort(function(a, b) { + return a.ps - b.ps; + }); +} + +function sortCoins(coins) { + return coins.sort(function(a, b) { + a = a.height === -1 ? 0x7fffffff : a.height; + b = b.height === -1 ? 0x7fffffff : b.height; + return a - b; + }); +} + +/* + * Expose + */ + +module.exports = HTTPServer; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js new file mode 100644 index 00000000..77385ffe --- /dev/null +++ b/lib/wallet/rpc.js @@ -0,0 +1,1847 @@ +/*! + * rpc.js - bitcoind-compatible json rpc for bcoin. + * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var fs = require('../utils/fs'); +var util = require('../utils/util'); +var co = require('../utils/co'); +var crypto = require('../crypto/crypto'); +var Amount = require('../btc/amount'); +var Script = require('../script/script'); +var Address = require('../primitives/address'); +var KeyRing = require('../primitives/keyring'); +var Lock = require('../utils/lock'); +var MerkleBlock = require('../primitives/merkleblock'); +var MTX = require('../primitives/mtx'); +var Outpoint = require('../primitives/outpoint'); +var Output = require('../primitives/output'); +var TX = require('../primitives/tx'); +var encoding = require('../utils/encoding'); +var consensus = require('../protocol/consensus'); +var pkg = require('../pkg'); + +/** + * Bitcoin Core RPC + * @alias module:wallet.RPC + * @constructor + * @param {Node} node + */ + +function RPC(walletdb) { + if (!(this instanceof RPC)) + return new RPC(walletdb); + + assert(walletdb, 'RPC requires a WalletDB.'); + + this.network = walletdb.network; + this.logger = walletdb.logger; + this.walletdb = walletdb; + this.wallet = null; + + this.locker = new Lock(); + + this.feeRate = null; +} + +RPC.magic = 'Bitcoin Signed Message:\n'; + +RPC.prototype.attach = function attach(rpc) { + rpc.add('fundrawtransaction', this.fundrawtransaction, this); + rpc.add('resendwallettransactions', this.resendwallettransactions, this); + rpc.add('abandontransaction', this.abandontransaction, this); + rpc.add('addmultisigaddress', this.addmultisigaddress, this); + rpc.add('addwitnessaddress', this.addwitnessaddress, this); + rpc.add('backupwallet', this.backupwallet, this); + rpc.add('dumpprivkey', this.dumpprivkey, this); + rpc.add('dumpwallet', this.dumpwallet, this); + rpc.add('encryptwallet', this.encryptwallet, this); + rpc.add('getaccountaddress', this.getaccountaddress, this); + rpc.add('getaccount', this.getaccount, this); + rpc.add('getaddressesbyaccount', this.getaddressesbyaccount, this); + rpc.add('getbalance', this.getbalance, this); + rpc.add('getnewaddress', this.getnewaddress, this); + rpc.add('getrawchangeaddress', this.getrawchangeaddress, this); + rpc.add('getreceivedbyaccount', this.getreceivedbyaccount, this); + rpc.add('getreceivedbyaddress', this.getreceivedbyaddress, this); + rpc.add('gettransaction', this.gettransaction, this); + rpc.add('getunconfirmedbalance', this.getunconfirmedbalance, this); + rpc.add('getwalletinfo', this.getwalletinfo, this); + rpc.add('importprivkey', this.importprivkey, this); + rpc.add('importwallet', this.importwallet, this); + rpc.add('importaddress', this.importaddress, this); + rpc.add('importprunedfunds', this.importprunedfunds, this); + rpc.add('importpubkey', this.importpubkey, this); + rpc.add('keypoolrefill', this.keypoolrefill, this); + rpc.add('listaccounts', this.listaccounts, this); + rpc.add('listaddressgroupings', this.listaddressgroupings, this); + rpc.add('listlockunspent', this.listlockunspent, this); + rpc.add('listreceivedbyaccount', this.listreceivedbyaccount, this); + rpc.add('listreceivedbyaddress', this.listreceivedbyaddress, this); + rpc.add('listsinceblock', this.listsinceblock, this); + rpc.add('listtransactions', this.listtransactions, this); + rpc.add('listunspent', this.listunspent, this); + rpc.add('lockunspent', this.lockunspent, this); + rpc.add('move', this.move, this); + rpc.add('sendfrom', this.sendfrom, this); + rpc.add('sendmany', this.sendmany, this); + rpc.add('sendtoaddress', this.sendtoaddress, this); + rpc.add('setaccount', this.setaccount, this); + rpc.add('settxfee', this.settxfee, this); + rpc.add('signmessage', this.signmessage, this); + rpc.add('walletlock', this.walletlock, this); + rpc.add('walletpassphrasechange', this.walletpassphrasechange, this); + rpc.add('walletpassphrase', this.walletpassphrase, this); + rpc.add('removeprunedfunds', this.removeprunedfunds, this); +}; + +RPC.prototype.execute = function execute(json, help) { + switch (json.method) { + case 'fundrawtransaction': + return this.fundrawtransaction(json.params, help); + case 'resendwallettransactions': + return this.resendwallettransactions(json.params, help); + case 'abandontransaction': + return this.abandontransaction(json.params, help); + case 'addmultisigaddress': + return this.addmultisigaddress(json.params, help); + case 'addwitnessaddress': + return this.addwitnessaddress(json.params, help); + case 'backupwallet': + return this.backupwallet(json.params, help); + case 'dumpprivkey': + return this.dumpprivkey(json.params, help); + case 'dumpwallet': + return this.dumpwallet(json.params, help); + case 'encryptwallet': + return this.encryptwallet(json.params, help); + case 'getaccountaddress': + return this.getaccountaddress(json.params, help); + case 'getaccount': + return this.getaccount(json.params, help); + case 'getaddressesbyaccount': + return this.getaddressesbyaccount(json.params, help); + case 'getbalance': + return this.getbalance(json.params, help); + case 'getnewaddress': + return this.getnewaddress(json.params, help); + case 'getrawchangeaddress': + return this.getrawchangeaddress(json.params, help); + case 'getreceivedbyaccount': + return this.getreceivedbyaccount(json.params, help); + case 'getreceivedbyaddress': + return this.getreceivedbyaddress(json.params, help); + case 'gettransaction': + return this.gettransaction(json.params, help); + case 'getunconfirmedbalance': + return this.getunconfirmedbalance(json.params, help); + case 'getwalletinfo': + return this.getwalletinfo(json.params, help); + case 'importprivkey': + return this.importprivkey(json.params, help); + case 'importwallet': + return this.importwallet(json.params, help); + case 'importaddress': + return this.importaddress(json.params, help); + case 'importprunedfunds': + return this.importprunedfunds(json.params, help); + case 'importpubkey': + return this.importpubkey(json.params, help); + case 'keypoolrefill': + return this.keypoolrefill(json.params, help); + case 'listaccounts': + return this.listaccounts(json.params, help); + case 'listaddressgroupings': + return this.listaddressgroupings(json.params, help); + case 'listlockunspent': + return this.listlockunspent(json.params, help); + case 'listreceivedbyaccount': + return this.listreceivedbyaccount(json.params, help); + case 'listreceivedbyaddress': + return this.listreceivedbyaddress(json.params, help); + case 'listsinceblock': + return this.listsinceblock(json.params, help); + case 'listtransactions': + return this.listtransactions(json.params, help); + case 'listunspent': + return this.listunspent(json.params, help); + case 'lockunspent': + return this.lockunspent(json.params, help); + case 'move': + return this.move(json.params, help); + case 'sendfrom': + return this.sendfrom(json.params, help); + case 'sendmany': + return this.sendmany(json.params, help); + case 'sendtoaddress': + return this.sendtoaddress(json.params, help); + case 'setaccount': + return this.setaccount(json.params, help); + case 'settxfee': + return this.settxfee(json.params, help); + case 'signmessage': + return this.signmessage(json.params, help); + case 'walletlock': + return this.walletlock(json.params, help); + case 'walletpassphrasechange': + return this.walletpassphrasechange(json.params, help); + case 'walletpassphrase': + return this.walletpassphrase(json.params, help); + case 'removeprunedfunds': + return this.removeprunedfunds(json.params, help); + default: + return Promise.reject(new Error('Unknown RPC call: ' + json.method)); + } +}; + +RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args, help) { + var wallet = this.wallet; + var feeRate = this.feeRate; + var tx, options, changeAddress; + + if (help || args.length < 1 || args.length > 2) + throw new RPCError('fundrawtransaction "hexstring" ( options )'); + + if (!util.isHex(args[0])) + throw new RPCError('Invalid parameter.'); + + tx = MTX.fromRaw(args[0], 'hex'); + + if (tx.outputs.length === 0) + throw new RPCError('TX must have at least one output.'); + + if (args.length > 1) { + options = toObject(args[1]); + changeAddress = toString(options.changeAddress); + + if (changeAddress) + changeAddress = Address.fromBase58(changeAddress, this.network); + + feeRate = options.feeRate; + + if (feeRate != null) + feeRate = toSatoshi(feeRate); + } + + options = { + rate: feeRate, + changeAddress: changeAddress + }; + + yield wallet.fund(tx, options); + + return { + hex: tx.toRaw().toString('hex'), + changepos: tx.changeIndex, + fee: Amount.btc(tx.getFee(), true) + }; +}); + +RPC.prototype._createRedeem = co(function* _createRedeem(args, help) { + var wallet = this.wallet; + var i, m, n, keys, hash, script, key, ring; + + if (!util.isNumber(args[0]) + || !Array.isArray(args[1]) + || args[0] < 1 + || args[1].length < args[0] + || args[1].length > 16) { + throw new RPCError('Invalid parameter.'); + } + + m = args[0]; + n = args[1].length; + keys = args[1]; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + + if (!util.isBase58(key)) { + if (!util.isHex(key)) + throw new RPCError('Invalid key.'); + keys[i] = new Buffer(key, 'hex'); + continue; + } + + hash = Address.getHash(key, 'hex'); + + if (!hash) + throw new RPCError('Invalid key.'); + + ring = yield wallet.getKey(hash); + + if (!ring) + throw new RPCError('Invalid key.'); + + keys[i] = ring.publicKey; + } + + try { + script = Script.fromMultisig(m, n, keys); + } catch (e) { + throw new RPCError('Invalid parameters.'); + } + + if (script.getSize() > consensus.MAX_SCRIPT_PUSH) + throw new RPCError('Redeem script exceeds size limit.'); + + return script; +}); + +RPC.prototype.createmultisig = co(function* createmultisig(args, help) { + var script, address; + + if (help || args.length < 2 || args.length > 2) + throw new RPCError('createmultisig nrequired ["key",...]'); + + script = yield this._createRedeem(args); + address = script.getAddress(); + + return { + address: address.toBase58(this.network), + redeemScript: script.toJSON() + }; +}); + +/* + * Wallet + */ + +RPC.prototype.resendwallettransactions = co(function* resendwallettransactions(args, help) { + var wallet = this.wallet; + var hashes = []; + var i, tx, txs; + + if (help || args.length !== 0) + throw new RPCError('resendwallettransactions'); + + txs = yield wallet.resend(); + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + hashes.push(tx.txid()); + } + + return hashes; +}); + +RPC.prototype.addmultisigaddress = co(function* addmultisigaddress(args, help) { + if (help || args.length < 2 || args.length > 3) { + throw new RPCError('addmultisigaddress' + + ' nrequired ["key",...] ( "account" )'); + } + + // Impossible to implement in bcoin (no address book). + throw new Error('Not implemented.'); +}); + +RPC.prototype.addwitnessaddress = co(function* addwitnessaddress(args, help) { + if (help || args.length < 1 || args.length > 1) + throw new RPCError('addwitnessaddress "address"'); + + // Unlikely to be implemented. + throw new Error('Not implemented.'); +}); + +RPC.prototype.backupwallet = co(function* backupwallet(args, help) { + var dest; + + if (help || args.length !== 1) + throw new RPCError('backupwallet "destination"'); + + dest = toString(args[0]); + + yield this.walletdb.backup(dest); + + return null; +}); + +RPC.prototype.dumpprivkey = co(function* dumpprivkey(args, help) { + var wallet = this.wallet; + var hash, ring; + + if (help || args.length !== 1) + throw new RPCError('dumpprivkey "bitcoinaddress"'); + + hash = Address.getHash(toString(args[0]), 'hex'); + + if (!hash) + throw new RPCError('Invalid address.'); + + ring = yield wallet.getPrivateKey(hash); + + if (!ring) + throw new RPCError('Key not found.'); + + return ring.toSecret(); +}); + +RPC.prototype.dumpwallet = co(function* dumpwallet(args, help) { + var wallet = this.wallet; + var i, file, time, address, fmt, str, out, hash, hashes, ring; + + if (help || args.length !== 1) + throw new RPCError('dumpwallet "filename"'); + + if (!args[0] || typeof args[0] !== 'string') + throw new RPCError('Invalid parameter.'); + + file = toString(args[0]); + time = util.date(); + 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),', + this.chain.height, this.chain.tip.rhash()), + util.fmt('# mined on %s', util.date(this.chain.tip.ts)), + util.fmt('# * File: %s', file), + '' + ]; + + hashes = yield wallet.getAddressHashes(); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + ring = yield wallet.getPrivateKey(hash); + + if (!ring) + continue; + + address = ring.getAddress('base58'); + fmt = '%s %s label= addr=%s'; + + if (ring.branch === 1) + fmt = '%s %s change=1 addr=%s'; + + str = util.fmt(fmt, ring.toSecret(), time, address); + + out.push(str); + } + + out.push(''); + out.push('# End of dump'); + out.push(''); + + out = out.join('\n'); + + if (fs.unsupported) + return out; + + yield fs.writeFile(file, out, 'utf8'); + + return out; +}); + +RPC.prototype.encryptwallet = co(function* encryptwallet(args, help) { + var wallet = this.wallet; + var passphrase; + + if (!wallet.master.encrypted && (help || args.length !== 1)) + throw new RPCError('encryptwallet "passphrase"'); + + if (wallet.master.encrypted) + throw new RPCError('Already running with an encrypted wallet'); + + passphrase = toString(args[0]); + + if (passphrase.length < 1) + throw new RPCError('encryptwallet "passphrase"'); + + yield wallet.setPassphrase(passphrase); + + return 'wallet encrypted; we do not need to stop!'; +}); + +RPC.prototype.getaccountaddress = co(function* getaccountaddress(args, help) { + var wallet = this.wallet; + var account; + + if (help || args.length !== 1) + throw new RPCError('getaccountaddress "account"'); + + account = toString(args[0]); + + if (!account) + account = 'default'; + + account = yield wallet.getAccount(account); + + if (!account) + return ''; + + return account.receive.getAddress('base58'); +}); + +RPC.prototype.getaccount = co(function* getaccount(args, help) { + var wallet = this.wallet; + var hash, path; + + if (help || args.length !== 1) + throw new RPCError('getaccount "bitcoinaddress"'); + + hash = Address.getHash(args[0], 'hex'); + + if (!hash) + throw new RPCError('Invalid address.'); + + path = yield wallet.getPath(hash); + + if (!path) + return ''; + + return path.name; +}); + +RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args, help) { + var wallet = this.wallet; + var i, path, account, address, addrs, paths; + + if (help || args.length !== 1) + throw new RPCError('getaddressesbyaccount "account"'); + + account = toString(args[0]); + + if (!account) + account = 'default'; + + addrs = []; + + paths = yield wallet.getPaths(account); + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + address = path.toAddress(); + addrs.push(address.toBase58(this.network)); + } + + return addrs; +}); + +RPC.prototype.getbalance = co(function* getbalance(args, help) { + var wallet = this.wallet; + var minconf = 0; + var account, value, balance; + + if (help || args.length > 3) + throw new RPCError('getbalance ( "account" minconf includeWatchonly )'); + + if (args.length >= 1) { + account = toString(args[0]); + + if (!account) + account = 'default'; + + if (account === '*') + account = null; + } + + if (args.length >= 2) + minconf = toNumber(args[1], 0); + + balance = yield wallet.getBalance(account); + + if (minconf) + value = balance.confirmed; + else + value = balance.unconfirmed; + + return Amount.btc(value, true); +}); + +RPC.prototype.getnewaddress = co(function* getnewaddress(args, help) { + var wallet = this.wallet; + var account, address; + + if (help || args.length > 1) + throw new RPCError('getnewaddress ( "account" )'); + + if (args.length === 1) + account = toString(args[0]); + + if (!account) + account = 'default'; + + address = yield wallet.createReceive(account); + + return address.getAddress('base58'); +}); + +RPC.prototype.getrawchangeaddress = co(function* getrawchangeaddress(args, help) { + var wallet = this.wallet; + var address; + + if (help || args.length > 1) + throw new RPCError('getrawchangeaddress'); + + address = yield wallet.createChange(); + + return address.getAddress('base58'); +}); + +RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args, help) { + var wallet = this.wallet; + var minconf = 0; + var total = 0; + var filter = {}; + var lastConf = -1; + var i, j, path, wtx, output, conf, hash, account, paths, txs; + + if (help || args.length < 1 || args.length > 2) + throw new RPCError('getreceivedbyaccount "account" ( minconf )'); + + account = toString(args[0]); + + if (!account) + account = 'default'; + + if (args.length === 2) + minconf = toNumber(args[1], 0); + + paths = yield wallet.getPaths(account); + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + filter[path.hash] = true; + } + + txs = yield wallet.getHistory(account); + + for (i = 0; i < txs.length; i++) { + wtx = txs[i]; + + conf = wtx.getDepth(this.chain.height); + + if (conf < minconf) + continue; + + if (lastConf === -1 || conf < lastConf) + lastConf = conf; + + for (j = 0; j < wtx.tx.outputs.length; j++) { + output = wtx.tx.outputs[j]; + hash = output.getHash('hex'); + if (hash && filter[hash]) + total += output.value; + } + } + + return Amount.btc(total, true); +}); + +RPC.prototype.getreceivedbyaddress = co(function* getreceivedbyaddress(args, help) { + var wallet = this.wallet; + var minconf = 0; + var total = 0; + var i, j, hash, wtx, output, txs; + + if (help || args.length < 1 || args.length > 2) + throw new RPCError('getreceivedbyaddress "bitcoinaddress" ( minconf )'); + + hash = Address.getHash(toString(args[0]), 'hex'); + + if (!hash) + throw new RPCError('Invalid address'); + + if (args.length === 2) + minconf = toNumber(args[1], 0); + + txs = yield wallet.getHistory(); + + for (i = 0; i < txs.length; i++) { + wtx = txs[i]; + + if (wtx.getDepth(this.chain.height) < minconf) + continue; + + for (j = 0; j < wtx.tx.outputs.length; j++) { + output = wtx.tx.outputs[j]; + if (output.getHash('hex') === hash) + total += output.value; + } + } + + return Amount.btc(total, true); +}); + +RPC.prototype._toWalletTX = co(function* _toWalletTX(wtx) { + var wallet = this.wallet; + var details = yield wallet.toDetails(wtx); + var det = []; + var sent = 0; + var received = 0; + var receive = true; + var i, member; + + if (!details) + throw new RPCError('TX not found.'); + + for (i = 0; i < details.inputs.length; i++) { + member = details.inputs[i]; + if (member.path) { + receive = false; + break; + } + } + + for (i = 0; i < details.outputs.length; i++) { + member = details.outputs[i]; + + if (member.path) { + if (member.path.branch === 1) + continue; + + det.push({ + account: member.path.name, + address: member.address.toBase58(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: '', + address: member.address + ? member.address.toBase58(this.network) + : null, + category: 'send', + amount: -(Amount.btc(member.value, true)), + fee: -(Amount.btc(details.fee, true)), + vout: i + }); + + sent += member.value; + } + + return { + amount: Amount.btc(receive ? received : -sent, true), + confirmations: details.confirmations, + blockhash: details.block ? util.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.ts, + txid: util.revHex(details.hash), + walletconflicts: [], + time: details.ps, + timereceived: details.ps, + 'bip125-replaceable': 'no', + details: det, + hex: details.tx.toRaw().toString('hex') + }; +}); + +RPC.prototype.gettransaction = co(function* gettransaction(args, help) { + var wallet = this.wallet; + var hash, wtx; + + if (help || args.length < 1 || args.length > 2) + throw new RPCError('gettransaction "txid" ( includeWatchonly )'); + + hash = toHash(args[0]); + + if (!hash) + throw new RPCError('Invalid parameter'); + + wtx = yield wallet.getTX(hash); + + if (!wtx) + throw new RPCError('TX not found.'); + + return yield this._toWalletTX(wtx); +}); + +RPC.prototype.abandontransaction = co(function* abandontransaction(args, help) { + var wallet = this.wallet; + var hash, result; + + if (help || args.length !== 1) + throw new RPCError('abandontransaction "txid"'); + + hash = toHash(args[0]); + + if (!hash) + throw new RPCError('Invalid parameter.'); + + result = yield wallet.abandon(hash); + + if (!result) + throw new RPCError('Transaction not in wallet.'); + + return null; +}); + +RPC.prototype.getunconfirmedbalance = co(function* getunconfirmedbalance(args, help) { + var wallet = this.wallet; + var balance; + + if (help || args.length > 0) + throw new RPCError('getunconfirmedbalance'); + + balance = yield wallet.getBalance(); + + return Amount.btc(balance.unconfirmed, true); +}); + +RPC.prototype.getwalletinfo = co(function* getwalletinfo(args, help) { + var wallet = this.wallet; + var balance; + + if (help || args.length !== 0) + throw new RPCError('getwalletinfo'); + + balance = yield wallet.getBalance(); + + return { + walletid: wallet.id, + walletversion: 6, + balance: Amount.btc(balance.unconfirmed, true), + unconfirmed_balance: Amount.btc(balance.unconfirmed, true), + txcount: wallet.txdb.state.tx, + keypoololdest: 0, + keypoolsize: 0, + unlocked_until: wallet.master.until, + paytxfee: this.feeRate != null + ? Amount.btc(this.feeRate, true) + : 0 + }; +}); + +RPC.prototype.importprivkey = co(function* importprivkey(args, help) { + var wallet = this.wallet; + var secret, label, rescan, key; + + if (help || args.length < 1 || args.length > 3) + throw new RPCError('importprivkey "bitcoinprivkey" ( "label" rescan )'); + + secret = toString(args[0]); + + if (args.length > 1) + label = toString(args[1]); + + if (args.length > 2) + rescan = toBool(args[2]); + + if (rescan && this.chain.options.prune) + throw new RPCError('Cannot rescan when pruned.'); + + key = KeyRing.fromSecret(secret, this.network); + + yield wallet.importKey(0, key); + + if (rescan) + yield this.walletdb.rescan(0); + + return null; +}); + +RPC.prototype.importwallet = co(function* importwallet(args, help) { + var wallet = this.wallet; + var file, keys, lines, line, parts; + var i, secret, time, label, addr; + var data, key, rescan; + + if (help || args.length !== 1) + throw new RPCError('importwallet "filename" ( rescan )'); + + if (fs.unsupported) + throw new RPCError('FS not available.'); + + file = toString(args[0]); + + if (args.length > 1) + rescan = toBool(args[1]); + + if (rescan && this.chain.options.prune) + throw new RPCError('Cannot rescan when pruned.'); + + data = yield fs.readFile(file, 'utf8'); + + lines = data.split(/\n+/); + keys = []; + + for (i = 0; i < lines.length; i++) { + line = lines[i].trim(); + + if (line.length === 0) + continue; + + if (/^\s*#/.test(line)) + continue; + + parts = line.split(/\s+/); + + if (parts.length < 4) + throw new RPCError('Malformed wallet.'); + + secret = KeyRing.fromSecret(parts[0], this.network); + + time = +parts[1]; + label = parts[2]; + addr = parts[3]; + + keys.push(secret); + } + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + yield wallet.importKey(0, key); + } + + if (rescan) + yield this.walletdb.rescan(0); + + return null; +}); + +RPC.prototype.importaddress = co(function* importaddress(args, help) { + var wallet = this.wallet; + var addr, label, rescan, p2sh; + + if (help || args.length < 1 || args.length > 4) + throw new RPCError('importaddress "address" ( "label" rescan p2sh )'); + + addr = toString(args[0]); + + if (args.length > 1) + label = toString(args[1]); + + if (args.length > 2) + rescan = toBool(args[2]); + + if (args.length > 3) + p2sh = toBool(args[3]); + + if (rescan && this.chain.options.prune) + throw new RPCError('Cannot rescan when pruned.'); + + addr = Address.fromBase58(addr, this.network); + + yield wallet.importAddress(0, addr); + + if (rescan) + yield this.walletdb.rescan(0); + + return null; +}); + +RPC.prototype.importpubkey = co(function* importpubkey(args, help) { + var wallet = this.wallet; + var pubkey, label, rescan, key; + + if (help || args.length < 1 || args.length > 4) + throw new RPCError('importpubkey "pubkey" ( "label" rescan )'); + + pubkey = toString(args[0]); + + if (!util.isHex(pubkey)) + throw new RPCError('Invalid parameter.'); + + if (args.length > 1) + label = toString(args[1]); + + if (args.length > 2) + rescan = toBool(args[2]); + + if (rescan && this.chain.options.prune) + throw new RPCError('Cannot rescan when pruned.'); + + pubkey = new Buffer(pubkey, 'hex'); + + key = KeyRing.fromPublic(pubkey, this.network); + + yield wallet.importKey(0, key); + + if (rescan) + yield this.walletdb.rescan(0); + + return null; +}); + +RPC.prototype.keypoolrefill = co(function* keypoolrefill(args, help) { + if (help || args.length > 1) + throw new RPCError('keypoolrefill ( newsize )'); + return null; +}); + +RPC.prototype.listaccounts = co(function* listaccounts(args, help) { + var wallet = this.wallet; + var i, map, accounts, account, balance; + + if (help || args.length > 2) + throw new RPCError('listaccounts ( minconf includeWatchonly)'); + + map = {}; + accounts = yield wallet.getAccounts(); + + for (i = 0; i < accounts.length; i++) { + account = accounts[i]; + balance = yield wallet.getBalance(account); + map[account] = Amount.btc(balance.unconfirmed, true); + } + + return map; +}); + +RPC.prototype.listaddressgroupings = co(function* listaddressgroupings(args, help) { + if (help) + throw new RPCError('listaddressgroupings'); + throw new Error('Not implemented.'); +}); + +RPC.prototype.listlockunspent = co(function* listlockunspent(args, help) { + var wallet = this.wallet; + var i, outpoints, outpoint, out; + + if (help || args.length > 0) + throw new RPCError('listlockunspent'); + + outpoints = wallet.getLocked(); + out = []; + + for (i = 0; i < outpoints.length; i++) { + outpoint = outpoints[i]; + out.push({ + txid: outpoint.txid(), + vout: outpoint.index + }); + } + + return out; +}); + +RPC.prototype.listreceivedbyaccount = co(function* listreceivedbyaccount(args, help) { + var minconf = 0; + var includeEmpty = false; + + if (help || args.length > 3) { + throw new RPCError('listreceivedbyaccount' + + ' ( minconf includeempty includeWatchonly )'); + } + + if (args.length > 0) + minconf = toNumber(args[0], 0); + + if (args.length > 1) + includeEmpty = toBool(args[1], false); + + return yield this._listReceived(minconf, includeEmpty, true); +}); + +RPC.prototype.listreceivedbyaddress = co(function* listreceivedbyaddress(args, help) { + var minconf = 0; + var includeEmpty = false; + + if (help || args.length > 3) { + throw new RPCError('listreceivedbyaddress' + + ' ( minconf includeempty includeWatchonly )'); + } + + if (args.length > 0) + minconf = toNumber(args[0], 0); + + if (args.length > 1) + includeEmpty = toBool(args[1], false); + + return yield this._listReceived(minconf, includeEmpty, false); +}); + +RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account) { + var wallet = this.wallet; + var out = []; + var result = []; + var map = {}; + var paths = yield wallet.getPaths(); + var i, j, path, wtx, output, conf, hash; + var entry, address, keys, key, item, txs; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + address = path.toAddress(); + map[path.hash] = { + involvesWatchonly: wallet.watchOnly, + address: address.toBase58(this.network), + account: path.name, + amount: 0, + confirmations: -1, + label: '', + }; + } + + txs = yield wallet.getHistory(); + + for (i = 0; i < txs.length; i++) { + wtx = txs[i]; + + conf = wtx.getDepth(this.chain.height); + + if (conf < minconf) + continue; + + for (j = 0; j < wtx.tx.outputs.length; j++) { + output = wtx.tx.outputs[j]; + address = output.getAddress(); + + if (!address) + continue; + + hash = address.getHash('hex'); + entry = map[hash]; + + if (entry) { + if (entry.confirmations === -1 || conf < entry.confirmations) + entry.confirmations = conf; + entry.address = address.toBase58(this.network); + entry.amount += output.value; + } + } + } + + keys = Object.keys(map); + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + + if (account) { + map = {}; + + for (i = 0; i < out.length; i++) { + entry = out[i]; + item = map[entry.account]; + if (!item) { + map[entry.account] = entry; + entry.address = undefined; + continue; + } + item.amount += entry.amount; + } + + out = []; + keys = Object.keys(map); + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + } + + for (i = 0; i < out.length; i++) { + entry = out[i]; + + 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 = co(function* listsinceblock(args, help) { + var wallet = this.wallet; + var minconf = 0; + var out = []; + var i, block, highest, height; + var txs, wtx, json; + + if (help) { + throw new RPCError('listsinceblock' + + ' ( "blockhash" target-confirmations includeWatchonly)'); + } + + if (args.length > 0) { + block = toHash(args[0]); + if (!block) + throw new RPCError('Invalid parameter.'); + } + + if (args.length > 1) + minconf = toNumber(args[1], 0); + + height = yield this.chain.db.getHeight(block); + + if (height === -1) + height = this.chain.height; + + txs = yield wallet.getHistory(); + + for (i = 0; i < txs.length; i++) { + wtx = txs[i]; + + if (wtx.height < height) + continue; + + if (wtx.getDepth(this.chain.height) < minconf) + continue; + + if (!highest || wtx.height > highest) + highest = wtx; + + json = yield this._toListTX(wtx); + + out.push(json); + } + + return { + transactions: out, + lastblock: highest && highest.block + ? util.revHex(highest.block) + : encoding.NULL_HASH + }; +}); + +RPC.prototype._toListTX = co(function* _toListTX(wtx) { + var wallet = this.wallet; + var sent = 0; + var received = 0; + var receive = true; + var sendMember, recMember, sendIndex, recIndex; + var i, member, index; + var details = yield wallet.toDetails(wtx); + + if (!details) + throw new RPCError('TX not found.'); + + for (i = 0; i < details.inputs.length; i++) { + member = details.inputs[i]; + if (member.path) { + receive = false; + break; + } + } + + for (i = 0; i < details.outputs.length; i++) { + 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; + } + + 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.toBase58(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.ts, + txid: util.revHex(details.hash), + walletconflicts: [], + time: details.ps, + timereceived: details.ps, + 'bip125-replaceable': 'no' + }; +}); + +RPC.prototype.listtransactions = co(function* listtransactions(args, help) { + var wallet = this.wallet; + var account = null; + var count = 10; + var i, txs, wtx, json; + + if (help || args.length > 4) { + throw new RPCError( + 'listtransactions ( "account" count from includeWatchonly)'); + } + + if (args.length > 0) { + account = toString(args[0]); + if (!account) + account = 'default'; + } + + if (args.length > 1) { + count = toNumber(args[1], 10); + if (count < 0) + count = 10; + } + + txs = yield wallet.getHistory(); + + sortTX(txs); + + for (i = 0; i < txs.length; i++) { + wtx = txs[i]; + json = yield this._toListTX(wtx); + txs[i] = json; + } + + return txs; +}); + +RPC.prototype.listunspent = co(function* listunspent(args, help) { + var wallet = this.wallet; + var minDepth = 1; + var maxDepth = 9999999; + var out = []; + var i, addresses, addrs, depth, address, hash, coins, coin, ring; + + if (help || args.length > 3) { + throw new RPCError('listunspent' + + ' ( minconf maxconf ["address",...] )'); + } + + if (args.length > 0) + minDepth = toNumber(args[0], 1); + + if (args.length > 1) + maxDepth = toNumber(args[1], maxDepth); + + if (args.length > 2) + addrs = toArray(args[2]); + + if (addrs) { + addresses = {}; + for (i = 0; i < addrs.length; i++) { + address = toString(addrs[i]); + hash = Address.getHash(address, 'hex'); + + if (!hash) + throw new RPCError('Invalid address.'); + + if (addresses[hash]) + throw new RPCError('Duplicate address.'); + + addresses[hash] = true; + } + } + + coins = yield wallet.getCoins(); + + sortCoins(coins); + + for (i = 0; i < coins.length; i++ ) { + coin = coins[i]; + depth = coin.getDepth(this.chain.height); + + if (!(depth >= minDepth && depth <= maxDepth)) + continue; + + address = coin.getAddress(); + + if (!address) + continue; + + hash = coin.getHash('hex'); + + if (addresses) { + if (!hash || !addresses[hash]) + continue; + } + + ring = yield wallet.getKey(hash); + + out.push({ + txid: coin.txid(), + vout: coin.index, + address: address ? address.toBase58(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 = co(function* lockunspent(args, help) { + var wallet = this.wallet; + var i, unlock, outputs, output, outpoint; + + if (help || args.length < 1 || args.length > 2) { + throw new RPCError('lockunspent' + + ' unlock ([{"txid":"txid","vout":n},...])'); + } + + unlock = toBool(args[0]); + + if (args.length === 1) { + if (unlock) + wallet.unlockCoins(); + return true; + } + + outputs = toArray(args[1]); + + if (!outputs) + throw new RPCError('Invalid parameter.'); + + for (i = 0; i < outputs.length; i++) { + output = outputs[i]; + + if (!output || typeof output !== 'object') + throw new RPCError('Invalid parameter.'); + + outpoint = new Outpoint(); + outpoint.hash = toHash(output.txid); + outpoint.index = toNumber(output.vout); + + if (!outpoint.hash) + throw new RPCError('Invalid parameter.'); + + if (outpoint.index < 0) + throw new RPCError('Invalid parameter.'); + + if (unlock) { + wallet.unlockCoin(outpoint); + continue; + } + + wallet.lockCoin(outpoint); + } + + return true; +}); + +RPC.prototype.move = co(function* move(args, help) { + // Not implementing: stupid and deprecated. + throw new Error('Not implemented.'); +}); + +RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) { + var wallet = this.wallet; + var tx, options; + + options = { + account: account, + subtractFee: subtractFee, + rate: this.feeRate, + outputs: [{ + address: address, + value: amount + }] + }; + + tx = yield wallet.send(options); + + return tx.txid(); +}); + +RPC.prototype.sendfrom = co(function* sendfrom(args, help) { + var account, address, amount; + + if (help || args.length < 3 || args.length > 6) { + throw new RPCError('sendfrom' + + ' "fromaccount" "tobitcoinaddress"' + + ' amount ( minconf "comment" "comment-to" )'); + } + + account = toString(args[0]); + address = Address.fromBase58(toString(args[1]), this.network); + amount = toSatoshi(args[2]); + + if (!account) + account = 'default'; + + return yield this._send(account, address, amount, false); +}); + +RPC.prototype.sendmany = co(function* sendmany(args, help) { + var wallet = this.wallet; + var minconf = 1; + var outputs = []; + var uniq = {}; + var account, sendTo, comment, subtractFee; + var i, keys, tx, key, value, address; + var hash, output, options; + + if (help || args.length < 2 || args.length > 5) { + throw new RPCError('sendmany' + + ' "fromaccount" {"address":amount,...}' + + ' ( minconf "comment" ["address",...] )'); + } + + account = toString(args[0]); + sendTo = toObject(args[1]); + + if (!account) + account = 'default'; + + if (!sendTo) + throw new RPCError('Invalid parameter.'); + + if (args.length > 2) + minconf = toNumber(args[2], 1); + + if (args.length > 3) + comment = toString(args[3]); + + if (args.length > 4) { + subtractFee = args[4]; + if (typeof subtractFee !== 'boolean') { + if (!util.isNumber(subtractFee)) + throw new RPCError('Invalid parameter.'); + } + } + + keys = Object.keys(sendTo); + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + value = toSatoshi(sendTo[key]); + address = Address.fromBase58(key, this.network); + hash = address.getHash('hex'); + + if (uniq[hash]) + throw new RPCError('Invalid parameter.'); + + uniq[hash] = true; + + output = new Output(); + output.value = value; + output.script.fromAddress(address); + outputs.push(output); + } + + options = { + outputs: outputs, + subtractFee: subtractFee, + account: account, + depth: minconf + }; + + tx = yield wallet.send(options); + + return tx.txid(); +}); + +RPC.prototype.sendtoaddress = co(function* sendtoaddress(args, help) { + var address, amount, subtractFee; + + if (help || args.length < 2 || args.length > 5) { + throw new RPCError('sendtoaddress' + + ' "bitcoinaddress" amount' + + ' ( "comment" "comment-to"' + + ' subtractfeefromamount )'); + } + + address = Address.fromBase58(toString(args[0]), this.network); + amount = toSatoshi(args[1]); + subtractFee = toBool(args[4]); + + return yield this._send(null, address, amount, subtractFee); +}); + +RPC.prototype.setaccount = co(function* setaccount(args, help) { + if (help || args.length < 1 || args.length > 2) + throw new RPCError('setaccount "bitcoinaddress" "account"'); + + // Impossible to implement in bcoin: + throw new Error('Not implemented.'); +}); + +RPC.prototype.settxfee = co(function* settxfee(args, help) { + if (help || args.length < 1 || args.length > 1) + throw new RPCError('settxfee amount'); + + this.feeRate = toSatoshi(args[0]); + + return true; +}); + +RPC.prototype.signmessage = co(function* signmessage(args, help) { + var wallet = this.wallet; + var address, msg, sig, ring; + + if (help || args.length !== 2) + throw new RPCError('signmessage "bitcoinaddress" "message"'); + + address = toString(args[0]); + msg = toString(args[1]); + + address = Address.getHash(address, 'hex'); + + if (!address) + throw new RPCError('Invalid address.'); + + ring = yield wallet.getKey(address); + + if (!ring) + throw new RPCError('Address not found.'); + + if (!wallet.master.key) + throw new RPCError('Wallet is locked.'); + + msg = new Buffer(RPC.magic + msg, 'utf8'); + msg = crypto.hash256(msg); + + sig = ring.sign(msg); + + return sig.toString('base64'); +}); + +RPC.prototype.walletlock = co(function* walletlock(args, help) { + var wallet = this.wallet; + + if (help || (wallet.master.encrypted && args.length !== 0)) + throw new RPCError('walletlock'); + + if (!wallet.master.encrypted) + throw new RPCError('Wallet is not encrypted.'); + + yield wallet.lock(); + + return null; +}); + +RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args, help) { + var wallet = this.wallet; + var old, new_; + + if (help || (wallet.master.encrypted && args.length !== 2)) { + throw new RPCError('walletpassphrasechange' + + ' "oldpassphrase" "newpassphrase"'); + } + + if (!wallet.master.encrypted) + throw new RPCError('Wallet is not encrypted.'); + + old = toString(args[0]); + new_ = toString(args[1]); + + if (old.length < 1 || new_.length < 1) + throw new RPCError('Invalid parameter'); + + yield wallet.setPassphrase(old, new_); + + return null; +}); + +RPC.prototype.walletpassphrase = co(function* walletpassphrase(args, help) { + var wallet = this.wallet; + var passphrase, timeout; + + if (help || (wallet.master.encrypted && args.length !== 2)) + throw new RPCError('walletpassphrase "passphrase" timeout'); + + if (!wallet.master.encrypted) + throw new RPCError('Wallet is not encrypted.'); + + passphrase = toString(args[0]); + timeout = toNumber(args[1]); + + if (passphrase.length < 1) + throw new RPCError('Invalid parameter'); + + if (timeout < 0) + throw new RPCError('Invalid parameter'); + + yield wallet.unlock(passphrase, timeout); + + return null; +}); + +RPC.prototype.importprunedfunds = co(function* importprunedfunds(args, help) { + var tx, block, hash, label, height; + + if (help || args.length < 2 || args.length > 3) { + throw new RPCError('importprunedfunds' + + ' "rawtransaction" "txoutproof" ( "label" )'); + } + + tx = args[0]; + block = args[1]; + + if (!util.isHex(tx) || !util.isHex(block)) + throw new RPCError('Invalid parameter.'); + + tx = TX.fromRaw(tx, 'hex'); + block = MerkleBlock.fromRaw(block, 'hex'); + hash = block.hash('hex'); + + if (args.length === 3) + label = toString(args[2]); + + if (!block.verify()) + throw new RPCError('Invalid proof.'); + + if (!block.hasTX(tx.hash('hex'))) + throw new RPCError('Invalid proof.'); + + height = yield this.chain.db.getHeight(hash); + + if (height === -1) + throw new RPCError('Invalid proof.'); + + block = { + hash: hash, + ts: block.ts, + height: height + }; + + if (!(yield this.walletdb.addTX(tx, block))) + throw new RPCError('No tracked address for TX.'); + + return null; +}); + +RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args, help) { + var wallet = this.wallet; + var hash; + + if (help || args.length !== 1) + throw new RPCError('removeprunedfunds "txid"'); + + hash = toHash(args[0]); + + if (!hash) + throw new RPCError('Invalid parameter.'); + + if (!(yield wallet.remove(hash))) + throw new RPCError('Transaction not in wallet.'); + + return null; +}); + +RPC.prototype.selectwallet = co(function* selectwallet(args, help) { + var id, wallet; + + if (help || args.length !== 1) + throw new RPCError('selectwallet "id"'); + + id = toString(args[0]); + wallet = yield this.walletdb.get(id); + + if (!wallet) + throw new RPCError('Wallet not found.'); + + this.wallet = wallet; + + return null; +}); + +/* + * Helpers + */ + +function RPCError(msg) { + Error.call(this); + + if (Error.captureStackTrace) + Error.captureStackTrace(this, RPCError); + + this.type = 'RPCError'; + this.message = msg; +} + +util.inherits(RPCError, Error); + +function toBool(obj, def) { + if (typeof obj === 'boolean' || typeof obj === 'number') + return !!obj; + return def || false; +} + +function toNumber(obj, def) { + if (util.isNumber(obj)) + return obj; + return def != null ? def : -1; +} + +function toString(obj, def) { + if (typeof obj === 'string') + return obj; + return def != null ? def : ''; +} + +function toArray(obj, def) { + if (Array.isArray(obj)) + return obj; + return def != null ? def : null; +} + +function toObject(obj, def) { + if (obj && typeof obj === 'object') + return obj; + return def != null ? def : null; +} + +function toHash(obj) { + if (!isHash(obj)) + return null; + return util.revHex(obj); +} + +function isHash(obj) { + return util.isHex(obj) && obj.length === 64; +} + +function toSatoshi(obj) { + if (typeof obj !== 'number') + throw new RPCError('Bad BTC amount.'); + return Amount.value(obj, true); +} + +function sortTX(txs) { + return txs.sort(function(a, b) { + return a.ps - b.ps; + }); +} + +function sortCoins(coins) { + return coins.sort(function(a, b) { + a = a.height === -1 ? 0x7fffffff : a.height; + b = b.height === -1 ? 0x7fffffff : b.height; + return a - b; + }); +} + +/* + * Expose + */ + +module.exports = RPC; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 979be82e..d3f96133 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1424,7 +1424,7 @@ Wallet.prototype._fund = co(function* fund(mtx, options) { rate = options.rate; if (rate == null) - rate = yield this.db.estimateFee(); + rate = yield this.db.estimateFee(options.blocks); if (options.smart) { coins = yield this.getSmartCoins(options.account); diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 38aa493b..b450f70a 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -26,6 +26,8 @@ var Logger = require('../node/logger'); var Outpoint = require('../primitives/outpoint'); var layouts = require('./layout'); var records = require('./records'); +var NodeClient = require('../node/nodeclient'); +var HTTP = require('./http'); var layout = layouts.walletdb; var ChainState = records.ChainState; var BlockMapRecord = records.BlockMapRecord; @@ -62,6 +64,20 @@ function WalletDB(options) { this.logger = this.options.logger; this.client = this.options.client; this.db = LDB(this.options); + this.primary = null; + + this.http = new HTTP({ + 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.wallets = Object.create(null); @@ -83,6 +99,54 @@ function WalletDB(options) { util.inherits(WalletDB, AsyncObject); +/** + * Plugin name. + * @const {String} + */ + +WalletDB.id = 'walletdb'; + +/** + * Plugin initialization. + * @param {Node} node + * @returns {WalletDB} + */ + +WalletDB.init = function init(node) { + var config = node.config; + var client = new NodeClient(node); + var wdb; + + wdb = new WalletDB({ + network: node.network, + logger: node.logger, + client: client, + prefix: config.prefix, + db: config.str(['wallet-db', 'db']), + maxFiles: config.num('wallet-max-files'), + cacheSize: config.mb('wallet-cache-size'), + witness: config.bool('wallet-witness'), + checkpoints: config.bool('wallet-checkpoints'), + startHeight: config.num('wallet-start-height'), + wipeNoReally: config.bool('wallet-wipe-no-really'), + apiKey: config.str(['wallet-api-key', 'api-key']), + walletAuth: config.bool('wallet-auth'), + noAuth: config.bool('no-auth'), + ssl: config.str('wallet-ssl'), + host: config.str('wallet-host'), + port: config.num('wallet-port'), + spv: node.chain.options.spv, + verify: node.chain.options.spv + }); + + if (node.http) { + wdb.http.attach(node.http); + wdb.http.rpc.attach(node.http.rpc); + } + + return wdb; +}; + /** * Database layout. * @type {Object} @@ -119,6 +183,8 @@ WalletDB.prototype._init = function _init() { */ WalletDB.prototype._open = co(function* open() { + var wallet; + yield this.db.open(); yield this.db.checkVersion('V', 6); @@ -134,6 +200,17 @@ WalletDB.prototype._open = co(function* open() { this.depth, this.state.height, this.state.startHeight); + + wallet = yield this.ensure({ + id: 'primary' + }); + + this.logger.info( + 'Loaded wallet with id=%s wid=%d address=%s', + wallet.id, wallet.wid, wallet.getAddress()); + + this.primary = wallet; + this.http.rpc.wallet = wallet; }); /** @@ -2185,6 +2262,12 @@ function WalletOptions(options) { this.startHeight = 0; this.keepBlocks = this.network.block.keepBlocks; this.wipeNoReally = false; + this.apiKey = null; + this.walletAuth = false; + this.noAuth = false; + this.ssl = false; + this.host = '127.0.0.1'; + this.port = this.network.rpcPort + 2; if (options) this.fromOptions(options); @@ -2201,6 +2284,7 @@ WalletOptions.prototype.fromOptions = function fromOptions(options) { if (options.network != null) { this.network = Network.get(options.network); this.keepBlocks = this.network.block.keepBlocks; + this.port = this.network.rpcPort + 2; } if (options.logger != null) { @@ -2270,6 +2354,36 @@ WalletOptions.prototype.fromOptions = function fromOptions(options) { this.wipeNoReally = options.wipeNoReally; } + if (options.apiKey != null) { + assert(typeof options.apiKey === 'string'); + this.apiKey = options.apiKey; + } + + if (options.walletAuth != null) { + assert(typeof options.walletAuth === 'boolean'); + this.walletAuth = options.walletAuth; + } + + if (options.noAuth != null) { + assert(typeof options.noAuth === 'boolean'); + this.noAuth = options.noAuth; + } + + if (options.ssl != null) { + assert(typeof options.ssl === 'boolean'); + this.ssl = options.ssl; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(typeof options.port === 'number'); + this.port = options.port; + } + return this; }; diff --git a/test/http-test.js b/test/http-test.js index 0710c5a8..eb16a4ce 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -13,13 +13,15 @@ var FullNode = require('../lib/node/fullnode'); var pkg = require('../lib/pkg'); describe('HTTP', function() { - var node, wallet, addr, hash; + var node, wallet, walletdb, addr, hash; node = new FullNode({ network: 'regtest', apiKey: 'foo', walletAuth: true, - db: 'memory' + db: 'memory', + loader: require, + plugins: ['walletdb'] }); wallet = new HTTP.Wallet({ @@ -27,6 +29,8 @@ describe('HTTP', function() { apiKey: 'foo' }); + walletdb = node.require('walletdb'); + node.on('error', function() {}); this.timeout(15000); @@ -82,7 +86,7 @@ describe('HTTP', function() { details = d; }); - yield node.walletdb.addTX(tx); + yield walletdb.addTX(tx); yield co.timeout(300); assert(receive); diff --git a/test/node-test.js b/test/node-test.js index 8a3ce2c5..c9a319a0 100644 --- a/test/node-test.js +++ b/test/node-test.js @@ -11,14 +11,19 @@ var MTX = require('../lib/primitives/mtx'); // var Client = require('../lib/wallet/client'); describe('Node', function() { - var node = new FullNode({ db: 'memory', apiKey: 'foo', network: 'regtest' }); + var node = new FullNode({ + db: 'memory', + apiKey: 'foo', + network: 'regtest', + loader: require, + plugins: ['../lib/wallet/walletdb'] + }); var chain = node.chain; - var walletdb = node.walletdb; + var walletdb = node.require('walletdb'); var miner = node.miner; var wallet, tip1, tip2, cb1, cb2, mineBlock; // walletdb.client = new Client({ apiKey: 'foo', network: 'regtest' }); - walletdb.options.resolution = false; node.on('error', function() {});