Skip to content

Commit 827dc6d

Browse files
authored
refactor: rewrite the retries page (#1961)
1 parent 61dbda3 commit 827dc6d

File tree

2 files changed

+129
-60
lines changed

2 files changed

+129
-60
lines changed
Lines changed: 111 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
---
2-
title: Retry User Operations
3-
description: Learn how to use Drop and Replace to retry failing user operations.
2+
title: Retry Transactions
3+
description: Learn how to use drop and replace to retry failing transactions.
44
slug: wallets/transactions/retries
55
---
66

7+
<Markdown src="../../shared/infra/drop-and-replace-description.mdx" />
8+
79
<Tabs>
810
<Tab title="React">
9-
## What is Drop and Replace?
11+
Use the `useDropAndReplaceUserOperation()` hook to drop and replace. Retry a transaction by:
1012

11-
If fees change and your user operation gets stuck in the mempool, you can use drop and replace to resend the user operation with higher fees.
13+
1. Call `sendUserOperationAsync()` from `useSendUserOperation()` to send the initial transaction; it returns the pending hash
14+
2. Use the `useWaitForUserOperationTransaction()` hook to wait for the pending transaction
15+
3. If the transaction is stuck in the mempool, use the `useDropAndReplaceUserOperation()` hook to cancel the existing attempt and resubmit with an adjusted fee
1216

13-
Here's a quick example of how to use the `useDropAndReplaceUserOperation()` hook to drop and replace a user operation.
17+
The example below implements this flow and wraps it in a retry helper with a configurable number of attempts.
1418

1519
```tsx twoslash
1620
import React from "react";
1721
import { View, Pressable, Text } from "react-native";
1822
import {
23+
useWaitForUserOperationTransaction,
1924
useDropAndReplaceUserOperation,
2025
useSendUserOperation,
2126
useSmartAccountClient,
2227
} from "@account-kit/react-native";
2328

29+
// Configure how many drop-and-replace attempts to try if timeouts persist
30+
const MAX_REPLACEMENTS = 3;
31+
2432
export function ComponentWithDropAndReplaceUO() {
2533
const { client } = useSmartAccountClient({});
2634

@@ -29,6 +37,11 @@ slug: wallets/transactions/retries
2937
client,
3038
});
3139

40+
const {
41+
waitForUserOperationTransaction,
42+
isWaitingForUserOperationTransaction,
43+
} = useWaitForUserOperationTransaction({ client });
44+
3245
const { dropAndReplaceUserOperation, isDroppingAndReplacingUserOperation } =
3346
useDropAndReplaceUserOperation({
3447
client,
@@ -38,26 +51,62 @@ slug: wallets/transactions/retries
3851
onError: (error) => {
3952
// [optional] Do something with the error
4053
},
41-
// [optional] ...additional mutationArgs
4254
});
4355

56+
function waitForUserOperationTransactionPromise(hash: `0x${string}`) {
57+
return new Promise<`0x${string}`>((resolve, reject) => {
58+
waitForUserOperationTransaction(
59+
{ hash, retries: { maxRetries: 3, intervalMs: 5_000 } },
60+
{ onSuccess: resolve, onError: reject },
61+
);
62+
});
63+
}
64+
65+
// On failure retry and drop and replace, no backoff is required because a backoff is already
66+
// waited for in the waitForUserOperationTransaction function
67+
const waitUntilMinedWithRetries = async (params: {
68+
hash: `0x${string}`;
69+
request: any;
70+
maxReplacements: number;
71+
}): Promise<`0x${string}`> => {
72+
try {
73+
return await waitForUserOperationTransactionPromise(params.hash);
74+
} catch (e) {
75+
if (params.maxReplacements <= 0) throw e;
76+
const { hash: newHash } =
77+
await dropAndReplaceUserOperation({ uoToDrop: params.request });
78+
return waitUntilMinedWithRetries({
79+
hash: newHash as `0x${string}`,
80+
request: params.request,
81+
maxReplacements: params.maxReplacements - 1,
82+
});
83+
}
84+
};
85+
4486
return (
4587
<View>
4688
<Pressable
4789
onPress={async () => {
48-
const { request } = await sendUserOperationAsync({
90+
const { hash, request } = await sendUserOperationAsync({
4991
uo: {
5092
target: "0xTARGET_ADDRESS",
5193
data: "0x",
5294
value: 0n,
5395
},
5496
});
5597

56-
dropAndReplaceUserOperation({
57-
uoToDrop: request,
98+
const minedTxHash = await waitUntilMinedWithRetries({
99+
hash,
100+
request,
101+
maxReplacements: MAX_REPLACEMENTS,
58102
});
103+
console.log("Mined transaction hash:", minedTxHash);
59104
}}
60-
disabled={isSendingUserOperation || isDroppingAndReplacingUserOperation}
105+
disabled={
106+
isSendingUserOperation ||
107+
isDroppingAndReplacingUserOperation ||
108+
isWaitingForUserOperationTransaction
109+
}
61110
>
62111
<View>
63112
<Text>
@@ -74,45 +123,64 @@ slug: wallets/transactions/retries
74123
}
75124
```
76125

77-
You can also build a more complex retry logic in a case you want more control over how many times you want to retry a failed user operation.
78-
79126
</Tab>
80127
<Tab title="JavaScript">
81-
<Markdown src="../../shared/infra/drop-and-replace-description.mdx" />
82-
83-
<CodeBlocks>
84-
85-
```ts twoslash example.ts
86-
import { client } from "./client";
87-
88-
// 1. send a user operation
89-
const { hash, request } = await client.sendUserOperation({
90-
uo: {
91-
target: "0xTARGET_ADDRESS",
92-
data: "0x",
93-
value: 0n,
94-
},
95-
});
96-
97-
try {
98-
// 2. wait for it to be mined
99-
const txHash = await client.waitForUserOperationTransaction({ hash });
100-
} catch (e) {
101-
// 3. if it fails, resubmit the user operation via drop and replace
102-
const { hash: newHash } = await client.dropAndReplaceUserOperation({
103-
uoToDrop: request,
104-
});
128+
<div>
129+
Use the `Smart Account Client` directly to send a transaction, wait for it, and replace it if it times out. The example below shows how
130+
to instantiate and initialize the `Smart Account Client`.
105131

106-
// 4. wait for the new user operation to be mined
107-
await client.waitForUserOperationTransaction({ hash: newHash });
108-
}
109-
```
132+
<Markdown src="../../shared/infra/client.mdx" />
133+
134+
Once initialized:
135+
1. Send the initial transaction with `sendUserOperation()`; it returns the pending hash
136+
2. Wait for the pending transaction with `waitForUserOperationTransaction({ hash })`
137+
3. If `waitForUserOperationTransaction({ hash })` errors, drop and replace with `dropAndReplaceUserOperation()`
110138

111-
<Markdown src="../../shared/infra/client.mdx" />
139+
The example then wraps this flow in a retry helper with a configurable number of attempts.
112140

113-
</CodeBlocks>
141+
```ts twoslash
142+
import { client } from "./client";
114143

115-
In the above example, we only try to drop and replace once before failing completely, but you can build more complex retry logic using this combination of `waitForUserOperationTransaction` and `dropAndReplace`.
144+
const { hash, request } = await client.sendUserOperation({
145+
uo: {
146+
target: "0xTARGET_ADDRESS",
147+
data: "0x",
148+
value: 0n,
149+
},
150+
});
151+
152+
// On failure retry and drop and replace, no backoff is required because a backoff is already
153+
// waited for in the waitForUserOperationTransaction function
154+
const waitUntilMinedWithRetries = async (params: {
155+
hash: `0x${string}`;
156+
request: any;
157+
maxReplacements: number;
158+
}): Promise<`0x${string}`> => {
159+
try {
160+
return await client.waitForUserOperationTransaction({
161+
hash: params.hash,
162+
retries: { maxRetries: 3, intervalMs: 5_000, multiplier: 1 },
163+
});
164+
} catch (e) {
165+
if (params.maxReplacements <= 0) throw e;
166+
const { hash: newHash } =
167+
await client.dropAndReplaceUserOperation({ uoToDrop: params.request });
168+
return waitUntilMinedWithRetries({
169+
hash: newHash as `0x${string}`,
170+
request: params.request,
171+
maxReplacements: params.maxReplacements - 1,
172+
});
173+
}
174+
};
175+
176+
const minedTxHash = await waitUntilMinedWithRetries({
177+
hash,
178+
request,
179+
maxReplacements: 3,
180+
});
181+
console.log("Mined transaction hash:", minedTxHash);
182+
```
183+
</div>
116184

117185
</Tab>
118186
</Tabs>
Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
In the previous guides, we learned how to send user operations with gas sponsorship, but what happens when a user operation fails to mine? In this guide,
2-
we'll cover how to use drop and replace to resend failing user operations and ensure they get mined.
1+
Network fees can change quickly. When fees rise, pending transactions may become underpriced and stop progressing. Without retries, these transactions may never confirm. Use retries to keep your app reliable.
32

4-
## What is drop and replace?
3+
**Why retries matter**
54

6-
If fees change and your user operation gets stuck in the mempool, you can use drop and replace to resend the user operation with higher fees. This is most useful
7-
when used in combination with [`waitForUserOperationTransaction`](/wallets/reference/aa-sdk/core/functions/waitForUserOperationTransaction) to ensure the transaction is mined
8-
and then resend the user operation with higher fees if waiting times out.
5+
- **Production reliability**: Proper wait and retry mechanisms ensure transactions don’t get stuck and fail silently.
6+
- **Fee volatility**: Gas markets fluctuate constantly, especially on testnets, so transactions can become underpriced and take much longer to mine.
7+
- **Automatic recovery**: Retries automatically replace stuck transactions with higher fees so they continue to progress.
98

10-
Drop and replace works by resubmitting a user operation with the greater of:
9+
## How it works
1110

12-
1. 10% higher fees
13-
2. The current minimum fees
11+
Drop and replace resubmits a transaction with whichever is greater:
1412

15-
We export a `dropAndReplace` function from `@aa-sdk/core` that you can use to handle this flow for you and is automatically added to the Smart Account Client.
13+
- 10% higher fees
14+
- The current minimum fees
1615

17-
## How to drop and replace effectively
16+
The SDK exports a `dropAndReplace` function from `@aa-sdk/core`. It is available on the `Smart Account Client`, so use it directly.
1817

19-
Let's run through an example that uses drop and replace if waiting for a user operation to mine times out.
18+
Here’s a common production flow:
19+
20+
1. Send a transaction with `sendUserOperation`
21+
2. Wait for confirmation using `waitForUserOperationTransaction`
22+
3. If waiting times out, call `dropAndReplace` to resend the transaction with higher fees
2023

2124
<Warning>
22-
If sponsoring gas, like in the example below, each call to `sendUserOperation`
23-
and `dropAndReplace` will generate pending gas sponsorships in your dashboard.
24-
This can result in you hitting your gas manager limit. At the moment, pending
25-
sponsorships expire after 10 minutes of inactivity, but it is possible that
26-
retrying excessively can temporarily exhaust your sponsorship limits.
25+
If you sponsor gas, each retry creates a pending gas sponsorship in your
26+
dashboard. Pending sponsorships expire after 10 minutes of inactivity.
27+
Excessive retries can temporarily exhaust your sponsorship limit.
2728
</Warning>

0 commit comments

Comments
 (0)