Skip to content

Commit

Permalink
feat(core): use exponential backoff on waitForUserOperationTransaction (
Browse files Browse the repository at this point in the history
  • Loading branch information
avasisht23 authored and rthomare committed Jul 29, 2023
1 parent 08e5c83 commit e7fa847
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 9 deletions.
98 changes: 98 additions & 0 deletions packages/core/src/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Transaction } from "viem";
import { polygonMumbai } from "viem/chains";
import {
afterEach,
beforeEach,
describe,
it,
vi,
type SpyInstance,
} from "vitest";
import { SmartAccountProvider } from "../provider/base.js";
import type { UserOperationReceipt } from "../types.js";

describe("Base Tests", () => {
let retryMsDelays: number[] = [];

const providerMock = new SmartAccountProvider(
"ALCHEMY_RPC_URL",
"0xENTRYPOINT_ADDRESS",
polygonMumbai
);

const givenGetUserOperationFailsNTimes = (times: number) => {
const mock = vi.spyOn(providerMock, "getUserOperationReceipt");
for (let i = 0; i < times; i++) {
mock.mockImplementationOnce(() => {
if (i < times - 1) {
return Promise.reject("Failed request, must retry");
}

return Promise.resolve({
receipt: { transactionHash: "0xMOCK_USER_OP_RECEIPT" },
} as unknown as UserOperationReceipt);
});
}
return mock;
};

const thenExpectRetriesToBe = async (
expectedRetryMsDelays: number[],
expectedMockCalls: number,
getUserOperationReceiptMock: SpyInstance<
[hash: `0x${string}`],
Promise<UserOperationReceipt>
>
) => {
expect(retryMsDelays).toEqual(expectedRetryMsDelays);
expect(getUserOperationReceiptMock).toHaveBeenCalledTimes(
expectedMockCalls
);
};

beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(global, "setTimeout").mockImplementation(
// @ts-ignore: Mock implementation doesn't need to return a Timeout.
(callback: () => void, ms) => {
if (ms != null) {
retryMsDelays.push(ms);
}
callback();
}
);
vi.spyOn(global.Math, "random").mockImplementation(() => 0.5);
vi.spyOn(providerMock, "getTransaction").mockImplementation(() => {
return Promise.resolve({
hash: "0xMOCK_TXN_HASH",
} as unknown as Transaction);
});
});

afterEach(() => {
vi.restoreAllMocks();
retryMsDelays = [];
});

it("should apply only the initial delay for waitForUserOperationTransaction", async () => {
const getUserOperationReceiptMock = givenGetUserOperationFailsNTimes(1);
await providerMock.waitForUserOperationTransaction("0xTHIS_IS_A_TEST");
thenExpectRetriesToBe([2_050], 1, getUserOperationReceiptMock);
});

it("should retry twice with exponential delay for waitForUserOperationTransaction", async () => {
const getUserOperationReceiptMock = givenGetUserOperationFailsNTimes(2);
await providerMock.waitForUserOperationTransaction("0xTHIS_IS_A_TEST");
thenExpectRetriesToBe([2_050, 3_050], 2, getUserOperationReceiptMock);
});

it("should retry thrice with exponential delay for waitForUserOperationTransaction", async () => {
const getUserOperationReceiptMock = givenGetUserOperationFailsNTimes(3);
await providerMock.waitForUserOperationTransaction("0xTHIS_IS_A_TEST");
thenExpectRetriesToBe(
[2_050, 3_050, 4_550],
3,
getUserOperationReceiptMock
);
});
});
35 changes: 26 additions & 9 deletions packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type Hash,
type RpcTransactionRequest,
type Transport,
type Transaction,
} from "viem";
import { arbitrum, arbitrumGoerli } from "viem/chains";
import { BaseSmartContractAccount } from "../account/base.js";
Expand Down Expand Up @@ -43,17 +44,22 @@ export const noOpMiddleware: AccountMiddlewareFn = async (
) => struct;
export interface SmartAccountProviderOpts {
/**
* The maximum number of times tot try fetching a transaction receipt before giving up
* The maximum number of times to try fetching a transaction receipt before giving up (default: 5)
*/
txMaxRetries?: number;

/**
* The interval in milliseconds to wait between retries while waiting for tx receipts
* The interval in milliseconds to wait between retries while waiting for tx receipts (default: 2_000n)
*/
txRetryIntervalMs?: number;

/**
* used when computing the fees for a user operation (default: 1000000000n)
* The mulitplier on interval length to wait between retries while waiting for tx receipts (default: 1.5)
*/
txRetryMulitplier?: number;

/**
* used when computing the fees for a user operation (default: 100_000_000n)
*/
minPriorityFeePerBid?: bigint;
}
Expand All @@ -75,6 +81,8 @@ export class SmartAccountProvider<
{
private txMaxRetries: number;
private txRetryIntervalMs: number;
private txRetryMulitplier: number;

minPriorityFeePerBid: bigint;
rpcClient: PublicErc4337Client<Transport>;

Expand All @@ -87,6 +95,8 @@ export class SmartAccountProvider<
) {
this.txMaxRetries = opts?.txMaxRetries ?? 5;
this.txRetryIntervalMs = opts?.txRetryIntervalMs ?? 2000;
this.txRetryMulitplier = opts?.txRetryMulitplier ?? 1.5;

this.minPriorityFeePerBid =
opts?.minPriorityFeePerBid ??
minPriorityFeePerBidDefaults.get(chain.id) ??
Expand Down Expand Up @@ -180,17 +190,20 @@ export class SmartAccountProvider<

waitForUserOperationTransaction = async (hash: Hash): Promise<Hash> => {
for (let i = 0; i < this.txMaxRetries; i++) {
const txRetryIntervalWithJitterMs =
this.txRetryIntervalMs * Math.pow(this.txRetryMulitplier, i) +
Math.random() * 100;

await new Promise((resolve) =>
setTimeout(resolve, this.txRetryIntervalMs)
setTimeout(resolve, txRetryIntervalWithJitterMs)
);
const receipt = await this.rpcClient
.getUserOperationReceipt(hash as `0x${string}`)
const receipt = await this.getUserOperationReceipt(hash as `0x${string}`)
// TODO: should maybe log the error?
.catch(() => null);
if (receipt) {
return this.rpcClient
.getTransaction({ hash: receipt.receipt.transactionHash })
.then((x) => x.hash);
return this.getTransaction(receipt.receipt.transactionHash).then(
(x) => x.hash
);
}
}

Expand All @@ -205,6 +218,10 @@ export class SmartAccountProvider<
return this.rpcClient.getUserOperationReceipt(hash);
};

getTransaction = (hash: Hash): Promise<Transaction> => {
return this.rpcClient.getTransaction({ hash: hash });
};

sendUserOperation = async (
data: UserOperationCallData | BatchUserOperationCallData
): Promise<SendUserOperationResult> => {
Expand Down

0 comments on commit e7fa847

Please sign in to comment.