chia-agent
Version:
chia rpc/websocket client library
354 lines (353 loc) • 16.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RPCAgent = void 0;
exports.getConnectionInfoFromConfig = getConnectionInfoFromConfig;
exports.getConf = getConf;
exports.loadCertFilesFromConfig = loadCertFilesFromConfig;
const https_1 = require("https");
const http_1 = require("http");
const fs_1 = require("fs");
const JSONbigBuilder = require("@chiamine/json-bigint");
const logger_1 = require("../logger");
const index_1 = require("../config/index");
const JSONbig = JSONbigBuilder({
useNativeBigInt: true,
alwaysParseAsBig: false,
});
function getConnectionInfoFromConfig(destination, config) {
let hostname = "localhost";
let port = -1;
if (destination === "daemon") {
port = +config["/daemon_port"];
}
else if (destination === "farmer") {
port = +config["/farmer/rpc_port"];
}
else if (destination === "harvester") {
port = +config["/harvester/rpc_port"];
}
else if (destination === "full_node") {
port = +config["/full_node/rpc_port"];
}
else if (destination === "wallet") {
port = +config["/wallet/rpc_port"];
}
else if (destination === "data_layer") {
port = +config["/data_layer/rpc_port"];
}
else if (destination === "pool") {
const poolUrl = config["/pool/pool_list/0/pool_url"];
const regex = /^(https?:\/\/)?([^/:]+):?(\d*)/;
const match = regex.exec(poolUrl);
if (match) {
hostname = match[2];
port = match[3] ? +match[3] : 80;
}
else {
(0, logger_1.getLogger)().error("Pool list was not found in config.yaml.");
throw new Error("Pool list was not found in config.yaml");
}
}
else {
throw new Error(`Unknown destination: ${destination}`);
}
return { hostname, port };
}
function getConf(configPath) {
configPath = configPath || index_1.configPath;
if (!(0, fs_1.existsSync)(configPath)) {
(0, logger_1.getLogger)().error(`chia config file does not exist at: ${configPath}`);
throw new Error("chia config file Not Found.");
}
return (0, index_1.getConfig)(configPath);
}
function loadCertFilesFromConfig(config) {
const clientCertPath = (0, index_1.resolveFromChiaRoot)([
config["/daemon_ssl/private_crt"],
]);
const clientKeyPath = (0, index_1.resolveFromChiaRoot)([
config["/daemon_ssl/private_key"],
]);
const caCertPath = (0, index_1.resolveFromChiaRoot)([
config["/private_ssl_ca/crt"],
]);
(0, logger_1.getLogger)().debug(() => `Loading client cert file from ${clientCertPath}`);
(0, logger_1.getLogger)().debug(() => `Loading client key file from ${clientKeyPath}`);
(0, logger_1.getLogger)().debug(() => `Loading ca cert file from ${caCertPath}`);
const getCertOrKey = (path) => {
if (!(0, fs_1.existsSync)(path)) {
(0, logger_1.getLogger)().error(`ssl crt/key does not exist at: ${path}`);
throw new Error(`crt/key Not Found at ${path}`);
}
return (0, fs_1.readFileSync)(path);
};
const clientCert = getCertOrKey(clientCertPath);
const clientKey = getCertOrKey(clientKeyPath);
const caCert = getCertOrKey(caCertPath);
return { clientCert, clientKey, caCert };
}
const userAgent = "chia-agent/1.0.0";
class RPCAgent {
constructor(props) {
this._skip_hostname_verification = false;
this._host = "";
this._port = 0;
if ("httpsAgent" in props) {
this._protocol = "https";
this._agent = props.httpsAgent;
this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
// Extract host/port from httpsAgent options
// Note: TypeScript doesn't expose options property, but it exists at runtime
const agent = this._agent;
if (agent.options && agent.options.host && agent.options.port) {
this._host = agent.options.host;
this._port = agent.options.port;
(0, logger_1.getLogger)().debug(() => `Constructing RPCAgent with httpsAgent: ${this._host}:${this._port}`);
}
else {
(0, logger_1.getLogger)().debug(() => "Constructing RPCAgent with httpsAgent (host/port not available in agent options)");
}
}
else if ("httpAgent" in props) {
this._protocol = "http";
this._agent = props.httpAgent;
this._host = props.host;
this._port = props.port;
this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
(0, logger_1.getLogger)().debug(() => `Constructing RPCAgent with httpAgent: ${this._host}:${this._port}`);
}
else if ("protocol" in props) {
this._protocol = props.protocol;
const { host, port } = props;
let clientCert;
let clientKey;
let caCert;
const keepAlive = props.keepAlive !== false;
const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
? props.keepAliveMsecs
: 1000;
const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
? props.maxSockets
: Infinity;
const timeout = typeof props.timeout === "number" && props.timeout > 0
? props.timeout
: undefined;
this._host = host;
this._port = port;
if (props.protocol === "https") {
if ("configPath" in props) {
const config = getConf(props.configPath);
const certs = loadCertFilesFromConfig(config);
clientCert = certs.clientCert;
clientKey = certs.clientKey;
caCert = certs.caCert;
this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
}
else {
({
client_cert: clientCert,
client_key: clientKey,
ca_cert: caCert,
} = props);
this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
}
this._agent = new https_1.Agent({
host,
port,
ca: caCert,
cert: clientCert,
key: clientKey,
rejectUnauthorized: Boolean(caCert) && host !== "localhost",
keepAlive,
keepAliveMsecs,
maxSockets,
timeout,
});
(0, logger_1.getLogger)().debug(() => `Constructed RPCAgent with httpsAgent: ${host}:${port}`);
}
else {
this._agent = new http_1.Agent({
keepAlive,
keepAliveMsecs,
maxSockets,
timeout,
});
}
}
else {
this._protocol = "https";
let host;
let port;
const keepAlive = props.keepAlive !== false;
const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
? props.keepAliveMsecs
: 1000;
const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
? props.maxSockets
: Infinity;
const timeout = typeof props.timeout === "number" && props.timeout > 0
? props.timeout
: undefined;
const config = getConf("configPath" in props ? props.configPath : undefined);
if (props.host && typeof props.port === "number") {
({ host, port } = props);
}
else {
const info = getConnectionInfoFromConfig(props.service, config);
host = props.host ? props.host : info.hostname;
port = typeof props.port === "number" ? props.port : info.port;
}
(0, logger_1.getLogger)().debug(() => `Picked ${host}:${port} for ${props.service}`);
const certs = loadCertFilesFromConfig(config);
const { clientCert, clientKey, caCert } = certs;
this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
this._host = host;
this._port = port;
this._agent = new https_1.Agent({
host: host,
port: port,
ca: caCert,
cert: clientCert,
key: clientKey,
rejectUnauthorized: Boolean(caCert) && host !== "localhost",
keepAlive,
keepAliveMsecs,
maxSockets,
timeout,
});
}
}
async sendMessage(destination, command, data) {
// parameter `destination` is not used because target rpc server is determined by url.
(0, logger_1.getLogger)().debug(() => `Sending RPC message. dest=${destination} command=${command}`);
return this.request("POST", command, data);
}
async request(method, path, data) {
return new Promise((resolve, reject) => {
const body = data ? JSONbig.stringify(data) : "{}";
const pathname = `/${path.replace(/^\/+/, "")}`;
const METHOD = method.toUpperCase();
const headers = {
Accept: "application/json, text/plain, */*",
"User-Agent": userAgent,
};
if (this._host) {
headers.Host = this._host;
}
const options = {
path: pathname,
method: METHOD,
agent: this._agent,
headers,
};
// For HTTP protocol, we need to explicitly set hostname and port
if (this._protocol === "http") {
options.hostname = this._host;
options.port = this._port;
}
if (this._skip_hostname_verification) {
options.checkServerIdentity = () => {
return undefined;
};
}
if (METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") {
options.headers = {
...(options.headers || {}),
"Content-Type": "application/json;charset=utf-8",
"Content-Length": body.length,
};
}
else if (METHOD === "GET") {
// Add query string if `data` is object.
if (data && typeof data === "object") {
// Remove string after '?' on path to prevent duplication.
let p = options.path;
if (/\?/.test(p)) {
(0, logger_1.getLogger)().warning("querystring in `path` is replaced by `data`");
p.replace(/\?.*/, "");
}
p += "?";
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
p += `${key}=${data[key]}`;
}
}
options.path = p;
}
}
const transporter = this._protocol === "https" ? https_1.request : http_1.request;
(0, logger_1.getLogger)().debug(() => `Dispatching RPC ${METHOD} request to ${this._protocol}//${this._host}:${this._port}${options.path}`);
(0, logger_1.getLogger)().trace(() => `Request options: ${(0, logger_1.stringify)(options)}`);
(0, logger_1.getLogger)().trace(() => `Request body: ${body}`);
const req = transporter(options, (res) => {
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
(0, logger_1.getLogger)().error(`Status not ok: ${res.statusCode}`);
if (res.statusCode === 404) {
(0, logger_1.getLogger)().error("Maybe the RPCAgent is connecting to different service against target rpc command.");
(0, logger_1.getLogger)().error("For example, this happens when invoking 'new_farming_info' rpc command" +
" to 'full_node' service, which 'farm' service is correct");
(0, logger_1.getLogger)().error("Check invoking command is correct and connecting service/host is right for the command");
}
return reject(new Error(`Status not ok: ${res.statusCode}`));
}
const chunks = [];
res.on("data", (chunk) => {
chunks.push(chunk);
if (chunks.length === 0) {
(0, logger_1.getLogger)().debug(() => "The first response chunk data arrived");
}
(0, logger_1.getLogger)().trace(() => `Response chunk #${chunks.length} - ${chunk.length} bytes: ${chunk.toString()}`);
});
res.on("end", () => {
try {
if (chunks.length > 0) {
const entireChunks = Buffer.concat(chunks);
const serializedData = entireChunks.toString();
const d = JSONbig.parse(serializedData);
if (typeof d !== "object" || !d) {
(0, logger_1.getLogger)().error(`Response is expected to be an object but received: ${serializedData}`);
return reject(new Error(`Unexpected response format: ${serializedData}`));
}
else if (!Object.prototype.hasOwnProperty.call(d, "success")) {
(0, logger_1.getLogger)().error("Response has no 'success' property");
return reject(new Error(`Response has no 'success' property: ${serializedData}`));
}
if (!d.success) {
(0, logger_1.getLogger)().info(`API failure: ${d.error}`);
return reject(d);
}
(0, logger_1.getLogger)().debug(() => `RPC response received from ${this._protocol}//${this._host}:${this._port}${options.path}`);
(0, logger_1.getLogger)().trace(() => `RPC response data: ${(0, logger_1.stringify)(d)}`);
return resolve(d);
}
// RPC Server should return response like
// {origin: string; destination: string; request_id: string; data: any; ...}
// If no such response is returned, reject it.
(0, logger_1.getLogger)().error("RPC Server returned no data. This is not expected.");
reject(new Error("Server responded without expected data"));
}
catch (e) {
(0, logger_1.getLogger)().error(`Failed to parse response data: ${(0, logger_1.stringify)(e)}`);
try {
(0, logger_1.getLogger)().error(Buffer.concat(chunks).toString());
}
catch (_e2) {
/* Do nothing */
}
reject(new Error("Server responded without expected data"));
}
});
});
req.on("error", (error) => {
(0, logger_1.getLogger)().error(() => (0, logger_1.stringify)(error));
reject(error);
});
if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") &&
body) {
req.write(body);
}
req.end();
});
}
}
exports.RPCAgent = RPCAgent;