fcoin/lib/wallet/http.js
2017-08-26 01:21:25 -07:00

1077 lines
25 KiB
JavaScript

/*!
* 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';
const assert = require('assert');
const path = require('path');
const HTTPBase = require('../http/base');
const util = require('../utils/util');
const base58 = require('../utils/base58');
const MTX = require('../primitives/mtx');
const Outpoint = require('../primitives/outpoint');
const Script = require('../script/script');
const digest = require('../crypto/digest');
const random = require('../crypto/random');
const ccmp = require('../crypto/ccmp');
const Network = require('../protocol/network');
const Validator = require('../utils/validator');
const Address = require('../primitives/address');
const KeyRing = require('../primitives/keyring');
const common = require('./common');
/**
* HTTPServer
* @alias module:wallet.HTTPServer
* @constructor
* @param {Object} options
* @see HTTPBase
* @emits HTTPServer#socket
*/
function HTTPServer(options) {
if (!(this instanceof HTTPServer))
return new HTTPServer(options);
options = new HTTPOptions(options);
HTTPBase.call(this, options);
this.options = options;
this.network = this.options.network;
this.logger = this.options.logger.context('http');
this.walletdb = this.options.walletdb;
this.server = new HTTPBase(this.options);
this.rpc = this.walletdb.rpc;
this.init();
}
Object.setPrototypeOf(HTTPServer.prototype, HTTPBase.prototype);
/**
* Attach to server.
* @private
* @param {HTTPServer} server
*/
HTTPServer.prototype.attach = function attach(server) {
server.mount('/wallet', this);
};
/**
* Initialize http server.
* @private
*/
HTTPServer.prototype.init = function init() {
this.on('request', (req, res) => {
if (req.method === 'POST' && req.pathname === '/')
return;
this.logger.debug('Request for method=%s path=%s (%s).',
req.method, req.pathname, req.socket.remoteAddress);
});
this.on('listening', (address) => {
this.logger.info('HTTP server listening on %s (port=%d).',
address.address, address.port);
});
this.initRouter();
this.initSockets();
};
/**
* Initialize routes.
* @private
*/
HTTPServer.prototype.initRouter = function initRouter() {
this.use(this.cors());
if (!this.options.noAuth) {
this.use(this.basicAuth({
password: this.options.apiKey,
realm: 'wallet'
}));
}
this.use(this.bodyParser({
contentType: 'json'
}));
this.use(this.jsonRPC(this.rpc));
this.hook(async (req, res) => {
const valid = req.valid();
if (req.path.length === 0)
return;
if (req.path[0] === '_admin')
return;
if (req.method === 'PUT' && req.path.length === 1)
return;
const id = valid.str('id');
const token = valid.buf('token');
if (!this.options.walletAuth) {
const wallet = await this.walletdb.get(id);
if (!wallet) {
res.send(404);
return;
}
req.wallet = wallet;
return;
}
let wallet;
try {
wallet = await 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);
});
// Rescan
this.post('/_admin/rescan', async (req, res) => {
const valid = req.valid();
const height = valid.u32('height');
res.send(200, { success: true });
await this.walletdb.rescan(height);
});
// Resend
this.post('/_admin/resend', async (req, res) => {
await this.walletdb.resend();
res.send(200, { success: true });
});
// Backup WalletDB
this.post('/_admin/backup', async (req, res) => {
const valid = req.valid();
const path = valid.str('path');
enforce(path, 'Path is required.');
await this.walletdb.backup(path);
res.send(200, { success: true });
});
// List wallets
this.get('/_admin/wallets', async (req, res) => {
const wallets = await this.walletdb.getWallets();
res.send(200, wallets);
});
// Get wallet
this.get('/:id', (req, res) => {
res.send(200, req.wallet.toJSON());
});
// Get wallet master key
this.get('/:id/master', (req, res) => {
res.send(200, req.wallet.master.toJSON(true));
});
// Create wallet (compat)
this.post('/', async (req, res) => {
const valid = req.valid();
const wallet = await this.walletdb.create({
id: valid.str('id'),
type: valid.str('type'),
m: valid.u32('m'),
n: valid.u32('n'),
passphrase: valid.str('passphrase'),
master: valid.str('master'),
mnemonic: valid.str('mnemonic'),
witness: valid.bool('witness'),
accountKey: valid.str('accountKey'),
watchOnly: valid.bool('watchOnly')
});
res.send(200, wallet.toJSON());
});
// Create wallet
this.put('/:id', async (req, res) => {
const valid = req.valid();
const wallet = await this.walletdb.create({
id: valid.str('id'),
type: valid.str('type'),
m: valid.u32('m'),
n: valid.u32('n'),
passphrase: valid.str('passphrase'),
master: valid.str('master'),
mnemonic: valid.str('mnemonic'),
witness: valid.bool('witness'),
accountKey: valid.str('accountKey'),
watchOnly: valid.bool('watchOnly')
});
res.send(200, wallet.toJSON());
});
// List accounts
this.get('/:id/account', async (req, res) => {
const accounts = await req.wallet.getAccounts();
res.send(200, accounts);
});
// Get account
this.get('/:id/account/:account', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const account = await req.wallet.getAccount(acct);
if (!account) {
res.send(404);
return;
}
res.send(200, account.toJSON());
});
// Create account (compat)
this.post('/:id/account', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const options = {
name: valid.str(['account', 'name']),
witness: valid.bool('witness'),
watchOnly: valid.bool('watchOnly'),
type: valid.str('type'),
m: valid.u32('m'),
n: valid.u32('n'),
accountKey: valid.str('accountKey'),
lookahead: valid.u32('lookahead')
};
const account = await req.wallet.createAccount(options, passphrase);
res.send(200, account.toJSON());
});
// Create account
this.put('/:id/account/:account', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const options = {
name: valid.str('account'),
witness: valid.bool('witness'),
watchOnly: valid.bool('watchOnly'),
type: valid.str('type'),
m: valid.u32('m'),
n: valid.u32('n'),
accountKey: valid.str('accountKey'),
lookahead: valid.u32('lookahead')
};
const account = await req.wallet.createAccount(options, passphrase);
res.send(200, account.toJSON());
});
// Change passphrase
this.post('/:id/passphrase', async (req, res) => {
const valid = req.valid();
const old = valid.str('old');
const new_ = valid.str('new');
enforce(old || new_, 'Passphrase is required.');
await req.wallet.setPassphrase(old, new_);
res.send(200, { success: true });
});
// Unlock wallet
this.post('/:id/unlock', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const timeout = valid.u32('timeout');
enforce(passphrase, 'Passphrase is required.');
await req.wallet.unlock(passphrase, timeout);
res.send(200, { success: true });
});
// Lock wallet
this.post('/:id/lock', async (req, res) => {
await req.wallet.lock();
res.send(200, { success: true });
});
// Import key
this.post('/:id/import', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const pub = valid.buf('publicKey');
const priv = valid.str('privateKey');
const b58 = valid.str('address');
if (pub) {
const key = KeyRing.fromPublic(pub, this.network);
await req.wallet.importKey(acct, key);
res.send(200, { success: true });
return;
}
if (priv) {
const key = KeyRing.fromSecret(priv, this.network);
await req.wallet.importKey(acct, key);
res.send(200, { success: true });
return;
}
if (b58) {
const addr = Address.fromString(b58, this.network);
await req.wallet.importAddress(acct, addr);
res.send(200, { success: true });
return;
}
enforce(false, 'Key or address is required.');
});
// Generate new token
this.post('/:id/retoken', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const token = await req.wallet.retoken(passphrase);
res.send(200, {
token: token.toString('hex')
});
});
// Send TX
this.post('/:id/send', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const outputs = valid.array('outputs');
const options = {
rate: valid.u64('rate'),
blocks: valid.u32('blocks'),
maxFee: valid.u64('maxFee'),
selection: valid.str('selection'),
smart: valid.bool('smart'),
subtractFee: valid.bool('subtractFee'),
subtractIndex: valid.i32('subtractIndex'),
depth: valid.u32(['confirmations', 'depth']),
outputs: []
};
for (const output of outputs) {
const valid = new Validator([output]);
const raw = valid.buf('script');
let script = null;
if (raw)
script = Script.fromRaw(raw);
options.outputs.push({
script: script,
address: valid.str('address'),
value: valid.u64('value')
});
}
const tx = await req.wallet.send(options, passphrase);
const details = await req.wallet.getDetails(tx.hash('hex'));
res.send(200, details.toJSON());
});
// Create TX
this.post('/:id/create', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const outputs = valid.array('outputs');
const options = {
rate: valid.u64('rate'),
maxFee: valid.u64('maxFee'),
selection: valid.str('selection'),
smart: valid.bool('smart'),
subtractFee: valid.bool('subtractFee'),
subtractIndex: valid.i32('subtractIndex'),
depth: valid.u32(['confirmations', 'depth']),
outputs: []
};
for (const output of outputs) {
const valid = new Validator([output]);
const raw = valid.buf('script');
let script = null;
if (raw)
script = Script.fromRaw(raw);
options.outputs.push({
script: script,
address: valid.str('address'),
value: valid.u64('value')
});
}
const tx = await req.wallet.createTX(options);
await req.wallet.sign(tx, passphrase);
res.send(200, tx.getJSON(this.network));
});
// Sign TX
this.post('/:id/sign', async (req, res) => {
const valid = req.valid();
const passphrase = valid.str('passphrase');
const raw = valid.buf('tx');
enforce(raw, 'TX is required.');
const tx = MTX.fromRaw(raw);
tx.view = await req.wallet.getCoinView(tx);
await req.wallet.sign(tx, passphrase);
res.send(200, tx.getJSON(this.network));
});
// Zap Wallet TXs
this.post('/:id/zap', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const age = valid.u32('age');
enforce(age, 'Age is required.');
await req.wallet.zap(acct, age);
res.send(200, { success: true });
});
// Abandon Wallet TX
this.del('/:id/tx/:hash', async (req, res) => {
const valid = req.valid();
const hash = valid.hash('hash');
enforce(hash, 'Hash is required.');
await req.wallet.abandon(hash);
res.send(200, { success: true });
});
// List blocks
this.get('/:id/block', async (req, res) => {
const heights = await req.wallet.getBlocks();
res.send(200, heights);
});
// Get Block Record
this.get('/:id/block/:height', async (req, res) => {
const valid = req.valid();
const height = valid.u32('height');
enforce(height != null, 'Height is required.');
const block = await req.wallet.getBlock(height);
if (!block) {
res.send(404);
return;
}
res.send(200, block.toJSON());
});
// Add key
this.put('/:id/shared-key', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const key = valid.str('accountKey');
enforce(key, 'Key is required.');
await req.wallet.addSharedKey(acct, key);
res.send(200, { success: true });
});
// Remove key
this.del('/:id/shared-key', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const key = valid.str('accountKey');
enforce(key, 'Key is required.');
await req.wallet.removeSharedKey(acct, key);
res.send(200, { success: true });
});
// Get key by address
this.get('/:id/key/:address', async (req, res) => {
const valid = req.valid();
const address = valid.str('address');
enforce(address, 'Address is required.');
const key = await req.wallet.getKey(address);
if (!key) {
res.send(404);
return;
}
res.send(200, key.toJSON());
});
// Get private key
this.get('/:id/wif/:address', async (req, res) => {
const valid = req.valid();
const address = valid.str('address');
const passphrase = valid.str('passphrase');
enforce(address, 'Address is required.');
const key = await req.wallet.getPrivateKey(address, passphrase);
if (!key) {
res.send(404);
return;
}
res.send(200, { privateKey: key.toSecret() });
});
// Create address
this.post('/:id/address', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const address = await req.wallet.createReceive(acct);
res.send(200, address.toJSON());
});
// Create change address
this.post('/:id/change', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const address = await req.wallet.createChange(acct);
res.send(200, address.toJSON());
});
// Create nested address
this.post('/:id/nested', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const address = await req.wallet.createNested(acct);
res.send(200, address.toJSON());
});
// Wallet Balance
this.get('/:id/balance', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const balance = await req.wallet.getBalance(acct);
if (!balance) {
res.send(404);
return;
}
res.send(200, balance.toJSON());
});
// Wallet UTXOs
this.get('/:id/coin', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const coins = await req.wallet.getCoins(acct);
const result = [];
common.sortCoins(coins);
for (const coin of coins)
result.push(coin.getJSON(this.network));
res.send(200, result);
});
// Locked coins
this.get('/:id/locked', async (req, res) => {
const locked = this.wallet.getLocked();
const result = [];
for (const outpoint of locked)
result.push(outpoint.toJSON());
res.send(200, result);
});
// Lock coin
this.put('/:id/locked/:hash/:index', async (req, res) => {
const valid = req.valid();
const hash = valid.hash('hash');
const index = valid.u32('index');
enforce(hash, 'Hash is required.');
enforce(index != null, 'Index is required.');
const outpoint = new Outpoint(hash, index);
this.wallet.lockCoin(outpoint);
});
// Unlock coin
this.del('/:id/locked/:hash/:index', async (req, res) => {
const valid = req.valid();
const hash = valid.hash('hash');
const index = valid.u32('index');
enforce(hash, 'Hash is required.');
enforce(index != null, 'Index is required.');
const outpoint = new Outpoint(hash, index);
this.wallet.unlockCoin(outpoint);
});
// Wallet Coin
this.get('/:id/coin/:hash/:index', async (req, res) => {
const valid = req.valid();
const hash = valid.hash('hash');
const index = valid.u32('index');
enforce(hash, 'Hash is required.');
enforce(index != null, 'Index is required.');
const coin = await req.wallet.getCoin(hash, index);
if (!coin) {
res.send(404);
return;
}
res.send(200, coin.getJSON(this.network));
});
// Wallet TXs
this.get('/:id/tx/history', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const txs = await req.wallet.getHistory(acct);
common.sortTX(txs);
const details = await req.wallet.toDetails(txs);
const result = [];
for (const item of details)
result.push(item.toJSON());
res.send(200, result);
});
// Wallet Pending TXs
this.get('/:id/tx/unconfirmed', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const txs = await req.wallet.getPending(acct);
common.sortTX(txs);
const details = await req.wallet.toDetails(txs);
const result = [];
for (const item of details)
result.push(item.toJSON());
res.send(200, result);
});
// Wallet TXs within time range
this.get('/:id/tx/range', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const options = {
start: valid.u32('start'),
end: valid.u32('end'),
limit: valid.u32('limit'),
reverse: valid.bool('reverse')
};
const txs = await req.wallet.getRange(acct, options);
const details = await req.wallet.toDetails(txs);
const result = [];
for (const item of details)
result.push(item.toJSON());
res.send(200, result);
});
// Last Wallet TXs
this.get('/:id/tx/last', async (req, res) => {
const valid = req.valid();
const acct = valid.str('account');
const limit = valid.u32('limit');
const txs = await req.wallet.getLast(acct, limit);
const details = await req.wallet.toDetails(txs);
const result = [];
for (const item of details)
result.push(item.toJSON());
res.send(200, result);
});
// Wallet TX
this.get('/:id/tx/:hash', async (req, res) => {
const valid = req.valid();
const hash = valid.hash('hash');
enforce(hash, 'Hash is required.');
const tx = await req.wallet.getTX(hash);
if (!tx) {
res.send(404);
return;
}
const details = await req.wallet.toDetails(tx);
res.send(200, details.toJSON());
});
// Resend
this.post('/:id/resend', async (req, res) => {
await req.wallet.resend();
res.send(200, { success: true });
});
};
/**
* Initialize websockets.
* @private
*/
HTTPServer.prototype.initSockets = function initSockets() {
if (!this.io)
return;
this.on('socket', (socket) => {
this.handleSocket(socket);
});
this.walletdb.on('tx', (id, tx, details) => {
const json = details.toJSON();
this.to(`w:${id}`, 'wallet tx', json);
});
this.walletdb.on('confirmed', (id, tx, details) => {
const json = details.toJSON();
this.to(`w:${id}`, 'wallet confirmed', json);
});
this.walletdb.on('unconfirmed', (id, tx, details) => {
const json = details.toJSON();
this.to(`w:${id}`, 'wallet unconfirmed', json);
});
this.walletdb.on('conflict', (id, tx, details) => {
const json = details.toJSON();
this.to(`w:${id}`, 'wallet conflict', json);
});
this.walletdb.on('balance', (id, balance) => {
const json = balance.toJSON();
this.to(`w:${id}`, 'wallet balance', json);
});
this.walletdb.on('address', (id, receive) => {
const json = [];
for (const addr of receive)
json.push(addr.toJSON());
this.to(`w:${id}`, 'wallet address', json);
});
};
/**
* Handle new websocket.
* @private
* @param {WebSocket} socket
*/
HTTPServer.prototype.handleSocket = function handleSocket(socket) {
socket.hook('wallet auth', (args) => {
if (socket.auth)
throw new Error('Already authed.');
if (!this.options.noAuth) {
const valid = new Validator([args]);
const key = valid.str(0, '');
if (key.length > 255)
throw new Error('Invalid API key.');
const data = Buffer.from(key, 'utf8');
const hash = digest.hash256(data);
if (!ccmp(hash, this.options.apiHash))
throw new Error('Invalid API key.');
}
socket.auth = true;
this.logger.info('Successful auth from %s.', socket.host);
this.handleAuth(socket);
return null;
});
};
/**
* Handle new auth'd websocket.
* @private
* @param {WebSocket} socket
*/
HTTPServer.prototype.handleAuth = function handleAuth(socket) {
socket.hook('wallet join', async (args) => {
const valid = new Validator([args]);
const id = valid.str(0, '');
const token = valid.buf(1);
if (!id)
throw new Error('Invalid parameter.');
if (!this.options.walletAuth) {
socket.join(`w:${id}`);
return null;
}
if (!token)
throw new Error('Invalid parameter.');
let wallet;
try {
wallet = await this.walletdb.auth(id, token);
} catch (e) {
this.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.');
this.logger.info('Successful wallet auth for %s.', id);
socket.join(`w:${id}`);
return null;
});
socket.hook('wallet leave', (args) => {
const valid = new Validator([args]);
const id = valid.str(0, '');
if (!id)
throw new Error('Invalid parameter.');
socket.leave(`w:${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(random.randomBytes(20));
this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii'));
this.serviceHash = this.apiHash;
this.noAuth = false;
this.walletAuth = false;
this.prefix = null;
this.host = '127.0.0.1';
this.port = 8080;
this.ssl = false;
this.keyFile = null;
this.certFile = null;
this.fromOptions(options);
}
/**
* 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 <= 255,
'API key must be under 255 bytes.');
assert(util.isAscii(options.apiKey),
'API key must be ASCII.');
this.apiKey = options.apiKey;
this.apiHash = digest.hash256(Buffer.from(this.apiKey, 'ascii'));
}
if (options.noAuth != null) {
assert(typeof options.noAuth === 'boolean');
this.noAuth = options.noAuth;
}
if (options.walletAuth != null) {
assert(typeof options.walletAuth === 'boolean');
this.walletAuth = options.walletAuth;
}
if (options.prefix != null) {
assert(typeof options.prefix === 'string');
this.prefix = options.prefix;
this.keyFile = path.join(this.prefix, 'key.pem');
this.certFile = path.join(this.prefix, 'cert.pem');
}
if (options.host != null) {
assert(typeof options.host === 'string');
this.host = options.host;
}
if (options.port != null) {
assert(util.isU16(options.port), 'Port must be a number.');
this.port = options.port;
}
if (options.ssl != null) {
assert(typeof options.ssl === 'boolean');
this.ssl = options.ssl;
}
if (options.keyFile != null) {
assert(typeof options.keyFile === 'string');
this.keyFile = options.keyFile;
}
if (options.certFile != null) {
assert(typeof options.certFile === 'string');
this.certFile = options.certFile;
}
// Allow no-auth implicitly
// if we're listening locally.
if (!options.apiKey) {
if (this.host === '127.0.0.1' || this.host === '::1')
this.noAuth = true;
}
return this;
};
/**
* Instantiate http options from object.
* @param {Object} options
* @returns {HTTPOptions}
*/
HTTPOptions.fromOptions = function fromOptions(options) {
return new HTTPOptions().fromOptions(options);
};
/*
* Helpers
*/
function enforce(value, msg) {
if (!value) {
const err = new Error(msg);
err.statusCode = 400;
throw err;
}
}
/*
* Expose
*/
module.exports = HTTPServer;