From 65b35fa5180161c26d6895666de3b38c511eb62f Mon Sep 17 00:00:00 2001 From: Steve Yu Date: Wed, 26 Jan 2022 01:18:10 +0800 Subject: [PATCH] Support nodeUrls (#733) --- lib/near.d.ts | 5 +++++ lib/near.js | 12 +++++++++++- lib/providers/json-rpc-provider.d.ts | 2 +- lib/providers/json-rpc-provider.js | 8 ++++---- lib/utils/web.d.ts | 3 ++- lib/utils/web.js | 17 +++++++++++------ src/near.ts | 20 +++++++++++++++++++- src/providers/json-rpc-provider.ts | 8 ++++---- src/utils/web.ts | 21 +++++++++++++-------- test/utils/web.test.js | 2 +- 10 files changed, 71 insertions(+), 27 deletions(-) diff --git a/lib/near.d.ts b/lib/near.d.ts index 939162f8fa..5379d49602 100644 --- a/lib/near.d.ts +++ b/lib/near.d.ts @@ -48,6 +48,11 @@ export interface NearConfig { * @see {@link JsonRpcProvider.JsonRpcProvider | JsonRpcProvider} */ nodeUrl: string; + /** + * NEAR RPC API urls. Compatibility with existing nodeUrl, multiple URLs for JSON RPC calls. + * @see {@link JsonRpcProvider.JsonRpcProvider | JsonRpcProvider} + */ + nodeUrls: string; /** * NEAR RPC API headers. Can be used to pass API KEY and other parameters. * @see {@link JsonRpcProvider.JsonRpcProvider | JsonRpcProvider} diff --git a/lib/near.js b/lib/near.js index c74bce98a0..bf4bb32e24 100644 --- a/lib/near.js +++ b/lib/near.js @@ -28,9 +28,19 @@ const account_creator_1 = require("./account_creator"); class Near { constructor(config) { this.config = config; + const nodeUrls = []; + if (config.nodeUrl) { + nodeUrls.push(config.nodeUrl); + } + if (config.nodeUrls) { + nodeUrls.push(...config.nodeUrls); + } + if (nodeUrls.length === 0) { + throw new Error('Need to pass nodeUrl or nodeUrls.'); + } this.connection = connection_1.Connection.fromConfig({ networkId: config.networkId, - provider: { type: 'JsonRpcProvider', args: { url: config.nodeUrl, headers: config.headers } }, + provider: { type: 'JsonRpcProvider', args: { selectUrlIndex: 0, urls: nodeUrls, headers: config.headers } }, signer: config.signer || { type: 'InMemorySigner', keyStore: config.keyStore || (config.deps && config.deps.keyStore) } }); if (config.masterAccount) { diff --git a/lib/providers/json-rpc-provider.d.ts b/lib/providers/json-rpc-provider.d.ts index ba97db325d..f95b3bbee1 100644 --- a/lib/providers/json-rpc-provider.d.ts +++ b/lib/providers/json-rpc-provider.d.ts @@ -14,7 +14,7 @@ export declare class JsonRpcProvider extends Provider { /** * @param connectionInfoOrUrl ConnectionInfo or RPC API endpoint URL (deprecated) */ - constructor(connectionInfoOrUrl?: string | ConnectionInfo); + constructor(connectionInfoOrUrls?: string | ConnectionInfo); /** * Gets the RPC's status * @see {@link https://docs.near.org/docs/develop/front-end/rpc#general-validator-status} diff --git a/lib/providers/json-rpc-provider.js b/lib/providers/json-rpc-provider.js index c4fb1c2623..3a079cbbca 100644 --- a/lib/providers/json-rpc-provider.js +++ b/lib/providers/json-rpc-provider.js @@ -34,15 +34,15 @@ class JsonRpcProvider extends provider_1.Provider { /** * @param connectionInfoOrUrl ConnectionInfo or RPC API endpoint URL (deprecated) */ - constructor(connectionInfoOrUrl) { + constructor(connectionInfoOrUrls) { super(); - if (connectionInfoOrUrl != null && typeof connectionInfoOrUrl == 'object') { - this.connection = connectionInfoOrUrl; + if (connectionInfoOrUrls != null && typeof connectionInfoOrUrls == 'object') { + this.connection = connectionInfoOrUrls; } else { const deprecate = depd_1.default('JsonRpcProvider(url?: string)'); deprecate('use `JsonRpcProvider(connectionInfo: ConnectionInfo)` instead'); - this.connection = { url: connectionInfoOrUrl }; + this.connection = { selectUrlIndex: 0, urls: [connectionInfoOrUrls] }; } } /** diff --git a/lib/utils/web.d.ts b/lib/utils/web.d.ts index 639cb740ea..be14fcdf35 100644 --- a/lib/utils/web.d.ts +++ b/lib/utils/web.d.ts @@ -1,5 +1,6 @@ export interface ConnectionInfo { - url: string; + selectUrlIndex: number; + urls: string[]; user?: string; password?: string; allowInsecure?: boolean; diff --git a/lib/utils/web.js b/lib/utils/web.js index 2cd5a75c8b..9558f817b4 100644 --- a/lib/utils/web.js +++ b/lib/utils/web.js @@ -12,23 +12,28 @@ const START_WAIT_TIME_MS = 1000; const BACKOFF_MULTIPLIER = 1.5; const RETRY_NUMBER = 10; async function fetchJson(connectionInfoOrUrl, json) { - let connectionInfo = { url: null }; + let connectionInfo = { selectUrlIndex: 0, urls: [] }; + let selectUrlIndex = 0; if (typeof (connectionInfoOrUrl) === 'string') { - connectionInfo.url = connectionInfoOrUrl; + connectionInfo.urls = [connectionInfoOrUrl]; } else { connectionInfo = connectionInfoOrUrl; } const response = await exponential_backoff_1.default(START_WAIT_TIME_MS, RETRY_NUMBER, BACKOFF_MULTIPLIER, async () => { + if (connectionInfo.selectUrlIndex) { + selectUrlIndex = connectionInfo.selectUrlIndex; + } + connectionInfo.selectUrlIndex = (selectUrlIndex + 1) % connectionInfo.urls.length; try { - const response = await fetch(connectionInfo.url, { + const response = await fetch(connectionInfo.urls[selectUrlIndex], { method: json ? 'POST' : 'GET', body: json ? json : undefined, headers: { ...connectionInfo.headers, 'Content-Type': 'application/json' } }); if (!response.ok) { if (response.status === 503) { - errors_1.logWarning(`Retrying HTTP request for ${connectionInfo.url} as it's not available now`); + errors_1.logWarning(`Retrying HTTP request for ${connectionInfo.urls[selectUrlIndex]} as it's not available now`); return null; } throw http_errors_1.default(response.status, await response.text()); @@ -37,14 +42,14 @@ async function fetchJson(connectionInfoOrUrl, json) { } catch (error) { if (error.toString().includes('FetchError') || error.toString().includes('Failed to fetch')) { - errors_1.logWarning(`Retrying HTTP request for ${connectionInfo.url} because of error: ${error}`); + errors_1.logWarning(`Retrying HTTP request for ${connectionInfo.urls[selectUrlIndex]} because of error: ${error}`); return null; } throw error; } }); if (!response) { - throw new providers_1.TypedError(`Exceeded ${RETRY_NUMBER} attempts for ${connectionInfo.url}.`, 'RetriesExceeded'); + throw new providers_1.TypedError(`Exceeded ${RETRY_NUMBER} attempts for ${connectionInfo.urls[selectUrlIndex]}.`, 'RetriesExceeded'); } return await response.json(); } diff --git a/src/near.ts b/src/near.ts index aeb7c9facb..4fcf80c949 100644 --- a/src/near.ts +++ b/src/near.ts @@ -55,6 +55,12 @@ export interface NearConfig { */ nodeUrl: string; + /** + * NEAR RPC API urls. Compatibility with existing nodeUrl, multiple URLs for JSON RPC calls. + * @see {@link JsonRpcProvider.JsonRpcProvider | JsonRpcProvider} + */ + nodeUrls: string; + /** * NEAR RPC API headers. Can be used to pass API KEY and other parameters. * @see {@link JsonRpcProvider.JsonRpcProvider | JsonRpcProvider} @@ -82,9 +88,21 @@ export class Near { constructor(config: NearConfig) { this.config = config; + + const nodeUrls = []; + if(config.nodeUrl) { + nodeUrls.push(config.nodeUrl); + } + if(config.nodeUrls) { + nodeUrls.push(...config.nodeUrls); + } + if(nodeUrls.length === 0) { + throw new Error('Need to pass nodeUrl or nodeUrls.'); + } + this.connection = Connection.fromConfig({ networkId: config.networkId, - provider: { type: 'JsonRpcProvider', args: { url: config.nodeUrl, headers: config.headers } }, + provider: { type: 'JsonRpcProvider', args: { selectUrlIndex: 0, urls: nodeUrls, headers: config.headers } }, signer: config.signer || { type: 'InMemorySigner', keyStore: config.keyStore || (config.deps && config.deps.keyStore) } }); if (config.masterAccount) { diff --git a/src/providers/json-rpc-provider.ts b/src/providers/json-rpc-provider.ts index 64204630eb..c1219b9159 100644 --- a/src/providers/json-rpc-provider.ts +++ b/src/providers/json-rpc-provider.ts @@ -56,14 +56,14 @@ export class JsonRpcProvider extends Provider { /** * @param connectionInfoOrUrl ConnectionInfo or RPC API endpoint URL (deprecated) */ - constructor(connectionInfoOrUrl?: string | ConnectionInfo) { + constructor(connectionInfoOrUrls?: string | ConnectionInfo) { super(); - if (connectionInfoOrUrl != null && typeof connectionInfoOrUrl == 'object') { - this.connection = connectionInfoOrUrl as ConnectionInfo; + if (connectionInfoOrUrls != null && typeof connectionInfoOrUrls == 'object') { + this.connection = connectionInfoOrUrls as ConnectionInfo; } else { const deprecate = depd('JsonRpcProvider(url?: string)'); deprecate('use `JsonRpcProvider(connectionInfo: ConnectionInfo)` instead'); - this.connection = { url: connectionInfoOrUrl as string }; + this.connection = { selectUrlIndex: 0, urls: [connectionInfoOrUrls as string] }; } } diff --git a/src/utils/web.ts b/src/utils/web.ts index 8e8dfe6a15..f3b42929ce 100644 --- a/src/utils/web.ts +++ b/src/utils/web.ts @@ -9,7 +9,8 @@ const BACKOFF_MULTIPLIER = 1.5; const RETRY_NUMBER = 10; export interface ConnectionInfo { - url: string; + selectUrlIndex: number; + urls: string[]; user?: string; password?: string; allowInsecure?: boolean; @@ -18,23 +19,27 @@ export interface ConnectionInfo { } export async function fetchJson(connectionInfoOrUrl: string | ConnectionInfo, json?: string): Promise { - let connectionInfo: ConnectionInfo = { url: null }; + let connectionInfo: ConnectionInfo = { selectUrlIndex: 0, urls: [] }; + let selectUrlIndex = 0; if (typeof (connectionInfoOrUrl) === 'string') { - connectionInfo.url = connectionInfoOrUrl; + connectionInfo.urls = [connectionInfoOrUrl]; } else { connectionInfo = connectionInfoOrUrl as ConnectionInfo; } - const response = await exponentialBackoff(START_WAIT_TIME_MS, RETRY_NUMBER, BACKOFF_MULTIPLIER, async () => { + if(connectionInfo.selectUrlIndex) { + selectUrlIndex = connectionInfo.selectUrlIndex; + } + connectionInfo.selectUrlIndex = (selectUrlIndex + 1) % connectionInfo.urls.length; try { - const response = await fetch(connectionInfo.url, { + const response = await fetch(connectionInfo.urls[selectUrlIndex], { method: json ? 'POST' : 'GET', body: json ? json : undefined, headers: { ...connectionInfo.headers, 'Content-Type': 'application/json' } }); if (!response.ok) { if (response.status === 503) { - logWarning(`Retrying HTTP request for ${connectionInfo.url} as it's not available now`); + logWarning(`Retrying HTTP request for ${connectionInfo.urls[selectUrlIndex]} as it's not available now`); return null; } throw createError(response.status, await response.text()); @@ -42,14 +47,14 @@ export async function fetchJson(connectionInfoOrUrl: string | ConnectionInfo, js return response; } catch (error) { if (error.toString().includes('FetchError') || error.toString().includes('Failed to fetch')) { - logWarning(`Retrying HTTP request for ${connectionInfo.url} because of error: ${error}`); + logWarning(`Retrying HTTP request for ${connectionInfo.urls[selectUrlIndex]} because of error: ${error}`); return null; } throw error; } }); if (!response) { - throw new TypedError(`Exceeded ${RETRY_NUMBER} attempts for ${connectionInfo.url}.`, 'RetriesExceeded'); + throw new TypedError(`Exceeded ${RETRY_NUMBER} attempts for ${connectionInfo.urls[selectUrlIndex]}.`, 'RetriesExceeded'); } return await response.json(); } diff --git a/test/utils/web.test.js b/test/utils/web.test.js index 880e9bd66a..7a96f52837 100644 --- a/test/utils/web.test.js +++ b/test/utils/web.test.js @@ -14,7 +14,7 @@ describe('web', () => { expect(result.result.chain_id).toBe('testnet'); }); test('object parameter in fetchJson', async () => { - const connection = { url: 'https://rpc.testnet.near.org' }; + const connection = { urls: ['https://rpc.testnet.near.org'], selectUrlIndex: 0 }; const statusRequest = { 'jsonrpc': '2.0', 'id': 'dontcare',