1149 lines
27 KiB
JavaScript
1149 lines
27 KiB
JavaScript
/*!
|
|
* server.js - http server for bcoin
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/* jshint -W069 */
|
|
|
|
var bcoin = require('../env');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var constants = bcoin.protocol.constants;
|
|
var http = require('./');
|
|
var HTTPBase = http.base;
|
|
var utils = require('../utils');
|
|
var assert = utils.assert;
|
|
var RPC; /*= require('./rpc'); - load lazily */
|
|
|
|
/**
|
|
* HTTPServer
|
|
* @exports HTTPServer
|
|
* @constructor
|
|
* @param {Object} options
|
|
* @param {Fullnode} options.node
|
|
* @see HTTPBase
|
|
* @emits HTTPServer#websocket
|
|
*/
|
|
|
|
function HTTPServer(options) {
|
|
if (!(this instanceof HTTPServer))
|
|
return new HTTPServer(options);
|
|
|
|
if (!options)
|
|
options = {};
|
|
|
|
EventEmitter.call(this);
|
|
|
|
this.options = options;
|
|
this.node = options.node;
|
|
|
|
assert(this.node, 'HTTP requires a Node.');
|
|
|
|
this.network = this.node.network;
|
|
this.walletdb = this.node.walletdb;
|
|
this.mempool = this.node.mempool;
|
|
this.pool = this.node.pool;
|
|
this.logger = options.logger || this.node.logger;
|
|
this.loaded = false;
|
|
this.apiKey = options.apiKey;
|
|
this.rpc = null;
|
|
|
|
if (this.apiKey) {
|
|
if (typeof this.apiKey === 'string') {
|
|
assert(utils.isHex(this.apiKey), 'API key must be a hex string.');
|
|
this.apiKey = new Buffer(this.apiKey, 'hex');
|
|
}
|
|
assert(Buffer.isBuffer(this.apiKey));
|
|
assert(this.apiKey.length === 32, 'API key must be 32 bytes.');
|
|
} else {
|
|
this.apiKey = bcoin.ec.random(32);
|
|
}
|
|
|
|
if (options.noAuth)
|
|
this.apiKey = null;
|
|
|
|
options.sockets = true;
|
|
|
|
this.server = new HTTPBase(options);
|
|
|
|
this._init();
|
|
}
|
|
|
|
utils.inherits(HTTPServer, EventEmitter);
|
|
|
|
/**
|
|
* Initialize routes.
|
|
* @private
|
|
*/
|
|
|
|
HTTPServer.prototype._init = function _init() {
|
|
var self = this;
|
|
|
|
this.server.on('request', function(req, res) {
|
|
self.logger.debug('Request for path=%s (%s).',
|
|
req.pathname, req.socket.remoteAddress);
|
|
});
|
|
|
|
this.use(function(req, res, next, send) {
|
|
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;
|
|
return res.end();
|
|
}
|
|
|
|
res.setHeader('X-Bcoin-Version', constants.USER_VERSION);
|
|
res.setHeader('X-Bcoin-Agent', constants.USER_AGENT);
|
|
res.setHeader('X-Bcoin-Network', self.network.type);
|
|
res.setHeader('X-Bcoin-Height', self.node.chain.height + '');
|
|
res.setHeader('X-Bcoin-Tip', utils.revHex(self.node.chain.tip.hash));
|
|
|
|
next();
|
|
});
|
|
|
|
this.use(function(req, res, next, send) {
|
|
var auth = req.headers['authorization'];
|
|
var parts;
|
|
|
|
if (!auth) {
|
|
req.username = null;
|
|
req.password = null;
|
|
return next();
|
|
}
|
|
|
|
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(':');
|
|
assert(parts.length >= 2, 'Invalid auth token.');
|
|
|
|
req.username = parts.shift();
|
|
req.password = parts.join(':');
|
|
|
|
next();
|
|
});
|
|
|
|
this.use(function(req, res, next, send) {
|
|
var params, options;
|
|
|
|
if (req.method === 'POST'
|
|
&& req.pathname === '/') {
|
|
if (self.apiKey) {
|
|
assert(utils.isHex(req.password), 'API key must be a hex string.');
|
|
assert(req.password.length === 64, 'API key must be 32 bytes.');
|
|
req.password = new Buffer(req.password, 'hex');
|
|
}
|
|
return next();
|
|
}
|
|
|
|
params = utils.merge({}, req.params, req.query, req.body);
|
|
options = {};
|
|
|
|
self.logger.debug('Params:');
|
|
self.logger.debug(params);
|
|
|
|
if (params.id) {
|
|
assert(params.id !== '!all');
|
|
options.id = params.id;
|
|
}
|
|
|
|
if (params.hash) {
|
|
if (params.hash.length !== 64)
|
|
options.height = params.hash >>> 0;
|
|
else
|
|
options.hash = utils.revHex(params.hash);
|
|
}
|
|
|
|
if (params.index != null)
|
|
options.index = params.index >>> 0;
|
|
|
|
if (params.height != null)
|
|
options.height = params.height >>> 0;
|
|
|
|
if (params.start != null)
|
|
options.start = params.start >>> 0;
|
|
|
|
if (params.end != null)
|
|
options.end = params.end >>> 0;
|
|
|
|
if (params.limit != null)
|
|
options.limit = params.limit >>> 0;
|
|
|
|
if (params.address) {
|
|
params.addresses = params.address;
|
|
options.address = params.address;
|
|
}
|
|
|
|
if (params.rate)
|
|
options.rate = utils.satoshi(params.rate);
|
|
|
|
if (params.subtractFee)
|
|
options.subtractFee = params.subtractFee;
|
|
|
|
if (Array.isArray(params.outputs)) {
|
|
options.outputs = params.outputs.map(function(output) {
|
|
return {
|
|
address: output.address,
|
|
script: decodeScript(output.script),
|
|
value: utils.satoshi(output.value)
|
|
};
|
|
});
|
|
}
|
|
|
|
if (params.addresses) {
|
|
if (typeof params.addresses === 'string')
|
|
options.addresses = params.addresses.split(',');
|
|
else
|
|
options.addresses = params.addresses;
|
|
}
|
|
|
|
if (params.tx) {
|
|
try {
|
|
if (typeof params.tx === 'object')
|
|
options.tx = bcoin.tx.fromJSON(params.tx);
|
|
else
|
|
options.tx = bcoin.tx.fromRaw(params.tx, 'hex');
|
|
} catch (e) {
|
|
return next(e);
|
|
}
|
|
}
|
|
|
|
if (typeof params.account === 'string')
|
|
options.account = params.account || null;
|
|
else if (typeof params.account === 'number')
|
|
options.account = params.account;
|
|
|
|
if (params.name)
|
|
options.name = params.name;
|
|
|
|
if (params.age)
|
|
options.age = params.age >>> 0;
|
|
|
|
if (params.key)
|
|
params.keys = params.key;
|
|
|
|
if (params.keys) {
|
|
if (typeof params.keys === 'string')
|
|
options.keys = params.keys.split(',');
|
|
else
|
|
options.keys = params.keys;
|
|
}
|
|
|
|
if (params.passphrase)
|
|
options.passphrase = params.passphrase;
|
|
|
|
if (req.password) {
|
|
assert(utils.isHex(req.password), 'API key must be a hex string.');
|
|
assert(req.password.length === 64, 'API key must be 32 bytes.');
|
|
options.token = new Buffer(req.password, 'hex');
|
|
}
|
|
|
|
if (req.headers['x-bcoin-api-key'])
|
|
params.apiKey = req.headers['x-bcoin-api-key'];
|
|
|
|
if (params.apiKey) {
|
|
assert(utils.isHex(params.apiKey), 'API key must be a hex string.');
|
|
assert(params.apiKey.length === 64, 'API key must be 32 bytes.');
|
|
options.apiKey = new Buffer(params.apiKey, 'hex');
|
|
}
|
|
|
|
req.options = options;
|
|
|
|
next();
|
|
});
|
|
|
|
this.use(function(req, res, next, send) {
|
|
if (self.apiKey) {
|
|
if (!utils.ccmp(req.options.apiKey, self.apiKey)) {
|
|
send(403, { error: 'Forbidden.' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (req.path.length < 2 || req.path[0] !== 'wallet')
|
|
return next();
|
|
|
|
if (!self.options.walletAuth)
|
|
return next();
|
|
|
|
self.walletdb.auth(req.options.id, req.options.token, function(err) {
|
|
if (err) {
|
|
if (err.message === 'Wallet not found.')
|
|
return next();
|
|
self.logger.info('Auth failure for %s: %s.',
|
|
req.options.id, err.message);
|
|
res.setHeader('WWW-Authenticate', 'Basic realm="wallet"');
|
|
send(401, { error: err.message });
|
|
return;
|
|
}
|
|
|
|
self.logger.info('Successful auth for %s.', req.options.id);
|
|
next();
|
|
});
|
|
});
|
|
|
|
function decodeScript(script) {
|
|
if (!script)
|
|
return;
|
|
if (typeof script === 'string')
|
|
return bcoin.script.fromRaw(script, 'hex');
|
|
return new bcoin.script(script);
|
|
}
|
|
|
|
// JSON RPC
|
|
this.post('/', function(req, res, next, send) {
|
|
if (!(req.body.method && req.body.params))
|
|
return next(new Error('Method not found.'));
|
|
|
|
if (self.apiKey) {
|
|
if (!utils.ccmp(req.password, self.apiKey)) {
|
|
res.setHeader('WWW-Authenticate', 'Basic realm="rpc"');
|
|
send(401, {
|
|
result: null,
|
|
error: {
|
|
message: 'Bad auth.',
|
|
code: 1
|
|
},
|
|
id: req.body.id
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!self.rpc) {
|
|
RPC = require('./rpc');
|
|
self.rpc = new RPC(self.node);
|
|
}
|
|
|
|
self.rpc.execute(req.body, function(err, json) {
|
|
if (err) {
|
|
self.logger.error(err);
|
|
return send(400, {
|
|
result: null,
|
|
error: {
|
|
message: err.message,
|
|
code: 1
|
|
},
|
|
id: req.body.id
|
|
});
|
|
}
|
|
send(200, {
|
|
result: json,
|
|
error: null,
|
|
id: req.body.id
|
|
});
|
|
});
|
|
});
|
|
|
|
this.get('/', function(req, res, next, send) {
|
|
send(200, {
|
|
version: constants.USER_VERSION,
|
|
agent: constants.USER_AGENT,
|
|
network: self.network.type,
|
|
height: self.node.chain.height,
|
|
tip: utils.revHex(self.node.chain.tip.hash),
|
|
peers: self.node.pool.peers.all.length,
|
|
progress: self.node.chain.getProgress()
|
|
});
|
|
});
|
|
|
|
// UTXO by address
|
|
this.get('/coin/address/:address', function(req, res, next, send) {
|
|
self.node.getCoinsByAddress(req.options.addresses, function(err, coins) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coins.length)
|
|
return send(404);
|
|
|
|
send(200, coins.map(function(coin) {
|
|
return coin.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
|
|
// UTXO by id
|
|
this.get('/coin/:hash/:index', function(req, res, next, send) {
|
|
self.node.getCoin(req.options.hash, req.options.index, function(err, coin) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coin)
|
|
return send(404);
|
|
|
|
send(200, coin.toJSON());
|
|
});
|
|
});
|
|
|
|
// Bulk read UTXOs
|
|
this.post('/coin/address', function(req, res, next, send) {
|
|
self.node.getCoinsByAddress(req.options.addresses, function(err, coins) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coins.length)
|
|
return send(404);
|
|
|
|
send(200, coins.map(function(coin) {
|
|
return coin.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
|
|
// TX by hash
|
|
this.get('/tx/:hash', function(req, res, next, send) {
|
|
self.node.getTX(req.options.hash, function(err, tx) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!tx)
|
|
return send(404);
|
|
|
|
self.node.fillHistory(tx, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
});
|
|
|
|
// TX by address
|
|
this.get('/tx/address/:address', function(req, res, next, send) {
|
|
self.node.getTXByAddress(req.options.addresses, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.node.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Bulk read TXs
|
|
this.post('/tx/address', function(req, res, next, send) {
|
|
self.node.getTXByAddress(req.options.addresses, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.node.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Block by hash/height
|
|
this.get('/block/:hash', function(req, res, next, send) {
|
|
var hash = req.options.hash || req.options.height;
|
|
self.node.getFullBlock(hash, function(err, block) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!block)
|
|
return send(404);
|
|
|
|
send(200, block.toJSON());
|
|
});
|
|
});
|
|
|
|
// Mempool snapshot
|
|
this.get('/mempool', function(req, res, next, send) {
|
|
self.node.mempool.getHistory(function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.node.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Broadcast TX
|
|
this.post('/broadcast', function(req, res, next, send) {
|
|
self.node.sendTX(req.options.tx, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { success: true });
|
|
});
|
|
});
|
|
|
|
// Get wallet
|
|
this.get('/wallet/:id', function(req, res, next, send) {
|
|
self.walletdb.getInfo(req.options.id, function(err, wallet) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!wallet)
|
|
return send(404);
|
|
|
|
send(200, wallet.toJSON());
|
|
});
|
|
});
|
|
|
|
// Create/get wallet
|
|
this.post('/wallet/:id?', function(req, res, next, send) {
|
|
var json;
|
|
self.walletdb.ensure(req.options, function(err, wallet) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!wallet)
|
|
return send(404);
|
|
|
|
json = wallet.toJSON();
|
|
wallet.destroy();
|
|
|
|
send(200, json);
|
|
});
|
|
});
|
|
|
|
// List accounts
|
|
this.get('/wallet/:id/account', function(req, res, next, send) {
|
|
self.walletdb.getAccounts(req.options.id, function(err, accounts) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (accounts.length === 0)
|
|
return send(404);
|
|
|
|
send(200, accounts);
|
|
});
|
|
});
|
|
|
|
// Create/get account
|
|
this.post('/wallet/:id/account/:account?', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
options.name = options.account || options.name;
|
|
self.walletdb.ensureAccount(id, options, function(err, account) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!account)
|
|
return send(404);
|
|
|
|
send(200, account.toJSON());
|
|
});
|
|
});
|
|
|
|
// Change passphrase
|
|
this.post('/wallet/:id/passphrase', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
var old = options.old;
|
|
var new_ = options.passphrase;
|
|
self.walletdb.setPassphrase(id, old, new_, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { success: true });
|
|
});
|
|
});
|
|
|
|
// Generate new token
|
|
this.post('/wallet/:id/retoken', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
self.walletdb.retoken(id, options.passphrase, function(err, token) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { token: token.toString('hex') });
|
|
});
|
|
});
|
|
|
|
// Send TX
|
|
this.post('/wallet/:id/send', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
|
|
self.walletdb.createTX(id, options, function(err, tx) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
self.walletdb.sign(id, tx, options, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
self.node.sendTX(tx, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Create TX
|
|
this.post('/wallet/:id/create', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
|
|
self.walletdb.createTX(id, options, function(err, tx) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
self.walletdb.sign(id, tx, options, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
});
|
|
|
|
// Sign TX
|
|
this.post('/wallet/:id/sign', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var options = req.options;
|
|
var tx = req.options.tx;
|
|
|
|
self.walletdb.sign(id, tx, options, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
|
|
// Fill TX
|
|
this.post('/wallet/:id/fill', function(req, res, next, send) {
|
|
var tx = req.options.tx;
|
|
|
|
self.walletdb.fillHistory(tx, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
|
|
// Zap Wallet TXs
|
|
this.post('/wallet/:id/zap', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
var age = req.options.age;
|
|
|
|
self.walletdb.zap(id, account, age, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { success: true });
|
|
});
|
|
});
|
|
|
|
// Add key
|
|
this.put('/wallet/:id/key', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
var key = req.options.key;
|
|
self.walletdb.addKey(id, account, key, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { success: true });
|
|
});
|
|
});
|
|
|
|
// Remove key
|
|
this.del('/wallet/:id/key', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
var key = req.options.key;
|
|
self.walletdb.removeKey(id, account, key, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, { success: true });
|
|
});
|
|
});
|
|
|
|
// Create address
|
|
this.post('/wallet/:id/address', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
self.walletdb.createAddress(id, account, false, function(err, address) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, address.toJSON());
|
|
});
|
|
});
|
|
|
|
// Wallet Balance
|
|
this.get('/wallet/:id/balance', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
self.walletdb.getBalance(id, account, function(err, balance) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!balance)
|
|
return send(404);
|
|
|
|
send(200, {
|
|
confirmed: utils.btc(balance.confirmed),
|
|
unconfirmed: utils.btc(balance.unconfirmed),
|
|
total: utils.btc(balance.total)
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wallet UTXOs
|
|
this.get('/wallet/:id/coin', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
self.walletdb.getCoins(id, account, function(err, coins) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coins.length)
|
|
return send(404);
|
|
|
|
send(200, coins.map(function(coin) {
|
|
return coin.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
|
|
// Wallet Coin
|
|
this.get('/wallet/:id/coin/:hash/:index', function(req, res, next, send) {
|
|
var hash = req.options.hash;
|
|
var index = req.options.index;
|
|
self.walletdb.getCoin(hash, index, function(err, coin) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!coin)
|
|
return send(404);
|
|
|
|
send(200, coin.toJSON());
|
|
});
|
|
});
|
|
|
|
// Wallet TXs
|
|
this.get('/wallet/:id/tx/history', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
self.walletdb.getHistory(id, account, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.walletdb.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wallet Pending TXs
|
|
this.get('/wallet/:id/tx/unconfirmed', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
self.walletdb.getUnconfirmed(id, account, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.walletdb.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wallet TXs within time range
|
|
this.get('/wallet/:id/tx/range', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
var options = req.options;
|
|
self.walletdb.getRange(id, account, options, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.walletdb.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wallet TXs within time range
|
|
this.get('/wallet/:id/tx/last', function(req, res, next, send) {
|
|
var id = req.options.id;
|
|
var account = req.options.account;
|
|
var limit = req.options.limit;
|
|
self.walletdb.getRange(id, account, limit, function(err, txs) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!txs.length)
|
|
return send(404);
|
|
|
|
utils.forEachSerial(txs, function(tx, next) {
|
|
self.walletdb.fillHistory(tx, next);
|
|
}, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
send(200, txs.map(function(tx) {
|
|
return tx.toJSON();
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Wallet TX
|
|
this.get('/wallet/:id/tx/:hash', function(req, res, next, send) {
|
|
self.walletdb.getTX(req.options.hash, function(err, tx) {
|
|
if (err)
|
|
return next(err);
|
|
|
|
if (!tx)
|
|
return send(404);
|
|
|
|
self.walletdb.fillHistory(tx, function(err) {
|
|
if (err)
|
|
return next(err);
|
|
send(200, tx.toJSON());
|
|
});
|
|
});
|
|
});
|
|
|
|
this.server.on('error', function(err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
this._initIO();
|
|
};
|
|
|
|
/**
|
|
* Initialize websockets.
|
|
* @private
|
|
*/
|
|
|
|
HTTPServer.prototype._initIO = function _initIO() {
|
|
var self = this;
|
|
|
|
if (!this.server.io)
|
|
return;
|
|
|
|
this.server.on('websocket', function(socket) {
|
|
socket.bcoin = new ClientSocket(self, socket);
|
|
socket.bcoin.startTimeout();
|
|
|
|
socket.on('error', function(err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
socket.on('auth', function(apiKey, callback) {
|
|
callback = utils.ensure(callback);
|
|
|
|
if (!self.apiKey) {
|
|
self.logger.info('Successful auth.');
|
|
socket.bcoin.stopTimeout();
|
|
self.emit('websocket', socket);
|
|
return callback();
|
|
}
|
|
|
|
if (!utils.isHex(apiKey))
|
|
return callback({ error: 'Bad key.' });
|
|
|
|
apiKey = new Buffer(apiKey, 'hex');
|
|
|
|
if (!utils.ccmp(apiKey, self.apiKey))
|
|
return callback({ error: 'Bad key.' });
|
|
|
|
self.logger.info('Successful auth.');
|
|
socket.bcoin.stopTimeout();
|
|
self.emit('websocket', socket);
|
|
|
|
return callback();
|
|
});
|
|
|
|
socket.emit('version', {
|
|
version: constants.USER_VERSION,
|
|
agent: constants.USER_AGENT,
|
|
network: self.network.type
|
|
});
|
|
});
|
|
|
|
this.on('websocket', function(socket) {
|
|
socket.on('wallet join', function(id, token, callback) {
|
|
callback = utils.ensure(callback);
|
|
|
|
if (!self.options.walletAuth) {
|
|
socket.join(id);
|
|
return callback();
|
|
}
|
|
|
|
self.walletdb.auth(id, token, function(err) {
|
|
if (err) {
|
|
self.logger.info('Wallet auth failure for %s: %s.', id, err.message);
|
|
return callback({ error: 'Bad token.' });
|
|
}
|
|
self.logger.info('Successful wallet auth for %s.', id);
|
|
socket.join(id);
|
|
return callback();
|
|
});
|
|
});
|
|
|
|
socket.on('wallet leave', function(id, callback) {
|
|
callback = utils.ensure(callback);
|
|
socket.leave(id);
|
|
return callback();
|
|
});
|
|
});
|
|
|
|
this.walletdb.on('tx', function(tx, map) {
|
|
var summary = map.toJSON();
|
|
tx = tx.toJSON();
|
|
map.getWallets().forEach(function(id) {
|
|
self.server.io.to(id).emit('wallet tx', tx, summary);
|
|
});
|
|
self.server.io.to('!all').emit('wallet tx', tx, summary);
|
|
});
|
|
|
|
this.walletdb.on('confirmed', function(tx, map) {
|
|
var summary = map.toJSON();
|
|
tx = tx.toJSON();
|
|
map.getWallets().forEach(function(id) {
|
|
self.server.io.to(id).emit('wallet confirmed', tx, summary);
|
|
});
|
|
self.server.io.to('!all').emit('wallet confirmed', tx, summary);
|
|
});
|
|
|
|
this.walletdb.on('updated', function(tx, map) {
|
|
var summary = map.toJSON();
|
|
tx = tx.toJSON();
|
|
map.getWallets().forEach(function(id) {
|
|
self.server.io.to(id).emit('wallet updated', tx, summary);
|
|
});
|
|
self.server.io.to('!all').emit('wallet updated', tx, summary);
|
|
});
|
|
|
|
this.walletdb.on('balances', function(balances) {
|
|
var json = {};
|
|
Object.keys(balances).forEach(function(id) {
|
|
json[id] = {
|
|
confirmed: utils.btc(balances[id].confirmed),
|
|
unconfirmed: utils.btc(balances[id].unconfirmed),
|
|
total: utils.btc(balances[id].total)
|
|
};
|
|
self.server.io.to(id).emit('wallet balance', json[id], id);
|
|
self.server.io.to('!all').emit('wallet balance', json[id], id);
|
|
});
|
|
self.server.io.to('!all').emit('wallet balances', json);
|
|
});
|
|
|
|
this.walletdb.on('address', function(receive, change, map) {
|
|
var summary = map.toJSON();
|
|
|
|
receive = receive.map(function(address) {
|
|
return address.toJSON();
|
|
});
|
|
|
|
change = change.map(function(address) {
|
|
return address.toJSON();
|
|
});
|
|
|
|
map.getWallets().forEach(function(id) {
|
|
self.server.io.to(id).emit('wallet address', receive, change, summary);
|
|
});
|
|
|
|
self.server.io.to('!all').emit('wallet address', receive, change, summary);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Open the server, wait for socket.
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
HTTPServer.prototype.open = function open(callback) {
|
|
if (this.apiKey)
|
|
this.logger.info('API key: %s', this.apiKey.toString('hex'));
|
|
else
|
|
this.logger.warning('WARNING: Your http server is open to the world.');
|
|
|
|
this.server.open(callback);
|
|
};
|
|
|
|
/**
|
|
* Close the server, wait for server socket to close.
|
|
* @param {Function} callback
|
|
*/
|
|
|
|
HTTPServer.prototype.close = function close(callback) {
|
|
this.server.close(callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#use
|
|
*/
|
|
|
|
HTTPServer.prototype.use = function use(path, callback) {
|
|
return this.server.use(path, callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#get
|
|
*/
|
|
|
|
HTTPServer.prototype.get = function get(path, callback) {
|
|
return this.server.get(path, callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#post
|
|
*/
|
|
|
|
HTTPServer.prototype.post = function post(path, callback) {
|
|
return this.server.post(path, callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#put
|
|
*/
|
|
|
|
HTTPServer.prototype.put = function put(path, callback) {
|
|
return this.server.put(path, callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#del
|
|
*/
|
|
|
|
HTTPServer.prototype.del = function del(path, callback) {
|
|
return this.server.del(path, callback);
|
|
};
|
|
|
|
/**
|
|
* @see HTTPBase#listen
|
|
*/
|
|
|
|
HTTPServer.prototype.listen = function listen(port, host, callback) {
|
|
var self = this;
|
|
return this.server.listen(port, host, function(err, address) {
|
|
if (err) {
|
|
if (callback)
|
|
return callback(err);
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
self.logger.info('HTTP server listening on %s (port=%d).',
|
|
address.address, address.port);
|
|
|
|
self.loaded = true;
|
|
self.emit('open');
|
|
|
|
if (callback)
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* ClientSocket
|
|
* @constructor
|
|
* @param {HTTPServer} server
|
|
* @param {SocketIO.Socket}
|
|
*/
|
|
|
|
function ClientSocket(server, socket) {
|
|
this.server = server;
|
|
this.socket = socket;
|
|
this.timeout = null;
|
|
}
|
|
|
|
ClientSocket.prototype.startTimeout = function startTimeout() {
|
|
var self = this;
|
|
this.stopTimeout();
|
|
this.timeout = setTimeout(function() {
|
|
self.destroy();
|
|
}, 60000);
|
|
};
|
|
|
|
ClientSocket.prototype.stopTimeout = function stopTimeout() {
|
|
if (this.timeout != null) {
|
|
clearTimeout(this.timeout);
|
|
this.timeout = null;
|
|
}
|
|
};
|
|
|
|
ClientSocket.prototype.destroy = function() {
|
|
this.socket.disconnect();
|
|
};
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = HTTPServer;
|