From 5ca9ba39567e0c8ece377c2782a10637bb393e04 Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:35:17 +0900 Subject: [PATCH 1/9] feat: fastest rpc is selected and others are discarded for faster results and less network calls --- types/rpc-service.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/types/rpc-service.ts b/types/rpc-service.ts index 759cf7d..2cef52e 100644 --- a/types/rpc-service.ts +++ b/types/rpc-service.ts @@ -57,7 +57,6 @@ export class RPCService { throw new Error(rpcUrl); } } - const promises = runtimeRpcs.map((rpcUrl) => requestEndpoint(rpcUrl)); async function getFirstSuccessfulRequest(requests: string[]) { if (requests.length === 0) { throw new Error("All requests failed."); @@ -65,9 +64,12 @@ export class RPCService { const promisesToResolve = requests.map((rpcUrl) => requestEndpoint(rpcUrl)); try { - return await Promise.race(promisesToResolve); + const res = await Promise.race(promisesToResolve); + if (!res.success) { + throw new Error(res.rpcUrl); + } + return res; } catch (err) { - console.error(`Failed to reach endpoint. ${err}`); if (err instanceof Error && requests.includes(err.message)) { return getFirstSuccessfulRequest(requests.filter((request) => request !== err.message)); } @@ -78,22 +80,9 @@ export class RPCService { if (fastest.success) { latencies[`${networkId}__${fastest.rpcUrl}`] = fastest.duration; + runtimeRpcs = runtimeRpcs.filter((o) => o === fastest.rpcUrl); } - const allResults = await Promise.allSettled(promises); - - allResults.forEach((result) => { - if (result.status === "fulfilled" && result.value.success) { - latencies[`${networkId}__${result.value.rpcUrl}`] = result.value.duration; - } else if (result.status === "fulfilled") { - const fulfilledResult = result.value; - const index = runtimeRpcs.indexOf(fulfilledResult.rpcUrl); - if (index > -1) { - runtimeRpcs.splice(index, 1); - } - } - }); - return { latencies, runtimeRpcs }; } From f19192c08e6b4a2ebbae8fccd4985a79dde35d9a Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:07:50 +0900 Subject: [PATCH 2/9] feat: new method to get the first available RPC --- tests/rpc-handler.test.ts | 12 ++++++++++++ types/rpc-handler.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/rpc-handler.test.ts b/tests/rpc-handler.test.ts index 7df0441..7427ab2 100644 --- a/tests/rpc-handler.test.ts +++ b/tests/rpc-handler.test.ts @@ -191,4 +191,16 @@ describe("RPCHandler", () => { }, 10000); } }); + + it("Should return the first available RPC", async () => { + const module = await import("../types/rpc-handler"); + const rpcHandler = new module.RPCHandler({ + ...testConfig, + networkId: "1", + rpcTimeout: 1000, + }); + + const provider = await rpcHandler.getFirstAvailableRpcProvider(); + expect(provider).not.toBeNull(); + }); }); diff --git a/types/rpc-handler.ts b/types/rpc-handler.ts index df4d78d..76b963f 100644 --- a/types/rpc-handler.ts +++ b/types/rpc-handler.ts @@ -7,6 +7,13 @@ import { StorageService } from "./storage-service"; const NO_RPCS_AVAILABLE = "No RPCs available"; +function shuffleArray(array: object[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} + export class RPCHandler implements HandlerInterface { private static _instance: RPCHandler | null = null; private _provider: JsonRpcProvider | null = null; @@ -55,6 +62,27 @@ export class RPCHandler implements HandlerInterface { this.testRpcPerformance.bind(this); } + /** + * Loops through all RPCs for a given network id and returns a provider with the first successful network. + */ + public async getFirstAvailableRpcProvider() { + const rpcList = [...networkRpcs[this._networkId].rpcs]; + shuffleArray(rpcList); + for (const rpc of rpcList) { + try { + const result = await RPCService.makeRpcRequest(rpc.url, this._rpcTimeout, { "Content-Type": "application/json" }); + if (result.success) { + return this.createProviderProxy(new JsonRpcProvider({ url: rpc.url, skipFetchSetup: true }, Number(this._networkId)), this); + } else { + console.error(`Failed to reach endpoint ${rpc.url}. ${result.error}`); + } + } catch (err) { + console.error(`Failed to reach endpoint ${rpc.url}. ${err}`); + } + } + return null; + } + public async getFastestRpcProvider(): Promise { let fastest = await this.testRpcPerformance(); From 405dca21ca2c084b5157dba22fe078f241e42d7e Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:16:06 +0900 Subject: [PATCH 3/9] v1.2.4-rc --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 653044b..b05e960 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@ubiquity-dao/rpc-handler", - "version": "1.2.3", + "name": "@ubiquibot/rpc-handler", + "version": "1.2.4-rc", "description": "Uses Chainlist's RPC collection racing them returning the lowest latency RPC", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -23,7 +23,7 @@ "prepare": "husky install", "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist", "build:index": "tsx build/esbuild-build.ts", - "build": "run-s clean build:index build:types", + "build": "run-s build:index build:types", "postinstall": "git submodule update --init --recursive", "test:anvil": "npx tsx ./tests/anvil.ts", "test": "jest", From 685ac3640db369b491e7dd751a8d065bb98b6489 Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:20:43 +0900 Subject: [PATCH 4/9] chore: revert package.json --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b05e960..653044b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@ubiquibot/rpc-handler", - "version": "1.2.4-rc", + "name": "@ubiquity-dao/rpc-handler", + "version": "1.2.3", "description": "Uses Chainlist's RPC collection racing them returning the lowest latency RPC", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -23,7 +23,7 @@ "prepare": "husky install", "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist", "build:index": "tsx build/esbuild-build.ts", - "build": "run-s build:index build:types", + "build": "run-s clean build:index build:types", "postinstall": "git submodule update --init --recursive", "test:anvil": "npx tsx ./tests/anvil.ts", "test": "jest", From b6ccb969dd8b0dafe2a7b9c0a121039bfa6cdcf0 Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Mon, 19 Aug 2024 05:43:58 +0900 Subject: [PATCH 5/9] chore: removed proxy on getFirstAvailableRpcProvider --- types/rpc-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/rpc-handler.ts b/types/rpc-handler.ts index 76b963f..36bad20 100644 --- a/types/rpc-handler.ts +++ b/types/rpc-handler.ts @@ -72,7 +72,7 @@ export class RPCHandler implements HandlerInterface { try { const result = await RPCService.makeRpcRequest(rpc.url, this._rpcTimeout, { "Content-Type": "application/json" }); if (result.success) { - return this.createProviderProxy(new JsonRpcProvider({ url: rpc.url, skipFetchSetup: true }, Number(this._networkId)), this); + return new JsonRpcProvider({ url: rpc.url, skipFetchSetup: true }, Number(this._networkId)); } else { console.error(`Failed to reach endpoint ${rpc.url}. ${result.error}`); } From 2b55c9845095e0ec6e62526bf60950519df1a43f Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Mon, 19 Aug 2024 06:29:12 +0900 Subject: [PATCH 6/9] chore: test rpc shift --- test.ts | 30 ++++++++++++++++++++++++++++++ tests/anvil.ts | 8 ++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 test.ts diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..f5b028a --- /dev/null +++ b/test.ts @@ -0,0 +1,30 @@ +import { RPCHandler } from "./types/rpc-handler"; + +async function test(ensName: string) { + console.log("1. resolveAddress"); + const rpc = new RPCHandler({ + networkId: "1", + // networkName: "ethereum-mainnet", + networkName: null, + // networkRpcs: null, + networkRpcs: [{ url: "https://eth.drpc.org" }], + autoStorage: false, + cacheRefreshCycles: 3, + // runtimeRpcs: null, + runtimeRpcs: ["1__https://eth.drpc.org"], + rpcTimeout: 600, + // tracking: "none", + proxySettings: { retryCount: 0, retryDelay: 1000, logTier: "verbose", logger: null, strictLogs: true }, + }); + console.log("2. resolveAddress"); + const provider = await rpc.getFastestRpcProvider(); + console.log("3. resolveAddress"); + return await provider.resolveName(ensName).catch((err) => { + console.trace({ err }); + return null; + }); +} + +test("mentlegen.eth") + .then((r) => console.log(`done ${r}`)) + .catch((err) => console.error(err)); diff --git a/tests/anvil.ts b/tests/anvil.ts index 309bf76..2d4ecd8 100644 --- a/tests/anvil.ts +++ b/tests/anvil.ts @@ -1,9 +1,11 @@ import { spawnSync } from "child_process"; +import { networkRpcs } from "../types/constants"; import { RPCHandler } from "../types/rpc-handler"; class Anvil { rpcs: string[] = []; rpcHandler: RPCHandler | null = null; + availableRpcs = [...networkRpcs["100"].rpcs]; async init() { this.rpcHandler = new RPCHandler({ @@ -13,7 +15,7 @@ class Anvil { networkName: "gnosis", rpcTimeout: 1000, runtimeRpcs: null, - networkRpcs: null, + networkRpcs: this.availableRpcs, proxySettings: { logger: null, logTier: "ok", @@ -58,7 +60,9 @@ class Anvil { if (anvil.status !== 0) { console.log(`Anvil failed to start with RPC: ${rpc}`); console.log(`Retrying with next RPC...`); - return this.spawner(this.rpcs.shift()); + this.availableRpcs.shift(); + await this.init(); + return this.spawner(this.rpcs[0]); } return true; From c576f7b317f1f8715588f039f325c56a4248e5c4 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Mon, 19 Aug 2024 06:29:42 +0900 Subject: [PATCH 7/9] chore: test rpc shift --- test.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 test.ts diff --git a/test.ts b/test.ts deleted file mode 100644 index f5b028a..0000000 --- a/test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { RPCHandler } from "./types/rpc-handler"; - -async function test(ensName: string) { - console.log("1. resolveAddress"); - const rpc = new RPCHandler({ - networkId: "1", - // networkName: "ethereum-mainnet", - networkName: null, - // networkRpcs: null, - networkRpcs: [{ url: "https://eth.drpc.org" }], - autoStorage: false, - cacheRefreshCycles: 3, - // runtimeRpcs: null, - runtimeRpcs: ["1__https://eth.drpc.org"], - rpcTimeout: 600, - // tracking: "none", - proxySettings: { retryCount: 0, retryDelay: 1000, logTier: "verbose", logger: null, strictLogs: true }, - }); - console.log("2. resolveAddress"); - const provider = await rpc.getFastestRpcProvider(); - console.log("3. resolveAddress"); - return await provider.resolveName(ensName).catch((err) => { - console.trace({ err }); - return null; - }); -} - -test("mentlegen.eth") - .then((r) => console.log(`done ${r}`)) - .catch((err) => console.error(err)); From d52e31459b7f1d30d65a00b2d38c3ef917f7c6f0 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Mon, 19 Aug 2024 14:09:48 +0900 Subject: [PATCH 8/9] chore: test all settled --- tests/anvil.ts | 8 ++---- types/rpc-service.ts | 59 +++++++++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/tests/anvil.ts b/tests/anvil.ts index 2d4ecd8..309bf76 100644 --- a/tests/anvil.ts +++ b/tests/anvil.ts @@ -1,11 +1,9 @@ import { spawnSync } from "child_process"; -import { networkRpcs } from "../types/constants"; import { RPCHandler } from "../types/rpc-handler"; class Anvil { rpcs: string[] = []; rpcHandler: RPCHandler | null = null; - availableRpcs = [...networkRpcs["100"].rpcs]; async init() { this.rpcHandler = new RPCHandler({ @@ -15,7 +13,7 @@ class Anvil { networkName: "gnosis", rpcTimeout: 1000, runtimeRpcs: null, - networkRpcs: this.availableRpcs, + networkRpcs: null, proxySettings: { logger: null, logTier: "ok", @@ -60,9 +58,7 @@ class Anvil { if (anvil.status !== 0) { console.log(`Anvil failed to start with RPC: ${rpc}`); console.log(`Retrying with next RPC...`); - this.availableRpcs.shift(); - await this.init(); - return this.spawner(this.rpcs[0]); + return this.spawner(this.rpcs.shift()); } return true; diff --git a/types/rpc-service.ts b/types/rpc-service.ts index 2cef52e..1ae8d47 100644 --- a/types/rpc-service.ts +++ b/types/rpc-service.ts @@ -57,32 +57,45 @@ export class RPCService { throw new Error(rpcUrl); } } - async function getFirstSuccessfulRequest(requests: string[]) { - if (requests.length === 0) { - throw new Error("All requests failed."); - } - const promisesToResolve = requests.map((rpcUrl) => requestEndpoint(rpcUrl)); + const promises = runtimeRpcs.map((rpcUrl) => requestEndpoint(rpcUrl)); + // async function getFirstSuccessfulRequest(requests: string[]) { + // if (requests.length === 0) { + // throw new Error("All requests failed."); + // } + // const promisesToResolve = requests.map((rpcUrl) => requestEndpoint(rpcUrl)); + // + // try { + // const res = await Promise.race(promisesToResolve); + // if (!res.success) { + // throw new Error(res.rpcUrl); + // } + // return res; + // } catch (err) { + // if (err instanceof Error && requests.includes(err.message)) { + // return getFirstSuccessfulRequest(requests.filter((request) => request !== err.message)); + // } + // return getFirstSuccessfulRequest(requests.slice(1)); + // } + // } + // const fastest = await getFirstSuccessfulRequest(runtimeRpcs); + // + // if (fastest.success) { + // latencies[`${networkId}__${fastest.rpcUrl}`] = fastest.duration; + // } - try { - const res = await Promise.race(promisesToResolve); - if (!res.success) { - throw new Error(res.rpcUrl); - } - return res; - } catch (err) { - if (err instanceof Error && requests.includes(err.message)) { - return getFirstSuccessfulRequest(requests.filter((request) => request !== err.message)); + const allResults = await Promise.allSettled(promises); + + allResults.forEach((result) => { + if (result.status === "fulfilled" && result.value.success) { + latencies[`${networkId}__${result.value.rpcUrl}`] = result.value.duration; + } else if (result.status === "fulfilled") { + const fulfilledResult = result.value; + const index = runtimeRpcs.indexOf(fulfilledResult.rpcUrl); + if (index > -1) { + runtimeRpcs.splice(index, 1); } - return getFirstSuccessfulRequest(requests.slice(1)); } - } - const fastest = await getFirstSuccessfulRequest(runtimeRpcs); - - if (fastest.success) { - latencies[`${networkId}__${fastest.rpcUrl}`] = fastest.duration; - runtimeRpcs = runtimeRpcs.filter((o) => o === fastest.rpcUrl); - } - + }); return { latencies, runtimeRpcs }; } From 938e148c192068387b3084932c387eb335006c75 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Mon, 19 Aug 2024 14:42:18 +0900 Subject: [PATCH 9/9] chore: back to all settled --- types/rpc-service.ts | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/types/rpc-service.ts b/types/rpc-service.ts index 1ae8d47..ad7af3b 100644 --- a/types/rpc-service.ts +++ b/types/rpc-service.ts @@ -57,32 +57,8 @@ export class RPCService { throw new Error(rpcUrl); } } - const promises = runtimeRpcs.map((rpcUrl) => requestEndpoint(rpcUrl)); - // async function getFirstSuccessfulRequest(requests: string[]) { - // if (requests.length === 0) { - // throw new Error("All requests failed."); - // } - // const promisesToResolve = requests.map((rpcUrl) => requestEndpoint(rpcUrl)); - // - // try { - // const res = await Promise.race(promisesToResolve); - // if (!res.success) { - // throw new Error(res.rpcUrl); - // } - // return res; - // } catch (err) { - // if (err instanceof Error && requests.includes(err.message)) { - // return getFirstSuccessfulRequest(requests.filter((request) => request !== err.message)); - // } - // return getFirstSuccessfulRequest(requests.slice(1)); - // } - // } - // const fastest = await getFirstSuccessfulRequest(runtimeRpcs); - // - // if (fastest.success) { - // latencies[`${networkId}__${fastest.rpcUrl}`] = fastest.duration; - // } + const promises = runtimeRpcs.map((rpcUrl) => requestEndpoint(rpcUrl)); const allResults = await Promise.allSettled(promises); allResults.forEach((result) => { @@ -96,6 +72,7 @@ export class RPCService { } } }); + return { latencies, runtimeRpcs }; }