import type { Address, BlockHash, BlockCountRPC, BlockInfoRPC, BlocksRPC, BlocksInfoRPC, RepresentativesRPC, RepresentativesOnlineRPC, RepresentativesOnlineWeightRPC, AccountHistoryRPC, AccountHistoryRawRPC, AccountInfoRPC, AccountBalanceRPC, AccountsBalancesRPC, AccountRepresentativeRPC, AccountsRepresentativesRPC, AccountWeightRPC, AccountReceivableRPC, AccountReceivableThresholdRPC, AccountReceivableSourceRPC, DelegatorsRPC, DelegatorsCountRPC } from "./rpc_types"; import { whole_to_raw } from "./util"; /** Implement this interface if the built-in RPC class does not fit your needs. The easiest way to do this is by just extending the built-in RPC class */ export interface RPCInterface { rpc_url: string; use_pending: boolean; DECIMALS?: number; call(payload: Record): Promise>; get_block_info(block_hash: BlockHash): Promise; get_account_info(account: Address, include_confirmed?: boolean, representative?: boolean, weight?: boolean, pending?: boolean): Promise; get_account_receivable(account: Address, count?: number, threshold?: `${number}`, source?: boolean): Promise; } /** Sends RPC requests to the RPC node, also has wrappers for actions that only read the network (write actions are handled by the Wallet class) */ export class RPC implements RPCInterface { rpc_url: string; use_pending: boolean; DECIMALS = undefined; //for nano, change to nano decimals debug: boolean = false; /** HTTP headers to send with any RPC requests, defaults to { "Content-Type": "application/json" } */ headers: Record | undefined; /** * @param {boolean} [use_pending = false] If true, uses "pending" instead of "receivable" in RPC action names, for compatibility with older versions of the node */ constructor(rpc_url: string, use_pending: boolean = false) { this.rpc_url = rpc_url; this.use_pending = use_pending; } //Network information related /** The function that sends the RPC POST request */ async call(payload: Record): Promise> { if (this.debug) console.log(JSON.stringify(payload)); const resp = await fetch(this.rpc_url, { method: "POST", headers: this.headers ?? { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!resp.ok && this.debug) console.log(await resp.text()); if (!resp.ok) throw Error(`Request to RPC node failed with status code ${resp.status}`); const resp_json = await resp.json(); if (resp_json.error) throw Error(`RPC node response: ${resp_json.error}`); return resp_json; } /** See https://docs.nano.org/commands/rpc-protocol/#block_count */ async get_block_count(): Promise { return (await this.call({ action: "block_count", })) as BlockCountRPC; } /** See https://docs.nano.org/commands/rpc-protocol/#block_info */ async get_block_info(block_hash: BlockHash): Promise { return (await this.call({ action: "block_info", hash: block_hash, json_block: true, })) as BlockInfoRPC; } /** See https://docs.nano.org/commands/rpc-protocol/#blocks */ async get_blocks(block_hashes: BlockHash[]): Promise { return (await this.call({ action: "blocks", hashes: block_hashes, json_block: true, })) as BlocksRPC; } /** https://docs.nano.org/commands/rpc-protocol/#blocks_info */ async get_blocks_info(block_hashes: BlockHash[]): Promise { return (await this.call({ action: "blocks_info", hashes: block_hashes, json_block: true, })) as BlocksInfoRPC; } /** https://docs.nano.org/commands/rpc-protocol/#representatives */ async get_representatives(): Promise { return (await this.call({ action: "representatives", })) as RepresentativesRPC; } /** https://docs.nano.org/commands/rpc-protocol/#representatives_online */ async get_representatives_online(weight?: T): Promise { return (await this.call({ action: "representatives_online", weight: weight ? "true" : undefined, //better not to include "weight" if false, rather than sending "weight": false })) as Promise; } //Account information related /** https://docs.nano.org/commands/rpc-protocol/#account_history */ async get_account_history(account: Address, count: number, raw?: boolean, head?: BlockHash, offset?: number, reverse?: boolean, account_filter?: Address[]): Promise { return (await this.call({ action: "account_history", account, count: `${count}`, raw: raw ? "true" : undefined, head, offset: offset ? `${offset}` : undefined, reverse: reverse ? "true" : undefined, account_filter, })) as Promise; } /** https://docs.nano.org/commands/rpc-protocol/#account_info */ async get_account_info(account: Address, include_confirmed?: boolean, representative?: boolean, weight?: boolean, pending?: boolean): Promise { return (await this.call({ action: "account_info", account, representative: representative ? "true" : undefined, weight: weight ? "true" : undefined, pending: pending ? "true" : undefined, })) as AccountInfoRPC; } /** https://docs.nano.org/commands/rpc-protocol/#account_balance */ async get_account_balance(account: Address): Promise { return (await this.call({ action: "account_balance", account, })) as AccountBalanceRPC; } /** https://docs.nano.org/commands/rpc-protocol/#accounts_balances */ async get_accounts_balances(accounts: Address[]): Promise { return (await this.call({ action: "accounts_balances", accounts, })) as AccountsBalancesRPC; } /** https://docs.nano.org/commands/rpc-protocol/#account_representative */ async get_account_representative(account: Address): Promise { return (await this.call({ action: "account_representative", account, })) as AccountRepresentativeRPC; } /** https://docs.nano.org/commands/rpc-protocol/#accounts_representatives */ async get_accounts_representatives(account: Address): Promise { return (await this.call({ action: "accounts_representatives", account, })) as AccountsRepresentativesRPC; } /** https://docs.nano.org/commands/rpc-protocol/#account_weight */ async get_account_weight(account: Address): Promise { return (await this.call({ action: "account_weight", account, })) as AccountWeightRPC; } //I hate nano node rpc here. I don't want to do the conditional type thing here, so have a union /** Keep in mind "threshold" parameter is in raw. https://docs.nano.org/commands/rpc-protocol/#receivable */ async get_account_receivable(account: Address, count?: number, threshold?: `${number}`, source?: boolean): Promise { return (await this.call({ action: this.use_pending ? "pending" : "receivable", account, count: count ? `${count}` : undefined, threshold: threshold ? whole_to_raw(threshold, this.DECIMALS).toString() : undefined, source: source ? "true" : undefined, })) as AccountReceivableRPC | AccountReceivableThresholdRPC | AccountReceivableSourceRPC; } /** Keep in mind "threshold" parameter is in raw. https://docs.nano.org/commands/rpc-protocol/#delegators */ async get_delegators(account: Address, threshold?: number, count?: number, start?: Address): Promise { return (await this.call({ action: "delegators", account, threshold: threshold ? `${threshold}` : undefined, count: count ? `${count}` : undefined, start, })) as DelegatorsRPC; } /** https://docs.nano.org/commands/rpc-protocol/#account_weight */ async get_delegators_count(account: Address): Promise { return (await this.call({ action: "account_weight", account, })) as DelegatorsCountRPC; } } export class RPCWithBackup extends RPC { readonly rpc_urls: string[]; readonly timeout: number; /** * @param {number} [timeout] Request to RPC timeout, in milliseconds. If RPC request fails or timeouts, tries the next RPC */ constructor(rpc_urls: string[], timeout: number, use_pending: boolean = false) { if (rpc_urls.length < 2) throw Error("Must provide at least two RPC URLs"); super(rpc_urls[0], use_pending); this.rpc_urls = rpc_urls; } async call(payload: Record): Promise> { let i = 0; while (true) { try { const resp = await fetch(this.rpc_urls[i], { method: "POST", headers: this.headers ?? { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(this.timeout), }); if (!resp.ok) throw Error(`Request to RPC node failed with status code ${resp.status}`); const resp_json = await resp.json(); if (resp_json.error) throw Error(`RPC node response: ${resp_json.error}`); return resp_json; } catch (e) { //increment (so try next RPC in provided list), if all RPCs exhausted (all failed), throw error if (!this.rpc_urls[++i]) throw Error(e); } } } }