Drop and Replace

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, we’ll cover how to use drop and replace to resend failing user operations and ensure they get mined.

What is drop and replace?

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 when used in combination with waitForUserOperationTransaction to ensure the transaction is mined and then resend the user operation with higher fees if waiting times out.

Drop and replace works by resubmitting a user operation with the greater of:

  1. 10% higher fees
  2. The current minimum fees

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.

How to drop and replace effectively

Let’s run through an example that uses drop and replace if waiting for a user operation to mine times out.

If sponsoring gas, like in the example below, each call to sendUserOperation and dropAndReplace will generate pending gas sponsorships in your dashboard. This can result in you hitting your gas manager limit. At the moment, pending sponsorships expire after 10 minutes of inactivity, but it is possible that retrying excessively can temporarily exhaust your sponsorship limits.

import { 
import client
client
} from "./client";
// 1. send a user operation const {
const hash: any
hash
,
const request: any
request
} = await
import client
client
.
any
sendUserOperation
({
uo: { target: string; data: string; value: bigint; }
uo
: {
target: string
target
: "0xTARGET_ADDRESS",
data: string
data
: "0x",
value: bigint
value
: 0n,
}, }); try { // 2. wait for it to be mined const
const txHash: any
txHash
= await
import client
client
.
any
waitForUserOperationTransaction
({
hash: any
hash
});
} catch (
var e: unknown
e
) {
// 3. if it fails, resubmit the user operation via drop and replace const {
any
hash
:
const newHash: any
newHash
} = await
import client
client
.
any
dropAndReplaceUserOperation
({
uoToDrop: any
uoToDrop
:
const request: any
request
,
}); // 4. wait for the new user operation to be mined await
import client
client
.
any
waitForUserOperationTransaction
({
hash: any
hash
:
const newHash: any
newHash
});
}
import {
  
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
function createAlchemySmartAccountClient<TChain extends Chain = Chain, TAccount extends SmartContractAccount | undefined = SmartContractAccount | undefined, TContext extends UserOperationContext | undefined = UserOperationContext | undefined>(params: AlchemySmartAccountClientConfig<TChain, TAccount, TContext>): AlchemySmartAccountClient<TChain, TAccount, Record<string, never>, TContext>
createAlchemySmartAccountClient
,
const sepolia: Chain
sepolia
,
} from "@account-kit/infra"; import {
function createLightAccount<TTransport extends Transport = Transport, TSigner extends SmartAccountSigner = SmartAccountSigner<any>, TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0">(config: CreateLightAccountParams<TTransport, TSigner, TLightAccountVersion>): Promise<LightAccount<TSigner, TLightAccountVersion>>
createLightAccount
} from "@account-kit/smart-contracts";
// You can replace this with any signer you'd like // We're using a LocalAccountSigner to generate a local key to sign with import {
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
} from "@aa-sdk/core";
import {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
const
const alchemyTransport: AlchemyTransport
alchemyTransport
=
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_API_KEY",
}); export const
const client: { [x: string]: never; account: LightAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>, "v2.0.0">; ... 85 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
client
=
createAlchemySmartAccountClient<Chain, LightAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>, "v2.0.0">, UserOperationContext | undefined>(params: AlchemySmartAccountClientConfig<...>): { ...; }
createAlchemySmartAccountClient
({
transport: AlchemyTransport

The RPC transport

transport
:
const alchemyTransport: AlchemyTransport
alchemyTransport
,
policyId?: string | string[] | undefined
policyId
: "YOUR_POLICY_ID",
chain?: Chain | undefined

Chain for the client.

chain
:
const sepolia: Chain
sepolia
,
account?: LightAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>, "v2.0.0"> | undefined
account
: await
createLightAccount<AlchemyTransport, LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>, "v2.0.0">(config: CreateLightAccountParams<...>): Promise<...>
createLightAccount
({
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
transport: AlchemyTransport
transport
:
const alchemyTransport: AlchemyTransport
alchemyTransport
,
signer: LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>
signer
:
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
.
LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>

Creates a LocalAccountSigner instance using the provided private key.

privateKeyToAccountSigner
(
function generatePrivateKey(): Hex
generatePrivateKey
()),
}), });

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.