From c88abb1847edaef135004803cd434c1fa93e7f43 Mon Sep 17 00:00:00 2001 From: micky Date: Mon, 10 Jun 2024 16:39:19 +0200 Subject: [PATCH 1/9] 1CT best nonce --- src/lib/contracts/callContract.tsx | 7 ++++--- src/lib/contracts/utils.ts | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lib/contracts/callContract.tsx b/src/lib/contracts/callContract.tsx index f9dbe48ad4..18f2552557 100644 --- a/src/lib/contracts/callContract.tsx +++ b/src/lib/contracts/callContract.tsx @@ -4,7 +4,7 @@ import { getExplorerUrl } from "config/chains"; import { Contract, Wallet, Overrides } from "ethers"; import { helperToast } from "../helperToast"; import { getErrorMessage } from "./transactionErrors"; -import { getGasLimit, setGasPrice } from "./utils"; +import { getGasLimit, setGasPrice, getBestNonce } from "./utils"; import { ReactNode } from "react"; import React from "react"; @@ -46,8 +46,9 @@ export async function callContract( } if (opts.customSigners) { - // If we send the transaction to multiple RPCs simultaneously, we should specify a fixed nonce to avoid possible txn duplication. - txnOpts.nonce = await wallet.getNonce(); + // If we send the transaction to multiple RPCs simultaneously, + // we should specify a fixed nonce to avoid possible txn duplication. + txnOpts.nonce = await getBestNonce([wallet, ...opts.customSigners]); } if (opts.showPreliminaryMsg && !opts.hideSentMsg) { diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index e28f5a6a2e..316d080a39 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -1,5 +1,5 @@ import { GAS_PRICE_ADJUSTMENT_MAP, MAX_GAS_PRICE_MAP } from "config/chains"; -import { Contract, BaseContract, Provider } from "ethers"; +import { Contract, BaseContract, Provider, Wallet } from "ethers"; export async function setGasPrice(txnOpts: any, provider: Provider, chainId: number) { let maxGasPrice = MAX_GAS_PRICE_MAP[chainId]; @@ -46,3 +46,18 @@ export async function getGasLimit( return (gasLimit * 11n) / 10n; // add a 10% buffer } + +export async function getBestNonce(providers: Wallet[], timeout = 3000): Promise { + const results: number[] = []; + + const promises = providers.map((provider) => provider.getNonce().then((nonce) => results.push(nonce))); + + // wait for either: 1. all providers requests are settled 2. or timeout + await Promise.any([Promise.allSettled(promises), new Promise((resolve) => setTimeout(resolve, timeout))]); + + if (results.length === 0) { + throw new Error(`None of providers returned nonce in ${timeout} ms`); + } + + return Math.max(...results); +} From 3d1b1fc2f645ac26d6439c5cfea5dd7f8ea6bf4e Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 01:37:09 +0200 Subject: [PATCH 2/9] get best nonce new rules --- src/lib/__tests__/getBestNonce.spec.ts | 157 +++++++++++++++++++++++++ src/lib/contracts/utils.ts | 55 +++++++-- 2 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 src/lib/__tests__/getBestNonce.spec.ts diff --git a/src/lib/__tests__/getBestNonce.spec.ts b/src/lib/__tests__/getBestNonce.spec.ts new file mode 100644 index 0000000000..4777c85666 --- /dev/null +++ b/src/lib/__tests__/getBestNonce.spec.ts @@ -0,0 +1,157 @@ +import { getBestNonce } from "../contracts/utils"; + +// Mocks for Wallet providers +class MockWallet { + nonce: number; + success: boolean; + timeout: number; + + constructor(nonce, success = true, timeout = 0) { + this.nonce = nonce; + this.success = success; + this.timeout = timeout; + } + + getNonce() { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (this.success) { + resolve(this.nonce); + } else { + reject(new Error("Failed to get nonce")); + } + }, this.timeout); + }); + } +} + +describe("getBestNonce", () => { + jest.useFakeTimers(); + + beforeEach(() => { + jest.spyOn(console, "error").mockImplementation(jest.fn()); + }); + + test("Should resolve to the highest nonce from successful responses under timeout", () => { + const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200), new MockWallet(3, true, 300)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + expect(res).resolves.toBe(3); + }); + + test("Should resolve to the highest nonce from successful responses under timeout", () => { + const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200), new MockWallet(3, true, 300)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + expect(res).resolves.toBe(3); + }); + + test("Should resolve to the highest nonce from successful responses, excluding failed ones", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, true, 200), + new MockWallet(3, false, 300), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + expect(res).resolves.toBe(2); + }); + + test("Should resolve with the only successful response when others fail", () => { + const providers: any[] = [ + new MockWallet(1, false, 100), + new MockWallet(2, true, 200), + new MockWallet(3, false, 300), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + expect(res).resolves.toBe(2); + }); + + test("Should throw error when all providers fail", () => { + const providers: any[] = [ + new MockWallet(1, false, 100), + new MockWallet(2, false, 200), + new MockWallet(3, false, 300), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + res.catch((error) => { + expect(error).toBeDefined(); + }); + }); + + test("Should resolve to the highest nonce under intermediate timeouts", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, true, 200), + new MockWallet(3, true, 1200), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(1100); + expect(res).resolves.toBe(2); + }); + + test("Should resolve to the highest nonce even if later response is delayed", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, false, 900), + new MockWallet(3, true, 1200), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(1300); + expect(res).resolves.toBe(3); + }); + + test("Should resolve to the first nonce if later responses fail", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, false, 900), + new MockWallet(3, false, 1200), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(1300); + expect(res).resolves.toBe(1); + }); + + test("Should resolve to nonce from the provider that responds within timeout", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, true, 4000), + new MockWallet(3, true, 5100), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(4100); + expect(res).resolves.toBe(2); + }); + + test("Should resolve to the first nonce when later providers fail", () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, false, 200), + new MockWallet(3, true, 5100), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(3100); + expect(res).resolves.toBe(1); + }); + + test("Should throw error when all providers respond past their timeout", () => { + const providers: any[] = [ + new MockWallet(1, true, 6000), + new MockWallet(2, true, 7000), + new MockWallet(3, true, 8000), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(5100); + res.catch((error) => { + expect(error).toBeDefined(); + }); + }); + + // Clean up timers after each test + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.clearAllTimers(); + }); +}); diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 316d080a39..7aa5012f9f 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -47,17 +47,54 @@ export async function getGasLimit( return (gasLimit * 11n) / 10n; // add a 10% buffer } -export async function getBestNonce(providers: Wallet[], timeout = 3000): Promise { - const results: number[] = []; +export function getBestNonce(providers: Wallet[]): Promise { + return new Promise(async (resolve, reject) => { + const results: number[] = []; - const promises = providers.map((provider) => provider.getNonce().then((nonce) => results.push(nonce))); + let pending = providers.length; + let threshold = 3; - // wait for either: 1. all providers requests are settled 2. or timeout - await Promise.any([Promise.allSettled(promises), new Promise((resolve) => setTimeout(resolve, timeout))]); + const handleSuccess = () => resolve(Math.max(...results)); + const handleFailure = () => reject("Failed to fetch nonce from any provider"); - if (results.length === 0) { - throw new Error(`None of providers returned nonce in ${timeout} ms`); - } + const checkExit = () => { + if (!pending) { + if (results.length) { + handleSuccess(); + } else { + handleFailure(); + } + } + + if (results.length >= threshold) { + handleSuccess(); + } + }; + + providers.forEach((provider, i) => + provider + .getNonce() + .then((nonce) => results.push(nonce)) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(`Error fetching nonce from provider ${i}: ${error.message}`); + }) + .finally(() => pending--) + .finally(checkExit) + ); + + setTimeout(() => { + threshold = 2; + checkExit(); + }, 1000); + + setTimeout(() => { + threshold = 1; + checkExit(); + }, 3000); - return Math.max(...results); + setTimeout(() => { + handleFailure(); + }, 5000); + }); } From 7e48eba5aab7357706ae5447ffffd67fa743413b Mon Sep 17 00:00:00 2001 From: micky Date: Fri, 31 May 2024 22:08:01 +0200 Subject: [PATCH 3/9] TESTIN COMMIT --- src/config/chains.ts | 2 +- src/lib/contracts/callContract.tsx | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/config/chains.ts b/src/config/chains.ts index 7531b42a98..35ccbce8b1 100644 --- a/src/config/chains.ts +++ b/src/config/chains.ts @@ -168,7 +168,7 @@ const constants = { }, }; -const ALCHEMY_WHITELISTED_DOMAINS = ["gmx.io", "app.gmx.io"]; +const ALCHEMY_WHITELISTED_DOMAINS = ["gmx.io", "app.gmx.io", "1ct-best-nonce.gmx-interface.pages.dev"]; export const RPC_PROVIDERS = { [ETH_MAINNET]: ["https://rpc.ankr.com/eth"], diff --git a/src/lib/contracts/callContract.tsx b/src/lib/contracts/callContract.tsx index 18f2552557..3a88cfaf2a 100644 --- a/src/lib/contracts/callContract.tsx +++ b/src/lib/contracts/callContract.tsx @@ -61,7 +61,24 @@ export async function callContract( const customSignerContracts = opts.customSigners?.map((signer) => contract.connect(signer)) || []; - const txnCalls = [contract, ...customSignerContracts].map(async (cntrct) => { + const toCall: any = []; + + // @ts-expect-error + if (!window.disableBrowserWalletRpc) { + toCall.push(contract); + } + + // @ts-expect-error + if (!window.disablePublicRpc) { + toCall.push(customSignerContracts[0]); + } + + // @ts-expect-error + if (!window.disableFallbackRpc) { + toCall.push(customSignerContracts[1]); + } + + const txnCalls = toCall.map(async (cntrct) => { const txnInstance = { ...txnOpts }; txnInstance.gasLimit = opts.gasLimit ? opts.gasLimit : await getGasLimit(cntrct, method, params, opts.value); From 21c0348bb0c1ffc06725f0d8fcf507c4463962de Mon Sep 17 00:00:00 2001 From: micky Date: Mon, 3 Jun 2024 17:41:48 +0200 Subject: [PATCH 4/9] TEST console.logs --- src/lib/contracts/callContract.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/lib/contracts/callContract.tsx b/src/lib/contracts/callContract.tsx index 3a88cfaf2a..a71d3262b8 100644 --- a/src/lib/contracts/callContract.tsx +++ b/src/lib/contracts/callContract.tsx @@ -7,6 +7,7 @@ import { getErrorMessage } from "./transactionErrors"; import { getGasLimit, setGasPrice, getBestNonce } from "./utils"; import { ReactNode } from "react"; import React from "react"; +import { ARBITRUM } from "config/chains"; export async function callContract( chainId: number, @@ -65,20 +66,20 @@ export async function callContract( // @ts-expect-error if (!window.disableBrowserWalletRpc) { - toCall.push(contract); + toCall.push({ contract, caption: "Browser Wallet RPC" }); } // @ts-expect-error if (!window.disablePublicRpc) { - toCall.push(customSignerContracts[0]); + toCall.push({ contract: customSignerContracts[0], caption: "Public RPC" }); } // @ts-expect-error if (!window.disableFallbackRpc) { - toCall.push(customSignerContracts[1]); + toCall.push({ contract: customSignerContracts[1], caption: "Fallback RPC" }); } - const txnCalls = toCall.map(async (cntrct) => { + const txnCalls = toCall.map(async ({ contract: cntrct, caption }) => { const txnInstance = { ...txnOpts }; txnInstance.gasLimit = opts.gasLimit ? opts.gasLimit : await getGasLimit(cntrct, method, params, opts.value); @@ -89,9 +90,18 @@ export async function callContract( await setGasPrice(txnInstance, cntrct.runner.provider, chainId); - return cntrct[method](...params, txnInstance); + return cntrct[method](...params, txnInstance).then((res) => { + if (chainId === ARBITRUM) { + // eslint-disable-next-line no-console + console.log(`Transaction sent via ${caption}`, res); + } + return res; + }); }); + // eslint-disable-next-line no-console + console.log("All RPC calls: ", txnCalls); + const res = await Promise.any(txnCalls).catch(({ errors }) => { if (errors.length > 1) { // eslint-disable-next-line no-console From 7e76c567b4255c6886f9f503ab1bc5f1e7326a08 Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 01:43:39 +0200 Subject: [PATCH 5/9] ONE MORE TEST COMMIT --- src/lib/contracts/callContract.tsx | 19 ++++++++++++++++++- src/lib/contracts/utils.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/lib/contracts/callContract.tsx b/src/lib/contracts/callContract.tsx index a71d3262b8..dc19d4d93a 100644 --- a/src/lib/contracts/callContract.tsx +++ b/src/lib/contracts/callContract.tsx @@ -47,9 +47,26 @@ export async function callContract( } if (opts.customSigners) { + const wallets: Wallet[] = []; + + // @ts-expect-error + if (!window.disableBrowserWalletRpc) { + wallets.push(wallet); + } + + // @ts-expect-error + if (!window.disablePublicRpc) { + wallets.push(opts.customSigners[0]); + } + + // @ts-expect-error + if (!window.disableFallbackRpc) { + wallets.push(opts.customSigners[1]); + } + // If we send the transaction to multiple RPCs simultaneously, // we should specify a fixed nonce to avoid possible txn duplication. - txnOpts.nonce = await getBestNonce([wallet, ...opts.customSigners]); + txnOpts.nonce = await getBestNonce(wallets); } if (opts.showPreliminaryMsg && !opts.hideSentMsg) { diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 7aa5012f9f..0f56d12643 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -53,8 +53,16 @@ export function getBestNonce(providers: Wallet[]): Promise { let pending = providers.length; let threshold = 3; + let shown = false; - const handleSuccess = () => resolve(Math.max(...results)); + const handleSuccess = () => { + if (!shown) { + shown = true; + // eslint-disable-next-line no-console + console.log("Nonces been received: ", results); + } + resolve(Math.max(...results)); + }; const handleFailure = () => reject("Failed to fetch nonce from any provider"); const checkExit = () => { From 17fcdefbab889055923cb0ea060b8df306a1410a Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 16:01:46 +0200 Subject: [PATCH 6/9] add 1s nonce solution with tests --- src/lib/__tests__/getBestNonce.spec.ts | 136 +++++++++++++++++++------ src/lib/contracts/utils.ts | 84 ++++++++------- 2 files changed, 143 insertions(+), 77 deletions(-) diff --git a/src/lib/__tests__/getBestNonce.spec.ts b/src/lib/__tests__/getBestNonce.spec.ts index 4777c85666..4bb83c4c91 100644 --- a/src/lib/__tests__/getBestNonce.spec.ts +++ b/src/lib/__tests__/getBestNonce.spec.ts @@ -26,27 +26,19 @@ class MockWallet { } describe("getBestNonce", () => { - jest.useFakeTimers(); - beforeEach(() => { + jest.useFakeTimers(); jest.spyOn(console, "error").mockImplementation(jest.fn()); }); - test("Should resolve to the highest nonce from successful responses under timeout", () => { - const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200), new MockWallet(3, true, 300)]; - const res = getBestNonce(providers); - jest.advanceTimersByTime(400); - expect(res).resolves.toBe(3); - }); - - test("Should resolve to the highest nonce from successful responses under timeout", () => { + test("Should resolve to the highest nonce from successful responses under timeout", async () => { const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200), new MockWallet(3, true, 300)]; const res = getBestNonce(providers); jest.advanceTimersByTime(400); expect(res).resolves.toBe(3); }); - test("Should resolve to the highest nonce from successful responses, excluding failed ones", () => { + test("Should resolve to the highest nonce from successful responses, excluding failed ones", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, true, 200), @@ -57,7 +49,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(2); }); - test("Should resolve with the only successful response when others fail", () => { + test("Should resolve with the only successful response when others fail", async () => { const providers: any[] = [ new MockWallet(1, false, 100), new MockWallet(2, true, 200), @@ -68,7 +60,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(2); }); - test("Should throw error when all providers fail", () => { + test("Should throw error when all providers fail", async () => { const providers: any[] = [ new MockWallet(1, false, 100), new MockWallet(2, false, 200), @@ -81,62 +73,92 @@ describe("getBestNonce", () => { }); }); - test("Should resolve to the highest nonce under intermediate timeouts", () => { + test("Should resolve to the highest nonce under intermediate timeouts", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, true, 200), - new MockWallet(3, true, 1200), + new MockWallet(3, true, 1300), ]; const res = getBestNonce(providers); - jest.advanceTimersByTime(1100); + jest.advanceTimersByTime(300); + await waitOneTick(); + jest.advanceTimersByTime(1200); expect(res).resolves.toBe(2); }); - test("Should resolve to the highest nonce even if later response is delayed", () => { + test("Should resolve to the highest nonce even if later response is delayed", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, false, 900), - new MockWallet(3, true, 1200), + new MockWallet(3, true, 1000), ]; const res = getBestNonce(providers); - jest.advanceTimersByTime(1300); + jest.advanceTimersByTime(1100); expect(res).resolves.toBe(3); }); - test("Should resolve to the first nonce if later responses fail", () => { + test("Should resolve to the first nonce if later responses fail", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, false, 900), - new MockWallet(3, false, 1200), + new MockWallet(3, false, 1000), ]; const res = getBestNonce(providers); - jest.advanceTimersByTime(1300); + jest.advanceTimersByTime(1100); expect(res).resolves.toBe(1); }); - test("Should resolve to nonce from the provider that responds within timeout", () => { + test("Should resolve to nonce from the provider that responds within timeout", async () => { + const providers: any[] = [ + new MockWallet(1, false, 100), + new MockWallet(2, false, 200), + new MockWallet(3, true, 4800), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(4900); + expect(res).resolves.toBe(3); + }); + + test("Should resolve to the first nonce when later providers fail", async () => { const providers: any[] = [ new MockWallet(1, true, 100), - new MockWallet(2, true, 4000), - new MockWallet(3, true, 5100), + new MockWallet(2, false, 300), + new MockWallet(3, true, 1300), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(200); + await waitOneTick(); + jest.advanceTimersByTime(1200); + expect(res).resolves.toBe(1); + }); + + test("", async () => { + const providers: any[] = [ + new MockWallet(1, true, 4000), + new MockWallet(2, true, 5800), + new MockWallet(3, true, 6700), ]; const res = getBestNonce(providers); jest.advanceTimersByTime(4100); - expect(res).resolves.toBe(2); + await waitOneTick(); + jest.advanceTimersByTime(6800); + await await expect(res).resolves.toBe(1); }); - test("Should resolve to the first nonce when later providers fail", () => { + test("", async () => { const providers: any[] = [ - new MockWallet(1, true, 100), - new MockWallet(2, false, 200), - new MockWallet(3, true, 5100), + new MockWallet(1, true, 4900), + new MockWallet(2, true, 6100), + new MockWallet(3, true, 6200), ]; const res = getBestNonce(providers); - jest.advanceTimersByTime(3100); + jest.advanceTimersByTime(4950); + await waitOneTick(); + jest.advanceTimersByTime(6000); expect(res).resolves.toBe(1); }); - test("Should throw error when all providers respond past their timeout", () => { + test("Should throw error when all providers respond past their timeout", async () => { const providers: any[] = [ new MockWallet(1, true, 6000), new MockWallet(2, true, 7000), @@ -149,9 +171,57 @@ describe("getBestNonce", () => { }); }); + test("", async () => { + const providers: any[] = [new MockWallet(1, true, 100)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(200); + expect(res).resolves.toBe(1); + }); + + test("", async () => { + const providers: any[] = [new MockWallet(1, true, 4900)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(4950); + await waitOneTick(); + jest.advanceTimersByTime(5000); + expect(res).resolves.toBe(1); + }); + + test("", async () => { + const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(300); + expect(res).resolves.toBe(2); + }); + + test("", async () => { + const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 1000)]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(1100); + expect(res).resolves.toBe(2); + }); + + test("", async () => { + const providers: any[] = [ + new MockWallet(1, true, 100), + new MockWallet(2, true, 200), + new MockWallet(3, true, 300), + new MockWallet(4, true, 500), + ]; + const res = getBestNonce(providers); + jest.advanceTimersByTime(400); + expect(res).resolves.toBe(3); + }); + // Clean up timers after each test afterEach(() => { jest.runOnlyPendingTimers(); - jest.clearAllTimers(); + jest.useRealTimers(); }); }); + +async function waitOneTick() { + jest.useRealTimers(); + await new Promise((resolve) => queueMicrotask(() => resolve(null))); + jest.useFakeTimers(); +} diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 0f56d12643..26c1d9734c 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -48,61 +48,57 @@ export async function getGasLimit( } export function getBestNonce(providers: Wallet[]): Promise { + const MAX_NONCE_NEEDED = 3; + const MAX_WAIT = 5000; + const ONE_MORE_WAIT = 1000; + return new Promise(async (resolve, reject) => { const results: number[] = []; + let resolved = false; - let pending = providers.length; - let threshold = 3; - let shown = false; + const handleResolve = () => { + resolved = true; - const handleSuccess = () => { - if (!shown) { - shown = true; - // eslint-disable-next-line no-console - console.log("Nonces been received: ", results); + if (results.length) { + resolve(Math.max(...results)); + } else { + reject(new Error("Failed to fetch nonce from any provider")); } - resolve(Math.max(...results)); }; - const handleFailure = () => reject("Failed to fetch nonce from any provider"); - - const checkExit = () => { - if (!pending) { - if (results.length) { - handleSuccess(); - } else { - handleFailure(); - } - } - if (results.length >= threshold) { - handleSuccess(); + let timer = setTimeout(handleResolve, MAX_WAIT); + + const setResolveTimeout = (t: number) => { + clearTimeout(timer); + + if (resolved) return; + + if (t) { + timer = setTimeout(handleResolve, t); + } else { + handleResolve(); } }; - providers.forEach((provider, i) => - provider - .getNonce() - .then((nonce) => results.push(nonce)) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(`Error fetching nonce from provider ${i}: ${error.message}`); - }) - .finally(() => pending--) - .finally(checkExit) + await Promise.all( + providers.map((provider, i) => + provider + .getNonce("pending") + .then((nonce) => results.push(nonce)) + .then(() => { + if (results.length === providers.length || results.length >= MAX_NONCE_NEEDED) { + setResolveTimeout(0); + } else { + setResolveTimeout(ONE_MORE_WAIT); + } + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(`Error fetching nonce from provider ${i}: ${error.message}`); + }) + ) ); - setTimeout(() => { - threshold = 2; - checkExit(); - }, 1000); - - setTimeout(() => { - threshold = 1; - checkExit(); - }, 3000); - - setTimeout(() => { - handleFailure(); - }, 5000); + setResolveTimeout(0); }); } From 01b46e80a0269864f06a237b2cff6673b942068e Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 16:12:39 +0200 Subject: [PATCH 7/9] TEST COMMIT --- src/lib/contracts/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 26c1d9734c..8d5e59e6ee 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -60,6 +60,8 @@ export function getBestNonce(providers: Wallet[]): Promise { resolved = true; if (results.length) { + // eslint-disable-next-line no-console + console.log("Nonces been received: ", results); resolve(Math.max(...results)); } else { reject(new Error("Failed to fetch nonce from any provider")); From 4233559917af158e8d5d9311c96db2ad3cc57b98 Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 16:32:00 +0200 Subject: [PATCH 8/9] fix testcase names --- src/lib/__tests__/getBestNonce.spec.ts | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lib/__tests__/getBestNonce.spec.ts b/src/lib/__tests__/getBestNonce.spec.ts index 4bb83c4c91..15733abc29 100644 --- a/src/lib/__tests__/getBestNonce.spec.ts +++ b/src/lib/__tests__/getBestNonce.spec.ts @@ -31,14 +31,14 @@ describe("getBestNonce", () => { jest.spyOn(console, "error").mockImplementation(jest.fn()); }); - test("Should resolve to the highest nonce from successful responses under timeout", async () => { + test("Case 1", async () => { const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200), new MockWallet(3, true, 300)]; const res = getBestNonce(providers); jest.advanceTimersByTime(400); expect(res).resolves.toBe(3); }); - test("Should resolve to the highest nonce from successful responses, excluding failed ones", async () => { + test("Case 2", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, true, 200), @@ -49,7 +49,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(2); }); - test("Should resolve with the only successful response when others fail", async () => { + test("Case 3", async () => { const providers: any[] = [ new MockWallet(1, false, 100), new MockWallet(2, true, 200), @@ -60,7 +60,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(2); }); - test("Should throw error when all providers fail", async () => { + test("Case 4", async () => { const providers: any[] = [ new MockWallet(1, false, 100), new MockWallet(2, false, 200), @@ -73,7 +73,7 @@ describe("getBestNonce", () => { }); }); - test("Should resolve to the highest nonce under intermediate timeouts", async () => { + test("Case 5", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, true, 200), @@ -86,7 +86,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(2); }); - test("Should resolve to the highest nonce even if later response is delayed", async () => { + test("Case 6", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, false, 900), @@ -97,7 +97,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(3); }); - test("Should resolve to the first nonce if later responses fail", async () => { + test("Case 7", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, false, 900), @@ -108,7 +108,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(1); }); - test("Should resolve to nonce from the provider that responds within timeout", async () => { + test("Case 8", async () => { const providers: any[] = [ new MockWallet(1, false, 100), new MockWallet(2, false, 200), @@ -119,7 +119,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(3); }); - test("Should resolve to the first nonce when later providers fail", async () => { + test("Case 9", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, false, 300), @@ -132,7 +132,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(1); }); - test("", async () => { + test("Case 10", async () => { const providers: any[] = [ new MockWallet(1, true, 4000), new MockWallet(2, true, 5800), @@ -145,7 +145,7 @@ describe("getBestNonce", () => { await await expect(res).resolves.toBe(1); }); - test("", async () => { + test("Case 11", async () => { const providers: any[] = [ new MockWallet(1, true, 4900), new MockWallet(2, true, 6100), @@ -158,7 +158,7 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(1); }); - test("Should throw error when all providers respond past their timeout", async () => { + test("Case 12", async () => { const providers: any[] = [ new MockWallet(1, true, 6000), new MockWallet(2, true, 7000), @@ -171,14 +171,14 @@ describe("getBestNonce", () => { }); }); - test("", async () => { + test("Case 13", async () => { const providers: any[] = [new MockWallet(1, true, 100)]; const res = getBestNonce(providers); jest.advanceTimersByTime(200); expect(res).resolves.toBe(1); }); - test("", async () => { + test("Case 14", async () => { const providers: any[] = [new MockWallet(1, true, 4900)]; const res = getBestNonce(providers); jest.advanceTimersByTime(4950); @@ -187,21 +187,21 @@ describe("getBestNonce", () => { expect(res).resolves.toBe(1); }); - test("", async () => { + test("Case 15", async () => { const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 200)]; const res = getBestNonce(providers); jest.advanceTimersByTime(300); expect(res).resolves.toBe(2); }); - test("", async () => { + test("Case 16", async () => { const providers: any[] = [new MockWallet(1, true, 100), new MockWallet(2, true, 1000)]; const res = getBestNonce(providers); jest.advanceTimersByTime(1100); expect(res).resolves.toBe(2); }); - test("", async () => { + test("Case 17", async () => { const providers: any[] = [ new MockWallet(1, true, 100), new MockWallet(2, true, 200), From 8cc2bcc62c5ae864ed9a5100d6c7c62843cf54f9 Mon Sep 17 00:00:00 2001 From: micky Date: Tue, 11 Jun 2024 16:37:28 +0200 Subject: [PATCH 9/9] fix naming --- src/lib/contracts/utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/contracts/utils.ts b/src/lib/contracts/utils.ts index 8d5e59e6ee..06f1c84dfd 100644 --- a/src/lib/contracts/utils.ts +++ b/src/lib/contracts/utils.ts @@ -68,15 +68,15 @@ export function getBestNonce(providers: Wallet[]): Promise { } }; - let timer = setTimeout(handleResolve, MAX_WAIT); + let timerId = setTimeout(handleResolve, MAX_WAIT); - const setResolveTimeout = (t: number) => { - clearTimeout(timer); + const setResolveTimeout = (time: number) => { + clearTimeout(timerId); if (resolved) return; - if (t) { - timer = setTimeout(handleResolve, t); + if (time) { + timerId = setTimeout(handleResolve, time); } else { handleResolve(); }