1101 lines
27 KiB
JavaScript
1101 lines
27 KiB
JavaScript
/*!
|
|
* server.js - http server for hsd
|
|
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
|
|
* https://github.com/handshake-org/hsd
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('bsert');
|
|
const path = require('path');
|
|
const {Server} = require('bweb');
|
|
const Validator = require('bval');
|
|
const base58 = require('bcrypto/lib/encoding/base58');
|
|
const {BloomFilter} = require('bfilter');
|
|
const sha256 = require('bcrypto/lib/sha256');
|
|
const random = require('bcrypto/lib/random');
|
|
const {safeEqual} = require('bcrypto/lib/safe');
|
|
const util = require('../utils/util');
|
|
const TX = require('../primitives/tx');
|
|
const Claim = require('../primitives/claim');
|
|
const Address = require('../primitives/address');
|
|
const Network = require('../protocol/network');
|
|
const scanActions = require('../blockchain/common').scanActions;
|
|
const pkg = require('../pkg');
|
|
|
|
/**
|
|
* HTTP
|
|
* @alias module:http.Server
|
|
*/
|
|
|
|
class HTTP extends Server {
|
|
/**
|
|
* Create an http server.
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(new HTTPOptions(options));
|
|
|
|
this.network = this.options.network;
|
|
this.logger = this.options.logger.context('node-http');
|
|
this.node = this.options.node;
|
|
|
|
this.chain = this.node.chain;
|
|
this.mempool = this.node.mempool;
|
|
this.pool = this.node.pool;
|
|
this.fees = this.node.fees;
|
|
this.miner = this.node.miner;
|
|
this.rpc = this.node.rpc;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize routes.
|
|
* @private
|
|
*/
|
|
|
|
init() {
|
|
this.on('request', (req, res) => {
|
|
if (req.method === 'POST' && req.pathname === '/')
|
|
return;
|
|
|
|
this.logger.debug('Request for method=%s path=%s (%s).',
|
|
req.method, req.pathname, req.socket.remoteAddress);
|
|
});
|
|
|
|
this.on('listening', (address) => {
|
|
this.logger.info('Node HTTP server listening on %s (port=%d).',
|
|
address.address, address.port);
|
|
});
|
|
|
|
this.initRouter();
|
|
this.initSockets();
|
|
}
|
|
|
|
/**
|
|
* Initialize routes.
|
|
* @private
|
|
*/
|
|
|
|
initRouter() {
|
|
if (this.options.cors)
|
|
this.use(this.cors());
|
|
|
|
if (!this.options.noAuth) {
|
|
this.use(this.basicAuth({
|
|
hash: sha256.digest,
|
|
password: this.options.apiKey,
|
|
realm: 'node'
|
|
}));
|
|
}
|
|
|
|
this.use(this.bodyParser({
|
|
type: 'json'
|
|
}));
|
|
|
|
this.use(this.jsonRPC());
|
|
this.use(this.router());
|
|
|
|
this.error((err, req, res) => {
|
|
const code = err.statusCode || 500;
|
|
res.json(code, {
|
|
error: {
|
|
type: err.type,
|
|
code: err.code,
|
|
message: err.message
|
|
}
|
|
});
|
|
});
|
|
|
|
this.get('/', async (req, res) => {
|
|
const totalTX = this.mempool ? this.mempool.map.size : 0;
|
|
const size = this.mempool ? this.mempool.getSize() : 0;
|
|
const claims = this.mempool ? this.mempool.claims.size : 0;
|
|
const airdrops = this.mempool ? this.mempool.airdrops.size : 0;
|
|
const orphans = this.mempool ? this.mempool.orphans.size : 0;
|
|
const brontide = this.pool.hosts.brontide;
|
|
|
|
const pub = {
|
|
listen: this.pool.options.listen,
|
|
host: null,
|
|
port: null,
|
|
brontidePort: null
|
|
};
|
|
|
|
const addr = this.pool.hosts.getLocal();
|
|
|
|
if (addr && pub.listen) {
|
|
pub.host = addr.host;
|
|
pub.port = addr.port;
|
|
pub.brontidePort = brontide.port;
|
|
}
|
|
|
|
const treeInterval = this.network.names.treeInterval;
|
|
const prevHeight = this.chain.height - 1;
|
|
const treeRootHeight = this.chain.height === 0 ? 0 :
|
|
prevHeight - (prevHeight % treeInterval) + 1;
|
|
|
|
const treeCompaction = {
|
|
compacted: false,
|
|
compactOnInit: false,
|
|
compactInterval: null,
|
|
lastCompaction: null,
|
|
nextCompaction: null
|
|
};
|
|
|
|
if (!this.chain.options.spv) {
|
|
const chainOptions = this.chain.options;
|
|
const {
|
|
compactionHeight,
|
|
compactFrom
|
|
} = await this.chain.getCompactionHeights();
|
|
|
|
treeCompaction.compactOnInit = chainOptions.compactTreeOnInit;
|
|
|
|
if (chainOptions.compactTreeOnInit) {
|
|
treeCompaction.compactInterval = chainOptions.compactTreeInitInterval;
|
|
treeCompaction.nextCompaction = compactFrom;
|
|
}
|
|
|
|
if (compactionHeight > 0) {
|
|
treeCompaction.compacted = true;
|
|
treeCompaction.lastCompaction = compactionHeight;
|
|
}
|
|
}
|
|
|
|
res.json(200, {
|
|
version: pkg.version,
|
|
network: this.network.type,
|
|
chain: {
|
|
height: this.chain.height,
|
|
tip: this.chain.tip.hash.toString('hex'),
|
|
treeRoot: this.chain.tip.treeRoot.toString('hex'),
|
|
treeRootHeight: treeRootHeight,
|
|
progress: this.chain.getProgress(),
|
|
indexers: {
|
|
indexTX: this.chain.options.indexTX,
|
|
indexAddress: this.chain.options.indexAddress
|
|
},
|
|
options: {
|
|
spv: this.chain.options.spv,
|
|
prune: this.chain.options.prune
|
|
},
|
|
treeCompaction: treeCompaction,
|
|
state: {
|
|
tx: this.chain.db.state.tx,
|
|
coin: this.chain.db.state.coin,
|
|
value: this.chain.db.state.value,
|
|
burned: this.chain.db.state.burned
|
|
}
|
|
},
|
|
pool: {
|
|
host: this.pool.options.host,
|
|
port: this.pool.options.port,
|
|
brontidePort: this.pool.options.brontidePort,
|
|
identitykey: brontide.getKey('base32'),
|
|
agent: this.pool.options.agent,
|
|
services: this.pool.options.services.toString(2),
|
|
outbound: this.pool.peers.outbound,
|
|
inbound: this.pool.peers.inbound,
|
|
public: pub
|
|
},
|
|
mempool: {
|
|
tx: totalTX,
|
|
size: size,
|
|
claims: claims,
|
|
airdrops: airdrops,
|
|
orphans: orphans
|
|
},
|
|
time: {
|
|
uptime: this.node.uptime(),
|
|
system: util.now(),
|
|
adjusted: this.network.now(),
|
|
offset: this.network.time.offset
|
|
},
|
|
memory: this.logger.memoryUsage()
|
|
});
|
|
});
|
|
|
|
// UTXO by address
|
|
this.get('/coin/address/:address', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const address = valid.str('address');
|
|
|
|
enforce(address, 'Address is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
|
|
|
|
const addr = Address.fromString(address, this.network);
|
|
const coins = await this.node.getCoinsByAddress(addr);
|
|
const result = [];
|
|
|
|
for (const coin of coins)
|
|
result.push(coin.getJSON(this.network));
|
|
|
|
res.json(200, result);
|
|
});
|
|
|
|
// UTXO by id
|
|
this.get('/coin/:hash/:index', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const hash = valid.bhash('hash');
|
|
const index = valid.u32('index');
|
|
|
|
enforce(hash, 'Hash is required.');
|
|
enforce(index != null, 'Index is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
|
|
|
|
const coin = await this.node.getCoin(hash, index);
|
|
|
|
if (!coin) {
|
|
res.json(404);
|
|
return;
|
|
}
|
|
|
|
res.json(200, coin.getJSON(this.network));
|
|
});
|
|
|
|
// Bulk read UTXOs
|
|
// TODO(boymanjor): Deprecate this endpoint
|
|
// once the equivalent functionality is included
|
|
// in the wallet API.
|
|
this.post('/coin/address', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const addresses = valid.array('addresses');
|
|
|
|
enforce(addresses, 'Addresses is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
|
|
|
|
this.logger.warning('%s %s %s',
|
|
'Warning: endpoint being considered for deprecation.',
|
|
'Known to cause CPU exhaustion if too many addresses',
|
|
'are queried or too many results are found.');
|
|
|
|
const addrs = [];
|
|
for (const address of addresses) {
|
|
addrs.push(Address.fromString(address, this.network));
|
|
}
|
|
|
|
const coins = await this.node.getCoinsByAddress(addrs);
|
|
const result = [];
|
|
|
|
for (const coin of coins)
|
|
result.push(coin.getJSON(this.network));
|
|
|
|
res.json(200, result);
|
|
});
|
|
|
|
// TX by hash
|
|
this.get('/tx/:hash', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const hash = valid.bhash('hash');
|
|
|
|
enforce(hash, 'Hash is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
|
|
|
|
const meta = await this.node.getMeta(hash);
|
|
|
|
if (!meta) {
|
|
res.json(404);
|
|
return;
|
|
}
|
|
|
|
const view = await this.node.getMetaView(meta);
|
|
|
|
res.json(200, meta.getJSON(this.network, view, this.chain.height));
|
|
});
|
|
|
|
// TX by address
|
|
this.get('/tx/address/:address', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const address = valid.str('address');
|
|
|
|
enforce(address, 'Address is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
|
|
|
|
const addr = Address.fromString(address, this.network);
|
|
const metas = await this.node.getMetaByAddress(addr);
|
|
const result = [];
|
|
|
|
for (const meta of metas) {
|
|
const view = await this.node.getMetaView(meta);
|
|
result.push(meta.getJSON(this.network, view, this.chain.height));
|
|
}
|
|
|
|
res.json(200, result);
|
|
});
|
|
|
|
// Bulk read TXs
|
|
// TODO(boymanjor): Deprecate this endpoint
|
|
// once the equivalent functionality is included
|
|
// in the wallet API.
|
|
this.post('/tx/address', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const addresses = valid.array('addresses');
|
|
|
|
enforce(addresses, 'Addresses is required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
|
|
|
|
this.logger.warning('%s %s %s',
|
|
'Warning: endpoint being considered for deprecation.',
|
|
'Known to cause CPU exhaustion if too many addresses',
|
|
'are queried or too many results are found.');
|
|
|
|
const addrs = [];
|
|
for (const address of addresses) {
|
|
addrs.push(Address.fromString(address, this.network));
|
|
}
|
|
|
|
const metas = await this.node.getMetaByAddress(addrs);
|
|
const result = [];
|
|
|
|
for (const meta of metas) {
|
|
const view = await this.node.getMetaView(meta);
|
|
result.push(meta.getJSON(this.network, view, this.chain.height));
|
|
}
|
|
|
|
res.json(200, result);
|
|
});
|
|
|
|
// Block by hash/height
|
|
this.get('/block/:block', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const hash = valid.uintbhash('block');
|
|
|
|
enforce(hash != null, 'Hash or height required.');
|
|
enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.');
|
|
|
|
const block = await this.chain.getBlock(hash);
|
|
|
|
if (!block) {
|
|
res.json(404);
|
|
return;
|
|
}
|
|
|
|
const view = await this.chain.getBlockView(block);
|
|
|
|
if (!view) {
|
|
res.json(404);
|
|
return;
|
|
}
|
|
|
|
const height = await this.chain.getHeight(hash);
|
|
const depth = this.chain.height - height + 1;
|
|
|
|
res.json(200, block.getJSON(this.network, view, height, depth));
|
|
});
|
|
|
|
// Block Header by hash/height
|
|
this.get('/header/:block', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const hash = valid.uintbhash('block');
|
|
|
|
enforce(hash != null, 'Hash or height required.');
|
|
|
|
const entry = await this.chain.getEntry(hash);
|
|
|
|
if (!entry) {
|
|
res.json(404);
|
|
return;
|
|
}
|
|
|
|
res.json(200, entry.toJSON());
|
|
});
|
|
|
|
// Mempool snapshot
|
|
this.get('/mempool', async (req, res) => {
|
|
enforce(this.mempool, 'No mempool available.');
|
|
|
|
const hashes = this.mempool.getSnapshot();
|
|
const result = [];
|
|
|
|
for (const hash of hashes)
|
|
result.push(hash.toString('hex'));
|
|
|
|
res.json(200, result);
|
|
});
|
|
|
|
// Mempool Rejection Filter
|
|
this.get('/mempool/invalid', async (req, res) => {
|
|
enforce(this.mempool, 'No mempool available.');
|
|
|
|
const valid = Validator.fromRequest(req);
|
|
const verbose = valid.bool('verbose', false);
|
|
|
|
const rejects = this.mempool.rejects;
|
|
res.json(200, {
|
|
items: rejects.items,
|
|
filter: verbose ? rejects.filter.toString('hex') : undefined,
|
|
size: rejects.size,
|
|
entries: rejects.entries,
|
|
n: rejects.n,
|
|
limit: rejects.limit,
|
|
tweak: rejects.tweak
|
|
});
|
|
});
|
|
|
|
// Mempool Rejection Test
|
|
this.get('/mempool/invalid/:hash', async (req, res) => {
|
|
enforce(this.mempool, 'No mempool available.');
|
|
|
|
const valid = Validator.fromRequest(req);
|
|
const hash = valid.bhash('hash');
|
|
|
|
enforce(hash, 'Must pass hash.');
|
|
|
|
const invalid = this.mempool.rejects.test(hash, 'hex');
|
|
|
|
res.json(200, { invalid });
|
|
});
|
|
|
|
// Broadcast TX
|
|
this.post('/broadcast', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const raw = valid.buf('tx');
|
|
|
|
enforce(raw, 'TX is required.');
|
|
|
|
const tx = TX.decode(raw);
|
|
|
|
await this.node.sendTX(tx);
|
|
|
|
res.json(200, { success: true });
|
|
});
|
|
|
|
// Broadcast Claim
|
|
this.post('/claim', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const raw = valid.buf('claim');
|
|
|
|
enforce(raw, 'Claim is required.');
|
|
|
|
const claim = Claim.decode(raw);
|
|
|
|
await this.node.sendClaim(claim);
|
|
|
|
res.json(200, { success: true });
|
|
});
|
|
|
|
// Estimate fee
|
|
this.get('/fee', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const blocks = valid.u32('blocks');
|
|
|
|
if (!this.fees) {
|
|
res.json(200, { rate: this.network.feeRate });
|
|
return;
|
|
}
|
|
|
|
const fee = this.fees.estimateFee(blocks);
|
|
|
|
res.json(200, { rate: fee });
|
|
});
|
|
|
|
// Reset chain
|
|
this.post('/reset', async (req, res) => {
|
|
const valid = Validator.fromRequest(req);
|
|
const height = valid.u32('height');
|
|
|
|
enforce(height != null, 'Height is required.');
|
|
enforce(height <= this.chain.height,
|
|
'Height cannot be greater than chain tip.');
|
|
|
|
await this.chain.reset(height);
|
|
|
|
res.json(200, { success: true });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle new websocket.
|
|
* @private
|
|
* @param {WebSocket} socket
|
|
*/
|
|
|
|
handleSocket(socket) {
|
|
socket.hook('auth', (...args) => {
|
|
if (socket.channel('auth'))
|
|
throw new Error('Already authed.');
|
|
|
|
if (!this.options.noAuth) {
|
|
const valid = new Validator(args);
|
|
const key = valid.str(0, '');
|
|
|
|
if (key.length > 255)
|
|
throw new Error('Invalid API key.');
|
|
|
|
const data = Buffer.from(key, 'ascii');
|
|
const hash = sha256.digest(data);
|
|
|
|
if (!safeEqual(hash, this.options.apiHash))
|
|
throw new Error('Invalid API key.');
|
|
}
|
|
|
|
socket.join('auth');
|
|
|
|
this.logger.info('Successful auth from %s.', socket.host);
|
|
this.handleAuth(socket);
|
|
|
|
return null;
|
|
});
|
|
|
|
socket.fire('version', {
|
|
version: pkg.version,
|
|
network: this.network.type
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle new auth'd websocket.
|
|
* @private
|
|
* @param {WebSocket} socket
|
|
*/
|
|
|
|
handleAuth(socket) {
|
|
socket.hook('watch chain', () => {
|
|
socket.join('chain');
|
|
return null;
|
|
});
|
|
|
|
socket.hook('unwatch chain', () => {
|
|
socket.leave('chain');
|
|
return null;
|
|
});
|
|
|
|
socket.hook('watch mempool', () => {
|
|
socket.join('mempool');
|
|
return null;
|
|
});
|
|
|
|
socket.hook('unwatch mempool', () => {
|
|
socket.leave('mempool');
|
|
return null;
|
|
});
|
|
|
|
socket.hook('set filter', (...args) => {
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
socket.filter = BloomFilter.decode(data);
|
|
|
|
return null;
|
|
});
|
|
|
|
socket.hook('get tip', () => {
|
|
return this.chain.tip.encode();
|
|
});
|
|
|
|
socket.hook('get entry', async (...args) => {
|
|
const valid = new Validator(args);
|
|
const block = valid.uintbhash(0);
|
|
|
|
if (block == null)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
const entry = await this.chain.getEntry(block);
|
|
|
|
if (!entry)
|
|
return null;
|
|
|
|
if (!await this.chain.isMainChain(entry))
|
|
return null;
|
|
|
|
return entry.encode();
|
|
});
|
|
|
|
socket.hook('get median time', async (...args) => {
|
|
const valid = new Validator(args);
|
|
const block = valid.uintbhash(0);
|
|
|
|
if (block == null)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
const entry = await this.chain.getEntry(block);
|
|
|
|
if (!entry)
|
|
return null;
|
|
|
|
const mtp = await this.chain.getMedianTime(entry);
|
|
|
|
return mtp;
|
|
});
|
|
|
|
socket.hook('get hashes', async (...args) => {
|
|
const valid = new Validator(args);
|
|
const start = valid.i32(0, -1);
|
|
const end = valid.i32(1, -1);
|
|
|
|
return this.chain.getHashes(start, end);
|
|
});
|
|
|
|
socket.hook('get entries', async (...args) => {
|
|
const valid = new Validator(args);
|
|
const start = valid.i32(0, -1);
|
|
const end = valid.i32(1, -1);
|
|
|
|
const entries = await this.chain.getEntries(start, end);
|
|
return entries.map(entry => entry.encode());
|
|
});
|
|
|
|
socket.hook('add filter', (...args) => {
|
|
const valid = new Validator(args);
|
|
const chunks = valid.array(0);
|
|
|
|
if (!chunks)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
if (!socket.filter)
|
|
throw new Error('No filter set.');
|
|
|
|
const items = new Validator(chunks);
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const data = items.buf(i);
|
|
|
|
if (!data)
|
|
throw new Error('Bad data chunk.');
|
|
|
|
socket.filter.add(data);
|
|
|
|
if (this.node.spv)
|
|
this.pool.watch(data);
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
socket.hook('reset filter', () => {
|
|
socket.filter = null;
|
|
return null;
|
|
});
|
|
|
|
socket.hook('estimate fee', (...args) => {
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0);
|
|
|
|
if (!this.fees)
|
|
return this.network.feeRate;
|
|
|
|
return this.fees.estimateFee(blocks);
|
|
});
|
|
|
|
socket.hook('send', (...args) => {
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
const tx = TX.decode(data);
|
|
|
|
this.node.relay(tx);
|
|
|
|
return null;
|
|
});
|
|
|
|
socket.hook('send claim', (...args) => {
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
const claim = Claim.decode(data);
|
|
|
|
this.node.relayClaim(claim);
|
|
|
|
return null;
|
|
});
|
|
|
|
socket.hook('get name', async (...args) => {
|
|
const valid = new Validator(args);
|
|
const nameHash = valid.bhash(0);
|
|
|
|
if (!nameHash)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
const ns = await this.node.getNameStatus(nameHash);
|
|
|
|
return ns.getJSON(this.chain.height + 1, this.network);
|
|
});
|
|
|
|
socket.hook('rescan', (...args) => {
|
|
const valid = new Validator(args);
|
|
const start = valid.uintbhash(0);
|
|
|
|
if (start == null)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
return this.scan(socket, start);
|
|
});
|
|
|
|
socket.hook('rescan interactive', (...args) => {
|
|
const valid = new Validator(args);
|
|
const start = valid.uintbhash(0);
|
|
const rawFilter = valid.buf(1);
|
|
const fullLock = valid.bool(2, false);
|
|
let filter = socket.filter;
|
|
|
|
if (start == null)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
if (rawFilter)
|
|
filter = BloomFilter.fromRaw(rawFilter);
|
|
|
|
return this.scanInteractive(socket, start, filter, fullLock);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bind to chain events.
|
|
* @private
|
|
*/
|
|
|
|
initSockets() {
|
|
const pool = this.mempool || this.pool;
|
|
|
|
this.chain.on('connect', (entry, block, view) => {
|
|
const sockets = this.channel('chain');
|
|
|
|
if (!sockets)
|
|
return;
|
|
|
|
const raw = entry.encode();
|
|
|
|
this.to('chain', 'chain connect', raw);
|
|
|
|
for (const socket of sockets) {
|
|
const txs = this.filterBlock(socket, block);
|
|
socket.fire('block connect', raw, txs);
|
|
}
|
|
});
|
|
|
|
this.chain.on('disconnect', (entry, block, view) => {
|
|
const sockets = this.channel('chain');
|
|
|
|
if (!sockets)
|
|
return;
|
|
|
|
const raw = entry.encode();
|
|
|
|
this.to('chain', 'chain disconnect', raw);
|
|
this.to('chain', 'block disconnect', raw);
|
|
});
|
|
|
|
this.chain.on('reset', (tip) => {
|
|
const sockets = this.channel('chain');
|
|
|
|
if (!sockets)
|
|
return;
|
|
|
|
this.to('chain', 'chain reset', tip.encode());
|
|
});
|
|
|
|
pool.on('tx', (tx) => {
|
|
const sockets = this.channel('mempool');
|
|
|
|
if (!sockets)
|
|
return;
|
|
|
|
const raw = tx.encode();
|
|
|
|
for (const socket of sockets) {
|
|
if (!this.filterTX(socket, tx))
|
|
continue;
|
|
|
|
socket.fire('tx', raw);
|
|
}
|
|
});
|
|
|
|
this.chain.on('tree commit', (root, entry, block) => {
|
|
const sockets = this.channel('chain');
|
|
|
|
if (!sockets)
|
|
return;
|
|
|
|
this.to('chain', 'tree commit', root, entry, block);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Filter block by socket.
|
|
* @private
|
|
* @param {WebSocket} socket
|
|
* @param {Block} block
|
|
* @returns {TX[]}
|
|
*/
|
|
|
|
filterBlock(socket, block) {
|
|
if (!socket.filter)
|
|
return [];
|
|
|
|
const txs = [];
|
|
|
|
for (const tx of block.txs) {
|
|
if (this.filterTX(socket, tx))
|
|
txs.push(tx.encode());
|
|
}
|
|
|
|
return txs;
|
|
}
|
|
|
|
/**
|
|
* Filter transaction by socket.
|
|
* @private
|
|
* @param {WebSocket} socket
|
|
* @param {TX} tx
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
filterTX(socket, tx) {
|
|
if (!socket.filter)
|
|
return false;
|
|
|
|
return tx.testAndMaybeUpdate(socket.filter);
|
|
}
|
|
|
|
/**
|
|
* Scan using a socket's filter.
|
|
* @private
|
|
* @param {WebSocket} socket
|
|
* @param {Hash} start
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async scan(socket, start) {
|
|
await this.node.scan(start, socket.filter, (entry, txs) => {
|
|
const block = entry.encode();
|
|
const raw = [];
|
|
|
|
for (const tx of txs)
|
|
raw.push(tx.encode());
|
|
|
|
return socket.call('block rescan', block, raw);
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Scan using a socket's filter (interactive).
|
|
* @param {WebSocket} socket
|
|
* @param {Hash} start
|
|
* @param {BloomFilter} filter
|
|
* @param {Boolean} [fullLock=false]
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async scanInteractive(socket, start, filter, fullLock = false) {
|
|
const iter = async (entry, txs) => {
|
|
const block = entry.encode();
|
|
const raw = [];
|
|
|
|
for (const tx of txs)
|
|
raw.push(tx.encode());
|
|
|
|
const action = await socket.call('block rescan interactive', block, raw);
|
|
const valid = new Validator(action);
|
|
const actionType = valid.i32('type');
|
|
|
|
switch (actionType) {
|
|
case scanActions.NEXT:
|
|
case scanActions.ABORT:
|
|
case scanActions.REPEAT: {
|
|
return {
|
|
type: actionType
|
|
};
|
|
}
|
|
case scanActions.REPEAT_SET: {
|
|
// NOTE: This is operation is on the heavier side,
|
|
// because it sends the whole Filter that can be quite
|
|
// big depending on the situation.
|
|
// NOTE: In HTTP Context REPEAT_SET wont modify socket.filter
|
|
// but instead setup new one for the rescan. Further REPEAT_ADDs will
|
|
// modify this filter instead of the socket.filter.
|
|
const rawFilter = valid.buf('filter');
|
|
let filter = null;
|
|
|
|
if (rawFilter != null)
|
|
filter = BloomFilter.fromRaw(rawFilter);
|
|
|
|
return {
|
|
type: scanActions.REPEAT_SET,
|
|
filter: filter
|
|
};
|
|
}
|
|
case scanActions.REPEAT_ADD: {
|
|
// NOTE: This operation depending on the filter
|
|
// that was provided can be either modifying the
|
|
// socket.filter or the filter provided by REPEAT_SET.
|
|
const chunks = valid.array('chunks');
|
|
|
|
if (!chunks)
|
|
throw new Error('Invalid parameter.');
|
|
|
|
return {
|
|
type: scanActions.REPEAT_ADD,
|
|
chunks: chunks
|
|
};
|
|
}
|
|
|
|
default:
|
|
throw new Error('Unknown action.');
|
|
}
|
|
};
|
|
|
|
try {
|
|
await this.node.scanInteractive(start, filter, iter, fullLock);
|
|
} catch (err) {
|
|
await socket.call('block rescan interactive abort', err.message);
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
class HTTPOptions {
|
|
/**
|
|
* HTTPOptions
|
|
* @alias module:http.HTTPOptions
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
this.network = Network.primary;
|
|
this.logger = null;
|
|
this.node = null;
|
|
this.apiKey = base58.encode(random.randomBytes(20));
|
|
this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
|
|
this.noAuth = false;
|
|
this.cors = 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}
|
|
*/
|
|
|
|
fromOptions(options) {
|
|
assert(options);
|
|
assert(options.node && typeof options.node === 'object',
|
|
'HTTP Server requires a Node.');
|
|
|
|
this.node = options.node;
|
|
this.network = options.node.network;
|
|
this.logger = options.node.logger;
|
|
|
|
this.port = this.network.rpcPort;
|
|
|
|
if (options.logger != null) {
|
|
assert(typeof options.logger === 'object');
|
|
this.logger = options.logger;
|
|
}
|
|
|
|
if (options.apiKey != null) {
|
|
assert(typeof options.apiKey === 'string',
|
|
'API key must be a string.');
|
|
assert(options.apiKey.length <= 255,
|
|
'API key must be under 256 bytes.');
|
|
this.apiKey = options.apiKey;
|
|
this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii'));
|
|
}
|
|
|
|
if (options.noAuth != null) {
|
|
assert(typeof options.noAuth === 'boolean');
|
|
this.noAuth = options.noAuth;
|
|
}
|
|
|
|
if (options.cors != null) {
|
|
assert(typeof options.cors === 'boolean');
|
|
this.cors = options.cors;
|
|
}
|
|
|
|
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((options.port & 0xffff) === 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.host === 'localhost')
|
|
this.noAuth = true;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Instantiate http options from object.
|
|
* @param {Object} options
|
|
* @returns {HTTPOptions}
|
|
*/
|
|
|
|
static 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 = HTTP;
|