From e1908f9ea05e309c7f1d260ecc18584503155cb4 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Tue, 5 Sep 2023 12:37:18 +0100 Subject: [PATCH] feat!: change rpc methods signature to support fetch options Signal are now supported --- packages/iso-filecoin/package.json | 3 +- packages/iso-filecoin/src/message.js | 2 +- packages/iso-filecoin/src/rpc.js | 157 +++++++++++++++++-------- packages/iso-filecoin/src/types.ts | 135 +++++++++++++-------- packages/iso-filecoin/test/rpc.test.js | 92 +++++++++++++-- packages/iso-filecoin/tsconfig.json | 3 + 6 files changed, 284 insertions(+), 108 deletions(-) diff --git a/packages/iso-filecoin/package.json b/packages/iso-filecoin/package.json index 718fc90..240f2bc 100644 --- a/packages/iso-filecoin/package.json +++ b/packages/iso-filecoin/package.json @@ -99,6 +99,7 @@ "@scure/bip39": "^1.2.1", "bignumber.js": "^9.1.1", "iso-base": "workspace:^", + "iso-web": "workspace:^", "zod": "^3.21.4" }, "devDependencies": { @@ -108,7 +109,7 @@ "assert": "^2.0.0", "hd-scripts": "^7.0.0", "mocha": "^10.2.0", - "playwright-test": "^12.1.1", + "playwright-test": "^12.2.0", "typescript": "5.1.6" }, "publishConfig": { diff --git a/packages/iso-filecoin/src/message.js b/packages/iso-filecoin/src/message.js index 5a24983..7efab4e 100644 --- a/packages/iso-filecoin/src/message.js +++ b/packages/iso-filecoin/src/message.js @@ -110,7 +110,7 @@ export class Message { (this.gasLimit === 0 && this.gasFeeCap === '0') || this.gasPremium === '0' ) { - const gas = await rpc.gasEstimate(this) + const gas = await rpc.gasEstimate({ msg: this }) if (gas.error) { throw new Error(gas.error.message) diff --git a/packages/iso-filecoin/src/rpc.js b/packages/iso-filecoin/src/rpc.js index 5956cae..9adc2b7 100644 --- a/packages/iso-filecoin/src/rpc.js +++ b/packages/iso-filecoin/src/rpc.js @@ -5,13 +5,17 @@ import { getNetworkPrefix } from './utils.js' export class RPC { /** * @param {import("./types.js").Options} options + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - constructor({ - api, - token, - network = 'mainnet', - fetch = globalThis.fetch.bind(globalThis), - }) { + constructor( + { + api, + token, + network = 'mainnet', + fetch = globalThis.fetch.bind(globalThis), + }, + fetchOptions = {} + ) { this.fetch = fetch this.api = new URL(api) this.network = network @@ -19,17 +23,30 @@ export class RPC { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), } + + this.fetchOptions = fetchOptions } - async version() { - return /** @type {import("./types.js").VersionResponse} */ ( - await this.call('Filecoin.Version') + /** + * Version returns the version of the Filecoin node. + * + * @param {import('./types.js').FetchOptions} [fetchOptions] + */ + async version(fetchOptions = {}) { + return /** @type {import('./types.js').VersionResponse} */ ( + await this.call({ method: 'Filecoin.Version' }, fetchOptions) ) } - async networkName() { + /** + * NetworkName returns the name of the network the node is synced to. + * + * @param {import('./types.js').FetchOptions} [fetchOptions] + * @returns + */ + async networkName(fetchOptions = {}) { return /** @type {import("./types.js").StateNetworkNameResponse} */ ( - await this.call('Filecoin.StateNetworkName') + await this.call({ method: 'Filecoin.StateNetworkName' }, fetchOptions) ) } @@ -37,20 +54,25 @@ export class RPC { * GasEstimateMessageGas estimates gas values for unset message gas fields * * @see https://lotus.filecoin.io/reference/lotus/gas/#gasestimatemessagegas - * @param {import("./types.js").PartialMessageObj} msg - * @param {string} maxFee - max fee to pay for gas (attoFIL/gas units) + * + * @param {import('./types.js').GasEstimateParams} params + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - async gasEstimate(msg, maxFee = '0') { - this.#validateNetwork(msg.from) - this.#validateNetwork(msg.to) + async gasEstimate(params, fetchOptions = {}) { + this.#validateNetwork(params.msg.from) + this.#validateNetwork(params.msg.to) return /** @type {import("./types.js").GasEstimateMessageGasResponse} */ ( await this.call( - 'Filecoin.GasEstimateMessageGas', - new Message(msg).toLotus(), - { MaxFee: maxFee }, - // eslint-disable-next-line unicorn/no-useless-undefined - undefined + { + method: 'Filecoin.GasEstimateMessageGas', + params: [ + new Message(params.msg).toLotus(), + { MaxFee: params.maxFee ?? '0' }, + undefined, + ], + }, + fetchOptions ) ) } @@ -59,12 +81,17 @@ export class RPC { * WalletBalance returns the balance of the given address at the current head of the chain. * * @see https://lotus.filecoin.io/reference/lotus/wallet/#walletbalance + * * @param {string} address + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - async balance(address) { + async balance(address, fetchOptions = {}) { address = this.#validateNetwork(address) return /** @type {import("./types.js").WalletBalanceResponse} */ ( - await this.call('Filecoin.WalletBalance', address) + await this.call( + { method: 'Filecoin.WalletBalance', params: [address] }, + fetchOptions + ) ) } @@ -73,11 +100,15 @@ export class RPC { * * @see https://lotus.filecoin.io/reference/lotus/mpool/#mpoolgetnonce * @param {string} address + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - async nonce(address) { + async nonce(address, fetchOptions = {}) { address = this.#validateNetwork(address) return /** @type {import("./types.js").MpoolGetNonceResponse} */ ( - await this.call('Filecoin.MpoolGetNonce', address) + await this.call( + { method: 'Filecoin.MpoolGetNonce', params: [address] }, + fetchOptions + ) ) } @@ -85,36 +116,50 @@ export class RPC { * MpoolPush pushes a signed message to mempool. * * @see https://lotus.filecoin.io/reference/lotus/mpool/#mpoolpush - * @param {import('./types.js').MessageObj} msg - * @param {import('./types.js').SignatureObj} signature + * + * @param {import('./types.js').PushMessageParams} params + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - async pushMessage(msg, signature) { - this.#validateNetwork(msg.from) - this.#validateNetwork(msg.to) + async pushMessage(params, fetchOptions = {}) { + this.#validateNetwork(params.msg.from) + this.#validateNetwork(params.msg.to) return /** @type {import("./types.js").MpoolPushResponse} */ ( - await this.call('Filecoin.MpoolPush', { - Message: new Message(msg).toLotus(), - Signature: new Signature(signature).toLotus(), - }) + await this.call( + { + method: 'Filecoin.MpoolPush', + params: [ + { + Message: new Message(params.msg).toLotus(), + Signature: new Signature(params.signature).toLotus(), + }, + ], + }, + fetchOptions + ) ) } /** * StateWaitMsg looks back in the chain for a message. If not found, it blocks until the message arrives on chain, and gets to the indicated confidence depth. * - * @param {{ "/": string }} cid - * @param {number} confidence - * @param {number} lookBackLimit + * @see https://lotus.filecoin.io/reference/lotus/state/#statewaitmsg + * @param {import('./types.js').waitMsgParams} params + * @param {import('./types.js').FetchOptions} [fetchOptions] */ - async stateWaitMsg(cid, confidence = 2, lookBackLimit = 100) { - return /** @type {any} */ ( + async waitMsg(params, fetchOptions = {}) { + return /** @type {import('./types.js').WaitMsgResponse} */ ( await this.call( - 'Filecoin.StateWaitMsg', - cid, - confidence, - lookBackLimit, - false + { + method: 'Filecoin.StateWaitMsg', + params: [ + params.cid, + params.confidence ?? 2, + params.lookback ?? 100, + false, + ], + }, + fetchOptions ) ) } @@ -123,20 +168,27 @@ export class RPC { * Generic method to call any method on the lotus rpc api. * * @template R - * @param {string} method - * @param {any[]} params + * @param {import('./types.js').RpcOptions} rpcOptions + * @param {import('./types.js').FetchOptions} [fetchOptions] + * @returns {Promise} */ - async call(method, ...params) { + + async call(rpcOptions, fetchOptions = {}) { + const opts = { + ...this.fetchOptions, + ...fetchOptions, + } try { const res = await this.fetch(this.api, { method: 'POST', headers: this.headers, body: JSON.stringify({ jsonrpc: '2.0', - method, - params, + method: rpcOptions.method, + params: rpcOptions.params, id: 1, }), + signal: opts.signal ?? AbortSignal.timeout(opts.timeout ?? 5000), }) if (res.ok) { @@ -187,6 +239,13 @@ export class RPC { }, }) } + + return /** @type {import("./types.js").RpcError} */ ({ + error: { + code: 0, + message: `ERROR: unknown error`, + }, + }) } /** diff --git a/packages/iso-filecoin/src/types.ts b/packages/iso-filecoin/src/types.ts index 5bda074..41f8d37 100644 --- a/packages/iso-filecoin/src/types.ts +++ b/packages/iso-filecoin/src/types.ts @@ -10,6 +10,10 @@ import type { z } from 'zod' export type ProtocolIndicator = typeof PROTOCOL_INDICATOR export type ProtocolIndicatorCode = ProtocolIndicator[keyof ProtocolIndicator] +export interface CID { + '/': string +} + export interface Address { protocol: ProtocolIndicatorCode payload: Uint8Array @@ -80,57 +84,92 @@ export interface Options { fetch?: typeof globalThis.fetch } +export interface RpcOptions { + method: `Filecoin.${string}` + params?: unknown[] +} + +export interface FetchOptions { + signal?: AbortSignal + keepalive?: boolean + timeout?: number +} + +export interface MsgReceipt { + ExitCode: number + Return: string | null + GasUsed: number + EventsRoot: CID | null +} +export interface MsgLookup { + Height: number + Message: CID + Receipt: MsgReceipt + ReturnDec: unknown | null + TipSet: CID[] +} /** * Lotus API responses + * + * @see https://filecoin-shipyard.github.io/js-lotus-client/api/api.html */ -export type VersionResponse = - | { - result: { Version: string; APIVersion: number; BlockDelay: number } - error: undefined - } - | RpcError - -export type StateNetworkNameResponse = - | { - result: Network - error: undefined - } - | RpcError - -export type MpoolGetNonceResponse = - | { - result: number - error: undefined - } - | RpcError - -export type GasEstimateMessageGasResponse = - | { - result: LotusMessage - error: undefined - } - | RpcError - -export type WalletBalanceResponse = - | { - /** - * Wallet balance in attoFIL - * - * @example '99999927137190925849' - */ - result: string - error: undefined - } - | RpcError - -export type MpoolPushResponse = - | { - result: { - ['/']: string - } - error: undefined - } - | RpcError + +export type LotusResponse = { result: T; error: undefined } | RpcError +export type VersionResponse = LotusResponse<{ + Version: string + APIVersion: number + BlockDelay: number +}> +export type StateNetworkNameResponse = LotusResponse +export type MpoolGetNonceResponse = LotusResponse +export type GasEstimateMessageGasResponse = LotusResponse + +/** + * Wallet balance in attoFIL + * + * @example '99999927137190925849' + */ +export type WalletBalanceResponse = LotusResponse +export type MpoolPushResponse = LotusResponse +export type WaitMsgResponse = LotusResponse + +// RPC methods params + +export interface GasEstimateParams { + /** + * Message to estimate gas for + * + * @see https://lotus.filecoin.io/reference/lotus/gas/#gasestimatemessagegas + */ + msg: PartialMessageObj + /** + * Max fee to pay for gas (attoFIL/gas units) + * + * @default '0' + */ + maxFee?: string +} + +export interface PushMessageParams { + msg: MessageObj + signature: SignatureObj +} + +export interface waitMsgParams { + cid: CID + /** + * Confidence depth to wait for + * + * @default 2 + */ + confidence?: number + /** + * How chain epochs to look back to find the message + * + * @default 100 + */ + lookback?: number +} // Token types export type FormatOptions = BigNumber.Format & { diff --git a/packages/iso-filecoin/test/rpc.test.js b/packages/iso-filecoin/test/rpc.test.js index dab8723..70dcacb 100644 --- a/packages/iso-filecoin/test/rpc.test.js +++ b/packages/iso-filecoin/test/rpc.test.js @@ -67,7 +67,7 @@ describe('lotus rpc', function () { value: '100000000000000000', }) - const estimate = await rpc.gasEstimate(msg) + const estimate = await rpc.gasEstimate({ msg }) if (estimate.error) { return assert.fail(estimate.error.message) } @@ -90,7 +90,7 @@ describe('lotus rpc', function () { value: '100000000000000000', }) - const estimate = await rpc.gasEstimate(msg) + const estimate = await rpc.gasEstimate({ msg }) if (estimate.error) { return assert.fail(estimate.error.message) } @@ -116,7 +116,7 @@ describe('lotus rpc', function () { value: '100000000000000000', }) - const estimate = await rpc.gasEstimate(msg) + const estimate = await rpc.gasEstimate({ msg }) if (estimate.error) { return assert.fail(estimate.error.message) } @@ -182,9 +182,12 @@ describe('lotus rpc', function () { value: '1', }).prepare(rpc) - const balance = await rpc.pushMessage(message, { - type: 'SECP256K1', - data: Wallet.signMessage(account.privateKey, 'SECP256K1', message), + const balance = await rpc.pushMessage({ + msg: message, + signature: { + type: 'SECP256K1', + data: Wallet.signMessage(account.privateKey, 'SECP256K1', message), + }, }) if (balance.error) { return assert.fail(balance.error.message) @@ -210,9 +213,12 @@ describe('lotus rpc', function () { value: '1', }).prepare(rpc) - const balance = await rpc.pushMessage(message, { - type: 'SECP256K1', - data: Wallet.signMessage(account.privateKey, 'SECP256K1', message), + const balance = await rpc.pushMessage({ + msg: message, + signature: { + type: 'SECP256K1', + data: Wallet.signMessage(account.privateKey, 'SECP256K1', message), + }, }) if (balance.error) { return assert.fail(balance.error.message) @@ -221,3 +227,71 @@ describe('lotus rpc', function () { assert.ok(typeof balance.result['/'] === 'string') }) }) + +describe('lotus rpc aborts', function () { + this.retries(3) + this.timeout(10_000) + it(`timeout`, async function () { + const rpc = new RPC({ api: API }, { timeout: 100 }) + + const version = await rpc.version() + + assert.ok(version.error) + + assert.ok(version.error.message.includes('FETCH_ERROR')) + }) + + it(`timeout on method`, async function () { + const rpc = new RPC({ api: API }, { timeout: 100 }) + + const version = await rpc.version({ timeout: 10 }) + + assert.ok(version.error) + assert.ok(version.error.message.includes('FETCH_ERROR')) + }) + + it(`timeout default`, async function () { + const rpc = new RPC({ api: API }) + + const version = await rpc.version() + + assert.ok(version.result) + }) + + it(`timeout hit the 5s default`, async function () { + const rpc = new RPC({ api: API }) + + const msg = await rpc.waitMsg({ + cid: { + '/': 'bafy2bzaceblgnc2umq2u7bzhq2dw3ck6qwksa33hyajltn4wiwtsldhmeuioo', + }, + lookback: 1, + }) + + assert.ok(msg.error) + assert.ok(msg.error.message.includes('FETCH_ERROR')) + }) + + it(`aborted`, async function () { + const rpc = new RPC({ api: API }, { signal: AbortSignal.abort() }) + + const version = await rpc.version() + + assert.ok(version.error) + + assert.ok(version.error.message.includes('FETCH_ERROR')) + }) + + it(`abort`, async function () { + const controller = new AbortController() + const rpc = new RPC({ api: API }) + + const version = rpc.version({ signal: controller.signal }) + controller.abort() + + const rsp = await version + assert.ok(rsp.error) + + assert.ok(rsp.error.message.includes('FETCH_ERROR')) + }) +}) diff --git a/packages/iso-filecoin/tsconfig.json b/packages/iso-filecoin/tsconfig.json index 1e9e457..335673a 100644 --- a/packages/iso-filecoin/tsconfig.json +++ b/packages/iso-filecoin/tsconfig.json @@ -8,6 +8,9 @@ "references": [ { "path": "../iso-base" + }, + { + "path": "../iso-web" } ], "include": ["src", "scripts", "test", "package.json"],