Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fallback nodes to provider #980

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions __tests__/rpcProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CallData,
Contract,
RPC,
RpcProvider,
TransactionExecutionStatus,
stark,
waitForTransactionOptions,
Expand Down Expand Up @@ -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');
});
});
70 changes: 60 additions & 10 deletions src/channel/rpc_0_6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -36,8 +37,6 @@ const defaultOptions = {
};

export class RpcChannel {
public nodeUrl: string;

public headers: object;

readonly retries: number;
Expand All @@ -52,38 +51,88 @@ export class RpcChannel {

readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed

public nodeUrls: string[];

constructor(optionsOrProvider?: RpcProviderOptions) {
const { nodeUrl, retries, headers, blockIdentifier, chainId, waitMode } =
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 };
this.blockIdentifier = blockIdentifier || defaultOptions.blockIdentifier;
this.chainId = chainId;
this.waitMode = waitMode || false;
this.requestId = 0;
this.nodeUrls = [primaryNode, ...(fallbackNodeUrls || [])];
}

get nodeUrl() {
return this.nodeUrls[0];
}

set nodeUrl(url) {
this.nodeUrls[0] = url;
}

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<string, string>,
});
}

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why we need this primaryNode here

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;
Expand All @@ -104,9 +153,10 @@ export class RpcChannel {
method: T,
params?: RPC.Methods[T]['params']
): Promise<RPC.Methods[T]['result']> {
const response = await this.fetchResponse(method, params);

try {
const rawResult = await this.fetch(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) {
Expand Down
2 changes: 1 addition & 1 deletion src/provider/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/types/provider/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export type RpcProviderOptions = {
chainId?: StarknetChainId;
default?: boolean;
waitMode?: boolean;
fallbackNodeUrls?: string[];
};