Interactive rescan by default does per block scan lock. This enables parallel rescans, as well as chain sync while rescan is in progress. But in specific cases, it may be more beneficial to stop the node from syncing while the rescan is in progress.
1076 lines
27 KiB
JavaScript
1076 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('@handshake-org/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 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('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) {
|
|
return socket.call('block rescan interactive abort', err.message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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;
|