Skip to content

Drop and replace failing user operations

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.

retry-user-operations.tsx
import React, { useState, useEffect } from "react";
import { Alert, View, Button } from "react-native";
import {
  createLightAccountClient,
  LightAccount,
} from "@account-kit/smart-contracts";
import { sepolia, alchemy } from "@account-kit/infra";
import { User } from "@account-kit/signer";
import { SmartAccountClient } from "@aa-sdk/core";
// import the signer
import { signer } from "./signer.ts";
 
export default function MyComponent() {
  const [client, setClient] = useState<SmartAccountClient | null>(null);
  const [user, setUser] = useState<User | null>(null);
  const [isSendingUserOperation, setIsSendingUserOperation] = useState(false);
 
  // Assume the user is already authenticated
  useEffect(() => {
    signer.getAuthDetails().then(setUser);
  }, []);
 
  useEffect(() => {
    if (user) {
      // Create a smart account client for the authenticated user
      createLightAccountClient({
        signer,
        chain: sepolia,
        transport: alchemy({ apiKey: "API_KEY" }),
      }).then((client) => {
        setClient(client);
      });
    }
  }, [user]);
 
  const handleSendUserOperation = async () => {
    // 0. Ensure the client is available before sending a user operation
    if (!client) return;
 
    // 1. Send a sponsored User Operation
    setIsSendingUserOperation(true);
    const { hash, request } = await client?.sendUserOperation({
      uo: {
        target: "0xTARGET_ADDRESS",
        data: "0x",
        value: 0n,
      },
      account: client.account,
    });
 
    try {
      // 2. Wait for the user operation to be mined
      const txHash = await client.waitForUserOperationTransaction({
        hash,
      });
 
      Alert.alert("User Operation Sent");
    } catch (error) {
      // 3. If the user operation fails, drop and replace it
      const { hash: newHash } = await client.dropAndReplaceUserOperation({
        uoToDrop: request,
      });
 
      // 4. Wait for the new user operation to be mined
      await client.waitForUserOperationTransaction({
        hash: newHash,
      });
    } finally {
      setIsSendingUserOperation(false);
    }
  };
 
  return (
    <View>
      <Button
        onClick={handleSendUserOperation}
        disabled={isSendingUserOperation}
        title={
          isSendingUserOperation
            ? "Sending..."
            : "Send Sponsored User Operation"
        }
      />
    </View>
  );
}

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.