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;