From eae7f7185a010e2e0e4134a6fe19cc631fa4dd13 Mon Sep 17 00:00:00 2001 From: jereldlimjy Date: Wed, 20 Dec 2023 11:10:34 +0800 Subject: [PATCH 1/3] feat: add optional fallbackNodeUrls to RpcProvider --- src/provider/rpc.ts | 36 +++++++++++++++++++++++++---- src/types/provider/configuration.ts | 1 + 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index c59b1cc58..c1e5f5627 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -80,8 +80,10 @@ export class RpcProvider implements ProviderInterface { private chainId?: StarknetChainId; + public fallbackNodeUrls?: string[]; + constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, rpcVersion } = + const { nodeUrl, retries, headers, blockIdentifier, chainId, rpcVersion, fallbackNodeUrls } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { // Network name provided for nodeUrl @@ -101,16 +103,17 @@ export class RpcProvider implements ProviderInterface { this.headers = { ...defaultOptions.headers, ...headers }; this.blockIdentifier = blockIdentifier || defaultOptions.blockIdentifier; this.chainId = chainId; // setting to a non-null value skips making a request in getChainId() + 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, @@ -137,11 +140,36 @@ export class RpcProvider implements ProviderInterface { params?: RPC.Methods[T]['params'] ): Promise { try { - const rawResult = await this.fetch(method, params); + const rawResult = await this.fetch(this.nodeUrl, method, params); 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/types/provider/configuration.ts b/src/types/provider/configuration.ts index e7640aa26..06e03cd93 100644 --- a/src/types/provider/configuration.ts +++ b/src/types/provider/configuration.ts @@ -13,6 +13,7 @@ export type RpcProviderOptions = { blockIdentifier?: BlockIdentifier; chainId?: StarknetChainId; default?: boolean; + fallbackNodeUrls?: string[]; rpcVersion?: 'v0_5' | 'v0_6'; }; From b233b137e5b3d8912e4fc4a8a09140ae11a8a236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Thu, 18 Jan 2024 12:17:07 +0000 Subject: [PATCH 2/3] Add tests ensuring usage of fallback nodes --- __tests__/rpcProvider.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 6346fc61c..bbdaeb4c7 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -339,4 +339,25 @@ describeIfRpc('RPCProvider', () => { expect(syncingStats).toMatchSchemaRef('GetSyncingStatsResponse'); }); }); + + describeIfRpc('Fallback node', () => { + beforeAll(() => {}); + test('Ensure fallback node is used when base node fails', async () => { + const provider: RpcProvider = new RpcProvider({ + nodeUrl: 'Incorrect URL', + fallbackNodeUrls: [process.env.TEST_RPC_URL!], + }); + const blockNumber = await provider.getBlockNumber(); + expect(typeof blockNumber).toBe('number'); + }); + }); + + test('Ensure fallback nodes are run until any of them succeeds', async () => { + const provider: RpcProvider = new RpcProvider({ + nodeUrl: 'Incorrect URL', + fallbackNodeUrls: ['Another incorrect URL', process.env.TEST_RPC_URL!], + }); + const blockNumber = await provider.getBlockNumber(); + expect(typeof blockNumber).toBe('number'); + }); }); From f47d19374f02d49da58481c11ebfab378461056d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Mon, 29 Jan 2024 15:14:47 +0000 Subject: [PATCH 3/3] fix: replace nodeUrl and fallbackNodes with nodeUrls Do not use fallback node if rpc error was thrown --- __tests__/rpcProvider.test.ts | 2 +- src/provider/rpc.ts | 90 ++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index bbdaeb4c7..1ac69d391 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -344,7 +344,7 @@ describeIfRpc('RPCProvider', () => { beforeAll(() => {}); test('Ensure fallback node is used when base node fails', async () => { const provider: RpcProvider = new RpcProvider({ - nodeUrl: 'Incorrect URL', + nodeUrl: 'http://[1080:0:0:0:8:800:200C:417A]', fallbackNodeUrls: [process.env.TEST_RPC_URL!], }); const blockNumber = await provider.getBlockNumber(); diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index c1e5f5627..526101a4c 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -27,6 +27,7 @@ import { getSimulateTransactionOptions, waitForTransactionOptions, } from '../types'; +import assert from '../utils/assert'; import { CallData } from '../utils/calldata'; import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; @@ -68,8 +69,6 @@ const defaultOptions = { }; export class RpcProvider implements ProviderInterface { - public nodeUrl: string; - public headers: object; private responseParser = new RPCResponseParser(); @@ -80,30 +79,39 @@ export class RpcProvider implements ProviderInterface { private chainId?: StarknetChainId; - public fallbackNodeUrls?: string[]; + public nodeUrls: string[]; constructor(optionsOrProvider?: RpcProviderOptions) { const { nodeUrl, retries, headers, blockIdentifier, chainId, rpcVersion, fallbackNodeUrls } = optionsOrProvider || {}; + let primaryNode; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { // Network name provided for nodeUrl - this.nodeUrl = getDefaultNodeUrl( + primaryNode = getDefaultNodeUrl( nodeUrl as NetworkName, optionsOrProvider?.default, rpcVersion ); } else if (nodeUrl) { // NodeUrl provided - this.nodeUrl = nodeUrl; + primaryNode = nodeUrl; } else { // none provided fallback to default testnet - this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default, rpcVersion); + primaryNode = getDefaultNodeUrl(undefined, optionsOrProvider?.default, rpcVersion); } this.retries = retries || defaultOptions.retries; this.headers = { ...defaultOptions.headers, ...headers }; this.blockIdentifier = blockIdentifier || defaultOptions.blockIdentifier; this.chainId = chainId; // setting to a non-null value skips making a request in getChainId() - 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) { @@ -135,41 +143,55 @@ export class RpcProvider implements ProviderInterface { } } + 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 async fetchEndpoint( 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); - 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; }