0

Optimistic Transactions

Previous

An experimental hook for optimistic transaction execution using Abstract's unstable_sendRawTransactionWithDetailedOutput endpoint to get instant transaction feedback.

Installation

pnpm dlx shadcn@latest add "https://build.abs.xyz/r/use-optimistic-write-contract.json"

Usage

"use client";
 
import { useOptimisticWriteContract } from "@/hooks/use-optimistic-write-contract";
import { parseAbi } from "viem";
 
export default function MintButton() {
  const { writeContract, isPending } = useOptimisticWriteContract();
 
  const handleMint = () => {
    writeContract({
      abi: parseAbi(["function mint(address,uint256) external"]),
      address: "0xC4822AbB9F05646A9Ce44EFa6dDcda0Bf45595AA",
      functionName: "mint",
      args: ["0x273B3527BF5b607dE86F504fED49e1582dD2a1C6", BigInt(1)],
      onSuccess: (data, startTime) => {
        console.log(`Transaction confirmed in ${Date.now() - startTime}ms`);
      },
    });
  };
 
  return (
    <button onClick={handleMint} disabled={isPending}>
      {isPending ? "Minting..." : "Mint Token"}
    </button>
  );
}

What's included

This command installs the following files:

use-optimistic-write-contract.ts

The main hook that provides optimistic contract write functionality with instant feedback.

"use client";

import { useState, useCallback } from "react";
import { useAccount, useWalletClient } from "wagmi";
import { type WriteContractParameters } from "wagmi/actions";
import { encodeFunctionData } from "viem";
import type { Abi, ContractFunctionName } from "viem";
import type { OptimisticTransactionResponse } from "@/types/optimistic-transactions";
import { sendRawTransactionWithDetailedOutput } from "../lib/abstract-api";

type OptimisticWriteConfig<
  TConfig extends WriteContractParameters = WriteContractParameters
> = TConfig & {
  onSuccess?: (data: OptimisticTransactionResponse, startTime: number) => void;
  onError?: (error: Error) => void;
};

export function useOptimisticWriteContract() {
  const { address } = useAccount();
  const { data: walletClient } = useWalletClient();

  const [isPending, setIsPending] = useState(false);
  const [data, setData] = useState<OptimisticTransactionResponse>();
  const [error, setError] = useState<Error>();

  const writeContract = useCallback(
    async <TConfig extends WriteContractParameters>(
      config: OptimisticWriteConfig<TConfig>
    ) => {
      if (!address || !walletClient) throw new Error("Wallet not connected");

      setIsPending(true);
      setError(undefined);
      setData(undefined);

      try {
        // Encode function data and prepare transaction request
        const { onSuccess, onError, ...contractParams } = config;
        const txData = encodeFunctionData({
          abi: contractParams.abi as Abi,
          functionName:
            contractParams.functionName as ContractFunctionName<Abi>,
          args: contractParams.args as unknown[] | undefined,
        });

        const request = await walletClient.prepareTransactionRequest({
          to: contractParams.address,
          data: txData,
        });

        // Sign transaction (this triggers wallet approval popup)
        const signedTransaction = await walletClient.signTransaction(request);

        // Start timing AFTER user approval
        const startTime = Date.now();

        // Send to Abstract's optimistic endpoint
        const result = await sendRawTransactionWithDetailedOutput(
          signedTransaction
        );

        setData(result);
        config.onSuccess?.(result, startTime);
        return result;
      } catch (err) {
        const error = err instanceof Error ? err : new Error("Unknown error");
        console.error(`❌ [Optimistic] Error:`, error.message);
        setError(error);
        config.onError?.(error);
        throw error;
      } finally {
        setIsPending(false);
      }
    },
    [address, walletClient]
  );

  const writeContractSync = useCallback(
    <TConfig extends WriteContractParameters>(
      config: OptimisticWriteConfig<TConfig>
    ) => {
      writeContract(config).catch(() => {
        // Errors already handled by onError callback and internal error state
      });
    },
    [writeContract]
  );

  const reset = useCallback(() => {
    setData(undefined);
    setError(undefined);
    setIsPending(false);
  }, []);

  return {
    writeContract: writeContractSync,
    writeContractAsync: writeContract,
    isPending,
    isLoading: isPending, // For backward compatibility
    data,
    error,
    isSuccess: !!data,
    isError: !!error,
    reset,
  };
}

abstract-api.ts

API client for Abstract's optimistic transaction endpoint with error handling and user-friendly error messages.

import type { 
  AbstractRpcRequest, 
  AbstractRpcResponse, 
  OptimisticTransactionResponse 
} from "./types"

const ABSTRACT_API_URL = "https://api.testnet.abs.xyz"

export async function sendRawTransactionWithDetailedOutput(
  signedTransaction: `0x${string}`
): Promise<OptimisticTransactionResponse> {
  const request: AbstractRpcRequest = {
    jsonrpc: "2.0",
    id: 1,
    method: "unstable_sendRawTransactionWithDetailedOutput",
    params: [signedTransaction],
  }

  const response = await fetch(ABSTRACT_API_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(request),
  })

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }

  const data: AbstractRpcResponse = await response.json()

  if (data.error) {
    // Parse common error messages to make them more user-friendly
    const errorMessage = data.error.message || ""
    
    let humanReadableError = ""

    if (errorMessage.includes("insufficient funds")) {
      humanReadableError = "ETH balance too low."
    } else if (errorMessage.includes("known transaction")) {
      humanReadableError = "Nonce issue. Please refresh."
    } else if (errorMessage.includes("nonce too low")) {
      humanReadableError = "Nonce issue. Please refresh."
    } else if (errorMessage.includes("gas required exceeds allowance")) {
      humanReadableError = "Gas issue. Please refresh."
    } else if (errorMessage.includes("replacement transaction underpriced")) {
      humanReadableError = "Gas issue. Please refresh."
    } else if (errorMessage.includes("max fee per gas less than block base fee")) {
      humanReadableError = "Transaction fee too low for current network conditions"
    } else {
      // For unknown errors, use a generic message with the error code
      humanReadableError = `Transaction failed (Error: ${errorMessage})`
    }

    throw new Error(humanReadableError)
  }

  if (!data.result || !data.result.transactionHash) {
    throw new Error(
      "Transaction submission did not return a transaction hash. Please try again."
    )
  }

  return data.result
}

types/optimistic-transactions.ts

TypeScript type definitions for optimistic transaction responses and API structures.

export interface OptimisticTransactionResponse {
  transactionHash: `0x${string}`
  storageLogs: StorageLog[]
  events: OptimisticEvent[]
}

export interface StorageLog {
  address: `0x${string}`
  key: `0x${string}`
  writtenValue: `0x${string}`
}

export interface OptimisticEvent {
  address: `0x${string}`
  topics: `0x${string}`[]
  data: `0x${string}`
  blockHash: null
  blockNumber: null
  l1BatchNumber: `0x${string}`
  transactionHash: `0x${string}`
  transactionIndex: `0x${string}`
  logIndex: null
  transactionLogIndex: null
  logType: null
  removed: false
}

export interface AbstractRpcRequest {
  jsonrpc: "2.0"
  id: number
  method: "unstable_sendRawTransactionWithDetailedOutput"
  params: [string] // signed transaction hex
}

export interface AbstractRpcResponse {
  jsonrpc: "2.0"
  id: number
  result?: OptimisticTransactionResponse
  error?: {
    code: number
    message: string
  }
}