import { randomBytes } from 'crypto';
import { WebSocket } from 'ws';
import { readFileSync } from 'fs';
import createRpcProxy from './rpc_proxy.js';
import { EventEmitter } from 'events';
import untildify from './untildify.js';
/** This can be found in the config but here for convenience. */
export let localDaemonConnection = {
host: 'localhost',
port: 55400,
key_path: '~/.chia/mainnet/config/ssl/daemon/private_daemon.key',
cert_path: '~/.chia/mainnet/config/ssl/daemon/private_daemon.crt',
timeout_seconds: 30,
};
/**
* This guy encapsulates asynchronous communication with the chia daemon
* which in turn proxies communication to the other chia services.
* @extends EventEmitter
*/
class ChiaDaemon extends EventEmitter {
/**
* Create a ChiaDaemon.
* @param {Object} connection - Details of the connection.
* @param {string} connection.host - The host name or IP address.
* @param {number} connection.port - The damon's listening port.
* @param {string} connection.key_path - File path to the certificate key file used to secure the connection.
* @param {string} connection.cert_path - File path to the certificate crt file used to secure the connection.
* @param {number} connection.timeout_seconds - Timeout, in seconds, for each call to the deamon.
* @param {string} service_name - the name of the client application or service talking to the daemon.
*/
constructor(connection, service_name = 'my_chia_app') {
super();
if (connection === undefined) {
throw new Error('Connection meta data must be provided');
}
this.connection = connection;
this.service_name = service_name;
this.outgoing = new Map(); // outgoing messages awaiting a response
this.incoming = new Map(); // incoming responses not yet consumed
}
/**
* Property with each of the rpc services exposed by the chia node.
* https://dkackman.github.io/chia-api/redoc/
* @return {Object} An object with each of the service endpoints.
*/
get services() {
return {
daemon: createRpcProxy(this, 'daemon'),
full_node: createRpcProxy(this, 'chia_full_node'),
wallet: createRpcProxy(this, 'chia_wallet'),
farmer: createRpcProxy(this, 'chia_farmer'),
harvester: createRpcProxy(this, 'chia_harvester'),
crawler: createRpcProxy(this, 'chia_crawler'),
simulator: createRpcProxy(this, 'chia_full_node_simulator'),
};
}
/**
* Opens the websocket connection and calls register_service on the daemon
* @fires ChiaDaemon#connecting
* @fires ChiaDaemon#connected
* @fires ChiaDaemon#event-message
* @fires ChiaDaemon#socket-error
* @fires ChiaDaemon#disconnected
* @returns {boolean} True if the socket is opened and service registered successfully. Otherwise false.
*/
async connect() {
if (this.ws !== undefined) {
throw new Error('Already connected');
}
const address = `wss://${this.connection.host}:${this.connection.port}`;
this.emit('connecting', address);
const ws = new WebSocket(address, {
rejectUnauthorized: false,
key: readFileSync(untildify(this.connection.key_path)),
cert: readFileSync(untildify(this.connection.cert_path)),
});
ws.on('open', () => {
const msg = formatMessage('daemon', 'register_service', this.service_name, { service: this.service_name });
ws.send(JSON.stringify(msg));
});
let connected = false;
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (this.outgoing.has(msg.request_id)) {
this.outgoing.delete(msg.request_id);
this.incoming.set(msg.request_id, msg);
} else if (msg.command === 'register_service') {
this.ws = ws;
this.emit('connected');
connected = true; // we consider ourselves connected only after register_service succeeds
} else {
// received a socket message that was not a response to something we sent
this.emit('event-message', msg);
}
});
ws.on('error', (e) => {
this.emit('socket-error', e);
});
ws.on('close', () => {
this.emit('disconnected');
});
const timeout_milliseconds = this.connection.timeout_seconds * 1000;
const timer = ms => new Promise(res => setTimeout(res, ms));
const start = Date.now();
// wait here until connected goes to true or we timeout
while (!connected) {
await timer(100);
const elapsed = Date.now() - start;
if (elapsed > timeout_milliseconds) {
this.emit('socket-error', new Error('Connection timeout expired'));
break;
}
}
return connected;
}
/** Closes the websocket and clears all state */
disconnect() {
if (this.ws === undefined) {
throw new Error('Not connected');
}
this.ws.close();
this.ws = undefined;
this.incoming.clear();
this.outgoing.clear();
}
/**
* Sends a command to the daemon. For the most part not needed in favor of the 'ChiaDaemon.services' endpoints.
* @param {string} destination - The destination service for the command. One of the known services like wallet or full_node
* @param {string} command - The command to send, i.e. the rpc endpoint such as get_blockchain_state.
* @param {Object} data - Any input arguments for the command. Omit if no rpc arguments are needed.
* @returns {*} Any response payload from the endpoint.
*/
async sendCommand(destination, command, data = {}) {
if (this.ws === undefined) {
throw new Error('Not connected');
}
const outgoingMsg = formatMessage(destination, command, this.service_name, data);
this.outgoing.set(outgoingMsg.request_id, outgoingMsg);
this.ws.send(JSON.stringify(outgoingMsg));
const timer = ms => new Promise(res => setTimeout(res, ms));
const start = Date.now();
// wait here until an incoming response shows up
while (!this.incoming.has(outgoingMsg.request_id)) {
await timer(100);
const elapsed = Date.now() - start;
if (elapsed / 1000 > this.connection.timeout_seconds) {
//clean up anything lingering for this message
if (this.outgoing.has(outgoingMsg.request_id)) {
this.outgoing.delete(outgoingMsg.request_id);
}
if (this.incoming.has(outgoingMsg.request_id)) {
this.incoming.delete(outgoingMsg.request_id);
}
throw new Error('Timeout expired');
}
}
const incomingMsg = this.incoming.get(outgoingMsg.request_id);
this.incoming.delete(outgoingMsg.request_id);
const incomingData = incomingMsg.data;
if (incomingData.success === false) {
throw new Error(incomingData.error);
}
return incomingData;
}
}
function formatMessage(destination, command, origin, data = {}) {
return {
command: command,
origin: origin,
destination: destination,
ack: false,
request_id: randomBytes(32).toString('hex'),
data: data,
};
}
const _Chia = ChiaDaemon;
export { _Chia as ChiaDaemon };
/**
* connecting event. Fires just before the WebSocket is created
*
* @event ChiaDaemon#connecting
* @property {string} address - the full address of the daemon websocket wss://host:port
* @type {object}
*/
/**
* connected event. Fires after the socket is opened and register_service returns
*
* @event ChiaDaemon#connected
* @type {object}
*/
/**
* event-message event. Fires when the daemon sends an event.
*
* @event ChiaDaemon#event-message
* @property {object} msg - The event message object.
* @type {object}
*/
/**
* socket-error event. Fires when the WebSocket reaises an error event.
*
* @event ChiaDaemon#socket-error
* @property {Error} e - The Error object rasied from the WebSocket.
*/
/**
* disconnected event. Fires after the WebSocket is closed.
*
* @event ChiaDaemon#disconnected
* @type {object}
*/