Skip to content

Commit f795f45

Browse files
arcoravenfarhanW3
andauthored
chore: Cancel userOps after 1 hour (#507)
* chore: Cancel userOps after 1 hour * update 4844 type to 3 * use secret key * Update: userOp Timeout completed (#508) * Updated UserOp Mine Flow to handle errors * updated comments --------- Co-authored-by: Farhan Khwaja <132962163+farhanW3@users.noreply.github.com>
1 parent 2c61ede commit f795f45

File tree

4 files changed

+153
-91
lines changed

4 files changed

+153
-91
lines changed

src/utils/date.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
*/
77
export const msSince = (from: Date) => {
88
const ms = new Date().getTime() - from.getTime();
9-
return Math.min(ms, 0);
9+
return Math.max(ms, 0);
1010
};

src/utils/sdk.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,22 @@ export const thirdwebClientId = sha256HexSync(
77
).slice(0, 32);
88

99
export const thirdwebClient = createThirdwebClient({
10-
clientId: thirdwebClientId,
10+
secretKey: env.THIRDWEB_API_SECRET_KEY,
1111
});
12+
13+
/**
14+
* Helper functions to handle v4 -> v5 SDK migration.
15+
*/
16+
17+
export const toTransactionStatus = (status: "success" | "reverted"): number =>
18+
status === "success" ? 1 : 0;
19+
20+
export const toTransactionType = (
21+
type: "legacy" | "eip1559" | "eip2930" | "eip4844",
22+
): number => {
23+
if (type === "legacy") return 0;
24+
if (type === "eip1559") return 1;
25+
if (type === "eip2930") return 2;
26+
if (type === "eip4844") return 3;
27+
throw new Error(`Unexpected transaction type ${type}`);
28+
};

src/worker/tasks/updateMinedTx.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "../../utils/usage";
1717
import { WebhookData, sendWebhooks } from "../../utils/webhook";
1818

19-
const MEMPOOL_DURATION_TIMEOUT_MS = 1000 * 60 * 60;
19+
const CANCEL_DEADLINE_MS = 1000 * 60 * 60; // 1 hour
2020

2121
export const updateMinedTx = async () => {
2222
try {
@@ -44,9 +44,7 @@ export const updateMinedTx = async () => {
4444

4545
// Cancel transactions submitted over 1 hour ago.
4646
// @TODO: move duration to config
47-
const sentAt = new Date(tx.sentAt!);
48-
const ageInMilliseconds = Date.now() - sentAt.getTime();
49-
if (ageInMilliseconds > MEMPOOL_DURATION_TIMEOUT_MS) {
47+
if (msSince(tx.sentAt!) > CANCEL_DEADLINE_MS) {
5048
try {
5149
await cancelTransactionAndUpdate({
5250
queueId: tx.id,
@@ -93,7 +91,7 @@ export const updateMinedTx = async () => {
9391
chainId: tx.chainId || undefined,
9492
transactionHash: tx.transactionHash || undefined,
9593
provider: provider.connection.url || undefined,
96-
msSinceSend: Date.now() - tx.sentAt!.getTime(),
94+
msSinceSend: msSince(tx.sentAt!),
9795
},
9896
action: UsageEventTxActionEnum.ErrorTx,
9997
});
Lines changed: 131 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,185 @@
1-
import { getBlock } from "@thirdweb-dev/sdk";
21
import { ERC4337EthersSigner } from "@thirdweb-dev/wallets/dist/declarations/src/evm/connectors/smart-wallet/lib/erc4337-signer";
2+
import { providers } from "ethers";
3+
import {
4+
defineChain,
5+
eth_getBlockByNumber,
6+
eth_getTransactionByHash,
7+
eth_getTransactionReceipt,
8+
getRpcClient,
9+
} from "thirdweb";
310
import { prisma } from "../../db/client";
411
import { getSentUserOps } from "../../db/transactions/getSentUserOps";
512
import { updateTx } from "../../db/transactions/updateTx";
613
import { TransactionStatus } from "../../server/schemas/transaction";
714
import { getSdk } from "../../utils/cache/getSdk";
15+
import { msSince } from "../../utils/date";
816
import { logger } from "../../utils/logger";
17+
import {
18+
thirdwebClient,
19+
toTransactionStatus,
20+
toTransactionType,
21+
} from "../../utils/sdk";
922
import {
1023
ReportUsageParams,
1124
UsageEventTxActionEnum,
1225
reportUsage,
1326
} from "../../utils/usage";
1427
import { WebhookData, sendWebhooks } from "../../utils/webhook";
1528

29+
const CANCEL_DEADLINE_MS = 1000 * 60 * 60; // 1 hour
30+
1631
export const updateMinedUserOps = async () => {
1732
try {
1833
const sendWebhookForQueueIds: WebhookData[] = [];
1934
const reportUsageForQueueIds: ReportUsageParams[] = [];
2035
await prisma.$transaction(
2136
async (pgtx) => {
2237
const userOps = await getSentUserOps({ pgtx });
23-
2438
if (userOps.length === 0) {
2539
return;
2640
}
2741

28-
// TODO: Improve spaghetti code...
29-
const updatedUserOps = (
30-
await Promise.all(
31-
userOps.map(async (userOp) => {
32-
const sdk = await getSdk({
33-
chainId: parseInt(userOp.chainId!),
34-
walletAddress: userOp.signerAddress!,
35-
accountAddress: userOp.accountAddress!,
36-
});
37-
const signer = sdk.getSigner() as ERC4337EthersSigner;
42+
const promises = userOps.map(async (userOp) => {
43+
try {
44+
if (
45+
!userOp.sentAt ||
46+
!userOp.signerAddress ||
47+
!userOp.accountAddress ||
48+
!userOp.userOpHash
49+
) {
50+
return;
51+
}
3852

39-
const userOpReceipt =
40-
await signer.smartAccountAPI.getUserOpReceipt(
41-
signer.httpRpcClient,
42-
userOp.userOpHash!,
43-
3000,
44-
);
53+
const sdk = await getSdk({
54+
chainId: parseInt(userOp.chainId),
55+
walletAddress: userOp.signerAddress,
56+
accountAddress: userOp.accountAddress,
57+
});
58+
const signer = sdk.getSigner() as ERC4337EthersSigner;
59+
let userOpReceipt: providers.TransactionReceipt | undefined;
60+
try {
61+
// Get userOp receipt.
62+
// If no receipt, try again later (or cancel userOps after 1 hour).
63+
// Else the transaction call was submitted to mempool.
64+
userOpReceipt = await signer.smartAccountAPI.getUserOpReceipt(
65+
signer.httpRpcClient,
66+
userOp.userOpHash,
67+
3_000, // 3 seconds
68+
);
69+
} catch (error) {
70+
// Exception is thrown when userOp is not found/null
71+
logger({
72+
service: "worker",
73+
level: "error",
74+
queueId: userOp.id,
75+
message: "Failed to get receipt for UserOp",
76+
error,
77+
});
78+
}
4579

46-
if (!userOpReceipt) {
47-
// If no receipt was received, return undefined to filter out tx
48-
return undefined;
80+
if (!userOpReceipt) {
81+
if (msSince(userOp.sentAt) > CANCEL_DEADLINE_MS) {
82+
await updateTx({
83+
pgtx,
84+
queueId: userOp.id,
85+
data: {
86+
status: TransactionStatus.Errored,
87+
errorMessage: "Transaction timed out.",
88+
},
89+
});
4990
}
50-
const _sdk = await getSdk({
51-
chainId: parseInt(userOp.chainId!),
52-
});
91+
return;
92+
}
5393

54-
const tx = await signer.provider!.getTransaction(
55-
userOpReceipt.transactionHash,
56-
);
57-
const txReceipt = await _sdk
58-
.getProvider()
59-
.getTransactionReceipt(tx.hash);
60-
const minedAt = new Date(
61-
(
62-
await getBlock({
63-
block: tx.blockNumber!,
64-
network: sdk.getProvider(),
65-
})
66-
).timestamp * 1000,
67-
);
94+
const chain = defineChain(parseInt(userOp.chainId));
95+
const rpcRequest = getRpcClient({
96+
client: thirdwebClient,
97+
chain,
98+
});
6899

69-
return {
70-
...userOp,
71-
blockNumber: tx.blockNumber!,
72-
minedAt,
73-
onChainTxStatus: txReceipt.status,
74-
transactionHash: txReceipt.transactionHash,
75-
transactionType: tx.type,
76-
gasLimit: tx.gasLimit.toString(),
77-
maxFeePerGas: tx.maxFeePerGas?.toString(),
78-
maxPriorityFeePerGas: tx.maxPriorityFeePerGas?.toString(),
79-
provider: signer.httpRpcClient.bundlerUrl,
80-
};
81-
}),
82-
)
83-
).filter((userOp) => !!userOp);
100+
// Get the transaction receipt.
101+
// If no receipt, try again later.
102+
// Else the transaction call was confirmed onchain.
103+
const transaction = await eth_getTransactionByHash(rpcRequest, {
104+
hash: userOpReceipt.transactionHash as `0x${string}`,
105+
});
106+
const transactionReceipt = await eth_getTransactionReceipt(
107+
rpcRequest,
108+
{ hash: transaction.hash },
109+
);
110+
if (!transactionReceipt) {
111+
// If no receipt, try again later.
112+
return;
113+
}
84114

85-
await Promise.all(
86-
updatedUserOps.map(async (userOp) => {
115+
let minedAt = new Date();
116+
try {
117+
const block = await eth_getBlockByNumber(rpcRequest, {
118+
blockNumber: transactionReceipt.blockNumber,
119+
includeTransactions: false,
120+
});
121+
minedAt = new Date(Number(block.timestamp) * 1000);
122+
} catch (e) {}
123+
124+
// Update the userOp transaction as mined.
87125
await updateTx({
88126
pgtx,
89-
queueId: userOp!.id,
127+
queueId: userOp.id,
90128
data: {
91129
status: TransactionStatus.Mined,
92-
minedAt: userOp!.minedAt,
93-
blockNumber: userOp!.blockNumber,
94-
onChainTxStatus: userOp!.onChainTxStatus,
95-
transactionHash: userOp!.transactionHash,
96-
transactionType: userOp!.transactionType || undefined,
97-
gasLimit: userOp!.gasLimit || undefined,
98-
maxFeePerGas: userOp!.maxFeePerGas || undefined,
99-
maxPriorityFeePerGas: userOp!.maxPriorityFeePerGas || undefined,
100-
gasPrice: userOp!.gasPrice || undefined,
130+
minedAt,
131+
blockNumber: Number(transactionReceipt.blockNumber),
132+
onChainTxStatus: toTransactionStatus(transactionReceipt.status),
133+
transactionHash: transactionReceipt.transactionHash,
134+
transactionType: toTransactionType(transaction.type),
135+
gasLimit: userOp.gasLimit ?? undefined,
136+
maxFeePerGas: transaction.maxFeePerGas?.toString(),
137+
maxPriorityFeePerGas:
138+
transaction.maxPriorityFeePerGas?.toString(),
139+
gasPrice: transaction.gasPrice?.toString(),
101140
},
102141
});
103142

104143
logger({
105144
service: "worker",
106145
level: "info",
107-
queueId: userOp!.id,
108-
message: `Updated with receipt`,
146+
queueId: userOp.id,
147+
message: "Updated with receipt",
109148
});
110149
sendWebhookForQueueIds.push({
111-
queueId: userOp!.id,
150+
queueId: userOp.id,
112151
status: TransactionStatus.Mined,
113152
});
114153
reportUsageForQueueIds.push({
115154
input: {
116-
fromAddress: userOp!.fromAddress || undefined,
117-
toAddress: userOp!.toAddress || undefined,
118-
value: userOp!.value || undefined,
119-
chainId: userOp!.chainId || undefined,
120-
userOpHash: userOp!.userOpHash || undefined,
121-
onChainTxStatus: userOp!.onChainTxStatus,
122-
functionName: userOp!.functionName || undefined,
123-
extension: userOp!.extension || undefined,
124-
provider: userOp!.provider || undefined,
125-
msSinceSend:
126-
userOp!.minedAt.getTime() - userOp!.sentAt!.getTime(),
155+
fromAddress: userOp.fromAddress ?? undefined,
156+
toAddress: userOp.toAddress ?? undefined,
157+
value: userOp.value ?? undefined,
158+
chainId: userOp.chainId,
159+
userOpHash: userOp.userOpHash ?? undefined,
160+
onChainTxStatus: toTransactionStatus(transactionReceipt.status),
161+
functionName: userOp.functionName ?? undefined,
162+
extension: userOp.extension ?? undefined,
163+
provider: signer.httpRpcClient.bundlerUrl,
164+
msSinceSend: msSince(userOp.sentAt!),
127165
},
128166
action: UsageEventTxActionEnum.MineTx,
129167
});
130-
}),
131-
);
168+
} catch (err) {
169+
logger({
170+
service: "worker",
171+
level: "error",
172+
queueId: userOp.id,
173+
message: "Failed to update receipt for UserOp ",
174+
error: err,
175+
});
176+
}
177+
});
178+
179+
await Promise.all(promises);
132180
},
133181
{
134-
timeout: 5 * 60000,
182+
timeout: 5 * 60 * 1000, // 5 minutes
135183
},
136184
);
137185

@@ -141,9 +189,8 @@ export const updateMinedUserOps = async () => {
141189
logger({
142190
service: "worker",
143191
level: "error",
144-
message: `Failed to update receipts`,
192+
message: "Failed to batch update receipts",
145193
error: err,
146194
});
147-
return;
148195
}
149196
};

0 commit comments

Comments
 (0)