From 12020d616cc740af9d276929f9d6a00e511c4d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Wed, 24 Jan 2024 13:30:36 +0000 Subject: [PATCH 1/2] feat: add fallback nodes to provider --- __tests__/rpcProvider.test.ts | 22 ++++++++++++++++++ src/channel/rpc_0_6.ts | 36 +++++++++++++++++++++++++---- src/provider/rpc.ts | 2 +- src/types/provider/configuration.ts | 1 + 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 3da84184c..f54069776 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -6,6 +6,7 @@ import { CallData, Contract, RPC, + RpcProvider, TransactionExecutionStatus, stark, waitForTransactionOptions, @@ -334,4 +335,25 @@ describeIfRpc('RPCProvider', () => { expect(syncingStats).toMatchSchemaRef('GetSyncingStatsResponse'); }); }); + + describeIfRpc('Fallback node', () => { + beforeAll(() => {}); + test('Ensure fallback node is used when base node fails', async () => { + const fallbackProvider: RpcProvider = new RpcProvider({ + nodeUrl: 'Incorrect URL', + fallbackNodeUrls: [process.env.TEST_RPC_URL!], + }); + const blockNumber = await fallbackProvider.getBlockNumber(); + expect(typeof blockNumber).toBe('number'); + }); + }); + + test('Ensure fallback nodes are run until any of them succeeds', async () => { + const fallbackProvider: RpcProvider = new RpcProvider({ + nodeUrl: 'Incorrect URL', + fallbackNodeUrls: ['Another incorrect URL', process.env.TEST_RPC_URL!], + }); + const blockNumber = await fallbackProvider.getBlockNumber(); + expect(typeof blockNumber).toBe('number'); + }); }); diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index 05952e2bf..0f64ca070 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -52,8 +52,10 @@ export class RpcChannel { readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed + public fallbackNodeUrls?: string[]; + constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, waitMode } = + const { nodeUrl, retries, headers, blockIdentifier, chainId, waitMode, fallbackNodeUrls } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); @@ -68,16 +70,17 @@ export class RpcChannel { this.chainId = chainId; this.waitMode = waitMode || false; this.requestId = 0; + this.fallbackNodeUrls = fallbackNodeUrls; } - public fetch(method: string, params?: object, id: string | number = 0) { + public fetch(url: string, method: string, params?: object, id: string | number = 0) { const rpcRequestBody: RPC.JRPC.RequestBody = { id, jsonrpc: '2.0', method, ...(params && { params }), }; - return fetch(this.nodeUrl, { + return fetch(url, { method: 'POST', body: stringify(rpcRequestBody), headers: this.headers as Record, @@ -105,11 +108,36 @@ export class RpcChannel { params?: RPC.Methods[T]['params'] ): Promise { try { - const rawResult = await this.fetch(method, params, (this.requestId += 1)); + const rawResult = await this.fetch(this.nodeUrl, method, params, (this.requestId += 1)); const { error, result } = await rawResult.json(); this.errorHandler(method, params, error); return result as RPC.Methods[T]['result']; } catch (error: any) { + if (this.fallbackNodeUrls) { + for (let i = 0; i < this.fallbackNodeUrls.length; i += 1) { + try { + // eslint-disable-next-line no-await-in-loop + const fallbackResult = await this.fetch(this.fallbackNodeUrls[i], method, params); + // eslint-disable-next-line no-await-in-loop + const { error: fallbackError, result } = await fallbackResult.json(); + this.errorHandler(method, params, fallbackError); + + // If a fallback node succeeds, update the primary and fallback URLs + const oldPrimaryUrl = this.nodeUrl; + this.nodeUrl = this.fallbackNodeUrls[i]; + this.fallbackNodeUrls.splice(i, 1); // Remove the new primary from the fallback list + this.fallbackNodeUrls.push(oldPrimaryUrl); // Add the old primary to the end of the fallback list + + return result as RPC.Methods[T]['result']; + } catch (fallbackError: any) { + if (i === this.fallbackNodeUrls.length - 1) { + this.errorHandler(method, params, fallbackError?.response?.data, fallbackError); + throw fallbackError; + } + } + } + } + this.errorHandler(method, params, error?.response?.data, error); throw error; } diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index f71ee8f48..8cc82311f 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -43,7 +43,7 @@ export class RpcProvider implements ProviderInterface { } public fetch(method: string, params?: object, id: string | number = 0) { - return this.channel.fetch(method, params, id); + return this.channel.fetch(this.channel.nodeUrl, method, params, id); } public async getChainId() { diff --git a/src/types/provider/configuration.ts b/src/types/provider/configuration.ts index b4d614f6d..ce0bf46c8 100644 --- a/src/types/provider/configuration.ts +++ b/src/types/provider/configuration.ts @@ -11,4 +11,5 @@ export type RpcProviderOptions = { chainId?: StarknetChainId; default?: boolean; waitMode?: boolean; + fallbackNodeUrls?: string[]; }; From 8e76a448c05a08c2234c9e469bf3774e9a5376da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Wed, 7 Feb 2024 14:00:03 +0000 Subject: [PATCH 2/2] fix: replace nodeUrl and fallbackNodes with nodeUrls Do not use fallback node if rpc error was thrown --- src/channel/rpc_0_6.ts | 90 ++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index 0f64ca070..d1872b2ae 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -19,6 +19,7 @@ import { waitForTransactionOptions, } from '../types'; import { ETransactionVersion } from '../types/api'; +import assert from '../utils/assert'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import fetch from '../utils/fetchPonyfill'; @@ -36,8 +37,6 @@ const defaultOptions = { }; export class RpcChannel { - public nodeUrl: string; - public headers: object; readonly retries: number; @@ -52,17 +51,18 @@ export class RpcChannel { readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed - public fallbackNodeUrls?: string[]; + public nodeUrls: string[]; constructor(optionsOrProvider?: RpcProviderOptions) { const { nodeUrl, retries, headers, blockIdentifier, chainId, waitMode, fallbackNodeUrls } = optionsOrProvider || {}; + let primaryNode; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { - this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); + primaryNode = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); } else if (nodeUrl) { - this.nodeUrl = nodeUrl; + primaryNode = nodeUrl; } else { - this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default); + primaryNode = getDefaultNodeUrl(undefined, optionsOrProvider?.default); } this.retries = retries || defaultOptions.retries; this.headers = { ...defaultOptions.headers, ...headers }; @@ -70,7 +70,15 @@ export class RpcChannel { this.chainId = chainId; this.waitMode = waitMode || false; this.requestId = 0; - this.fallbackNodeUrls = fallbackNodeUrls; + this.nodeUrls = [primaryNode, ...(fallbackNodeUrls || [])]; + } + + get nodeUrl() { + return this.nodeUrls[0]; + } + + set nodeUrl(url) { + this.nodeUrls[0] = url; } public fetch(url: string, method: string, params?: object, id: string | number = 0) { @@ -87,6 +95,44 @@ export class RpcChannel { }); } + protected async setPrimaryNode(node: string, index: number) { + // eslint-disable-next-line prefer-destructuring + this.nodeUrls[index] = this.nodeUrls[0]; + this.nodeUrls[0] = node; + } + + protected async fetchResponse(method: string, params?: object) { + const nodes = [...this.nodeUrls]; + const lastNode = nodes.pop(); + assert(lastNode !== undefined); + let response; + for (let i = 0; i < nodes.length - 1; i += 1) { + try { + // eslint-disable-next-line no-await-in-loop + response = await this.fetch(nodes[i], method, params); + + if (response.ok) { + this.setPrimaryNode(nodes[i], i); + return response; + } + } catch (error: any) { + /* empty */ + } + } + + // If all nodes fail return anything the last one returned + try { + response = await this.fetch(lastNode, method, params); + if (response.ok) { + this.setPrimaryNode(lastNode, this.nodeUrls.length - 1); + } + return response; + } catch (error: any) { + this.errorHandler(method, params, error?.response?.data, error); + throw error; + } + } + protected errorHandler(method: string, params: any, rpcError?: RPC.JRPC.Error, otherError?: any) { if (rpcError) { const { code, message, data } = rpcError; @@ -107,37 +153,13 @@ export class RpcChannel { method: T, params?: RPC.Methods[T]['params'] ): Promise { + const response = await this.fetchResponse(method, params); + try { - const rawResult = await this.fetch(this.nodeUrl, method, params, (this.requestId += 1)); - const { error, result } = await rawResult.json(); + const { error, result } = await response.json(); this.errorHandler(method, params, error); return result as RPC.Methods[T]['result']; } catch (error: any) { - if (this.fallbackNodeUrls) { - for (let i = 0; i < this.fallbackNodeUrls.length; i += 1) { - try { - // eslint-disable-next-line no-await-in-loop - const fallbackResult = await this.fetch(this.fallbackNodeUrls[i], method, params); - // eslint-disable-next-line no-await-in-loop - const { error: fallbackError, result } = await fallbackResult.json(); - this.errorHandler(method, params, fallbackError); - - // If a fallback node succeeds, update the primary and fallback URLs - const oldPrimaryUrl = this.nodeUrl; - this.nodeUrl = this.fallbackNodeUrls[i]; - this.fallbackNodeUrls.splice(i, 1); // Remove the new primary from the fallback list - this.fallbackNodeUrls.push(oldPrimaryUrl); // Add the old primary to the end of the fallback list - - return result as RPC.Methods[T]['result']; - } catch (fallbackError: any) { - if (i === this.fallbackNodeUrls.length - 1) { - this.errorHandler(method, params, fallbackError?.response?.data, fallbackError); - throw fallbackError; - } - } - } - } - this.errorHandler(method, params, error?.response?.data, error); throw error; }