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

1ct best nonce debug #1006

Open
wants to merge 9 commits into
base: release-29
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
2 changes: 1 addition & 1 deletion src/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
227 changes: 227 additions & 0 deletions src/lib/__tests__/getBestNonce.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
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", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(console, "error").mockImplementation(jest.fn());
});

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("Case 2", async () => {
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("Case 3", async () => {
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("Case 4", async () => {
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("Case 5", async () => {
const providers: any[] = [
new MockWallet(1, true, 100),
new MockWallet(2, true, 200),
new MockWallet(3, true, 1300),
];
const res = getBestNonce(providers);
jest.advanceTimersByTime(300);
await waitOneTick();
jest.advanceTimersByTime(1200);
expect(res).resolves.toBe(2);
});

test("Case 6", async () => {
const providers: any[] = [
new MockWallet(1, true, 100),
new MockWallet(2, false, 900),
new MockWallet(3, true, 1000),
];
const res = getBestNonce(providers);
jest.advanceTimersByTime(1100);
expect(res).resolves.toBe(3);
});

test("Case 7", async () => {
const providers: any[] = [
new MockWallet(1, true, 100),
new MockWallet(2, false, 900),
new MockWallet(3, false, 1000),
];
const res = getBestNonce(providers);
jest.advanceTimersByTime(1100);
expect(res).resolves.toBe(1);
});

test("Case 8", 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("Case 9", async () => {
const providers: any[] = [
new MockWallet(1, true, 100),
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("Case 10", 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);
await waitOneTick();
jest.advanceTimersByTime(6800);
await await expect(res).resolves.toBe(1);
});

test("Case 11", async () => {
const providers: any[] = [
new MockWallet(1, true, 4900),
new MockWallet(2, true, 6100),
new MockWallet(3, true, 6200),
];
const res = getBestNonce(providers);
jest.advanceTimersByTime(4950);
await waitOneTick();
jest.advanceTimersByTime(6000);
expect(res).resolves.toBe(1);
});

test("Case 12", async () => {
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();
});
});

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("Case 14", 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("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("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("Case 17", 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.useRealTimers();
});
});

async function waitOneTick() {
jest.useRealTimers();
await new Promise((resolve) => queueMicrotask(() => resolve(null)));
jest.useFakeTimers();
}
55 changes: 50 additions & 5 deletions src/lib/contracts/callContract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ 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";
import { ARBITRUM } from "config/chains";

export async function callContract(
chainId: number,
Expand Down Expand Up @@ -46,8 +47,26 @@ 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();
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(wallets);
}

if (opts.showPreliminaryMsg && !opts.hideSentMsg) {
Expand All @@ -60,7 +79,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, caption: "Browser Wallet RPC" });
}

// @ts-expect-error
if (!window.disablePublicRpc) {
toCall.push({ contract: customSignerContracts[0], caption: "Public RPC" });
}

// @ts-expect-error
if (!window.disableFallbackRpc) {
toCall.push({ contract: customSignerContracts[1], caption: "Fallback RPC" });
}

const txnCalls = toCall.map(async ({ contract: cntrct, caption }) => {
const txnInstance = { ...txnOpts };

txnInstance.gasLimit = opts.gasLimit ? opts.gasLimit : await getGasLimit(cntrct, method, params, opts.value);
Expand All @@ -71,9 +107,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
Expand Down
60 changes: 59 additions & 1 deletion src/lib/contracts/utils.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand Down Expand Up @@ -46,3 +46,61 @@ export async function getGasLimit(

return (gasLimit * 11n) / 10n; // add a 10% buffer
}

export function getBestNonce(providers: Wallet[]): Promise<number> {
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;

const handleResolve = () => {
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"));
}
};

let timerId = setTimeout(handleResolve, MAX_WAIT);

const setResolveTimeout = (time: number) => {
clearTimeout(timerId);

if (resolved) return;

if (time) {
timerId = setTimeout(handleResolve, time);
} else {
handleResolve();
}
};

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}`);
})
)
);

setResolveTimeout(0);
});
}