/*!
* nodeclient.js - node client for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const blacklist = require('bsock/lib/blacklist');
const AsyncEmitter = require('bevent');
/** @typedef {import('../node/node')} Node */
/**
* Node Client
* @alias module:node.NodeClient
*/
class NodeClient extends AsyncEmitter {
/** @type {Node} */
node;
/**
* Create a node client.
* @constructor
* @param {Node} node
*/
constructor(node) {
super();
this.node = node;
this.network = node.network;
this.filter = null;
this.opened = false;
this.hooks = new Map();
this.init();
}
/**
* Initialize the client.
*/
init() {
this.node.chain.on('connect', async (entry, block) => {
if (!this.opened)
return;
await this.emitAsync('block connect', entry, block.txs);
});
this.node.chain.on('disconnect', async (entry, block) => {
if (!this.opened)
return;
await this.emitAsync('block disconnect', entry);
});
this.node.on('tx', (tx) => {
if (!this.opened)
return;
this.emit('tx', tx);
});
this.node.on('reset', (tip) => {
if (!this.opened)
return;
this.emit('chain reset', tip);
});
}
/**
* Open the client.
* @returns {Promise}
*/
async open(options) {
assert(!this.opened, 'NodeClient is already open.');
this.opened = true;
setImmediate(() => this.emit('connect'));
}
/**
* Close the client.
* @returns {Promise}
*/
async close() {
assert(this.opened, 'NodeClient is not open.');
this.opened = false;
setImmediate(() => this.emit('disconnect'));
}
/**
* Add a listener.
* @param {String} type
* @param {Function} handler
*/
bind(type, handler) {
return this.on(type, handler);
}
/**
* Add a hook.
* @param {String} event
* @param {Function} handler
*/
hook(event, handler) {
assert(typeof event === 'string', 'Event must be a string.');
assert(typeof handler === 'function', 'Handler must be a function.');
assert(!this.hooks.has(event), 'Hook already bound.');
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
'Blacklisted event.');
this.hooks.set(event, handler);
}
/**
* Remove a hook.
* @param {String} event
*/
unhook(event) {
assert(typeof event === 'string', 'Event must be a string.');
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
'Blacklisted event.');
this.hooks.delete(event);
}
/**
* Call a hook.
* @param {String} event
* @param {...Object} args
* @returns {Promise}
*/
handleCall(event, ...args) {
const hook = this.hooks.get(event);
if (!hook)
throw new Error('No hook available.');
return hook(...args);
}
/**
* Get chain tip.
* @returns {Promise}
*/
async getTip() {
return this.node.chain.tip;
}
/**
* Get chain entry.
* @param {Hash} hash
* @returns {Promise}
*/
async getEntry(hash) {
const entry = await this.node.chain.getEntry(hash);
if (!entry)
return null;
if (!await this.node.chain.isMainChain(entry))
return null;
return entry;
}
/**
* Send a transaction. Do not wait for promise.
* @param {TX} tx
* @returns {Promise}
*/
async send(tx) {
this.node.relay(tx);
}
/**
* Send a claim. Do not wait for promise.
* @param {Claim} claim
* @returns {Promise}
*/
async sendClaim(claim) {
this.node.relayClaim(claim);
}
/**
* Set bloom filter.
* @param {Bloom} filter
* @returns {Promise}
*/
async setFilter(filter) {
this.filter = filter;
this.node.pool.setFilter(filter);
}
/**
* Add data to filter.
* @param {Buffer} data
* @returns {Promise}
*/
async addFilter(data) {
// `data` is ignored because pool.spvFilter === walletDB.filter
// and therefore is already updated.
// Argument is kept here to be consistent with API in
// wallet/client.js (client/node.js) and wallet/nullclient.js
this.node.pool.queueFilterLoad();
}
/**
* Reset filter.
* @returns {Promise}
*/
async resetFilter() {
this.node.pool.queueFilterLoad();
}
/**
* Esimate smart fee.
* @param {Number?} blocks
* @returns {Promise}
*/
async estimateFee(blocks) {
if (!this.node.fees)
return this.network.feeRate;
return this.node.fees.estimateFee(blocks);
}
/**
* Get hash range.
* @param {Number} start
* @param {Number} end
* @returns {Promise}
*/
async getHashes(start = -1, end = -1) {
return this.node.chain.getHashes(start, end);
}
/**
* Get entries range.
* @param {Number} start
* @param {Number} end
* @returns {Promise<ChainEntry[]>}
*/
async getEntries(start = -1, end = -1) {
return this.node.chain.getEntries(start, end);
}
/**
* Rescan for any missed transactions.
* @param {Number|Hash} start - Start block.
* @returns {Promise}
*/
async rescan(start) {
if (this.node.spv)
return this.node.chain.reset(start);
return this.node.chain.scan(start, this.filter, (entry, txs) => {
return this.handleCall('block rescan', entry, txs);
});
}
/**
* Rescan interactive for any missed transactions.
* @param {Number|Hash} start - Start block.
* @param {Boolean} [fullLock=false]
* @returns {Promise}
*/
async rescanInteractive(start, fullLock = true) {
if (this.node.spv)
return this.node.chain.reset(start);
const iter = async (entry, txs) => {
return await this.handleCall('block rescan interactive', entry, txs);
};
try {
return await this.node.scanInteractive(
start,
this.filter,
iter,
fullLock
);
} catch (e) {
await this.handleCall('block rescan interactive abort', e.message);
throw e;
}
}
/**
* Get name state.
* @param {Buffer} nameHash
* @returns {Object}
*/
async getNameStatus(nameHash) {
return this.node.getNameStatus(nameHash);
}
/**
* Get UTXO.
* @param {Hash} hash
* @param {Number} index
* @returns {Object}
*/
async getCoin(hash, index) {
return this.node.getCoin(hash, index);
}
/**
* Get block header.
* @param {Hash|Number} block
* @returns {Promise<ChainEntry>}
*/
async getBlockHeader(block) {
if (typeof block === 'string')
block = Buffer.from(block, 'hex');
const entry = await this.node.chain.getEntry(block);
if (!entry)
return null;
return entry.toJSON();
}
}
/*
* Expose
*/
module.exports = NodeClient;