Retry Transactions

Replace stuck transactions by preparing and sending calls again.

Key use cases:

  • Replace transactions stuck due to low gas prices
  • Speed up pending transactions by increasing gas fees
  • Override transactions that have been pending for too long
  • Cancel stuck transactions by replacing with a no-op

The Retry flow

  1. Send the initial transaction
  2. Monitor the transaction status
  3. If the transaction is stuck/pending too long, re-prepare the same call
  4. Send the transaction with higher gas to replace the stuck transaction
  5. The original transaction gets dropped from mempool

Prerequisites

If the original transaction is already being mined, the replacement transaction may be dropped. In this case, you won’t be able to retrieve data using the replacement’s call ID, and the original transaction will be included!

Required SDK version: ^v4.61.0

Use the useSendCalls hook to send and retry transactions. When retrying, don’t specify a nonceOverride - the client automatically uses the same nonce to replace the stuck transaction.

You’ll need both ALCHEMY_API_KEY and ALCHEMY_POLICY_ID environment variables set to follow along!

1import { config } from "@/app/config";
2import {
3 useSendCalls,
4 useSmartAccountClient,
5 useSmartWalletClient,
6} from "@account-kit/react";
7import { useState } from "react";
8
9export default function RetryTransaction() {
10 const { client } = useSmartAccountClient({});
11 const { sendCallsAsync } = useSendCalls({ client });
12 const walletClient = useSmartWalletClient({
13 account: client?.getAddress(),
14 });
15 const [isStuck, setIsStuck] = useState(false);
16
17 const handleSend = async () => {
18 if (!client) {
19 throw new Error("Smart account client not connected");
20 }
21 if (!walletClient) {
22 throw new Error("Wallet client not connected");
23 }
24
25 try {
26 const { ids } = await sendCallsAsync({
27 capabilities: {
28 paymasterService: {
29 policyId: config.policyId,
30 },
31 },
32 calls: [
33 {
34 to: "0x0000000000000000000000000000000000000000",
35 value: "0x00",
36 data: "0x",
37 },
38 ],
39 });
40
41 console.log("Transaction sent:", ids[0]);
42
43 // Check status after a delay
44 setTimeout(async () => {
45 const callStatus = await walletClient.getCallsStatus(ids[0]!);
46 console.log(callStatus.status);
47 if (callStatus.status === 100) {
48 setIsStuck(true);
49 console.log("Transaction is still in mempool");
50 }
51 }, 3000); // Check after 3 seconds
52 } catch (error) {
53 console.error(error);
54 }
55 };
56
57 const handleRetry = async () => {
58 if (!client) {
59 throw new Error("Smart account client not connected");
60 }
61 if (!walletClient) {
62 throw new Error("Wallet client not connected");
63 }
64
65 try {
66 // Re-send without nonceOverride to replace stuck transaction
67 const { ids } = await sendCallsAsync({
68 capabilities: {
69 paymasterService: {
70 policyId: config.policyId,
71 },
72 },
73 calls: [
74 {
75 to: "0x0000000000000000000000000000000000000000",
76 value: "0x00",
77 data: "0x",
78 },
79 ],
80 });
81
82 console.log("Replacement transaction sent:", ids[0]);
83 setIsStuck(false);
84
85 const result = await walletClient.waitForCallsStatus({ id: ids[0]! });
86 console.log("Replacement confirmed:", result);
87 } catch (error) {
88 console.error(error);
89 }
90 };
91
92 return (
93 <div>
94 <button onClick={handleSend}>Send Transaction</button>
95 {isStuck && (
96 <button onClick={handleRetry}>Replace Stuck Transaction</button>
97 )}
98 </div>
99 );
100}