An experimental hook for optimistic transaction execution using Abstract's unstable_sendRawTransactionWithDetailedOutput endpoint to get instant transaction feedback.
Track transaction confirmation speed
"use client"
import { useState, useEffect } from "react"
import { useOptimisticWriteContract } from "@/hooks/use-optimistic-write-contract"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader2, Clock, CheckCircle, Zap } from "lucide-react"
import { usePublicClient } from "wagmi"
const EXAMPLE_CONTRACT_ADDRESS = "0x1234567890123456789012345678901234567890" as const
const EXAMPLE_ABI = [
{
name: "increment",
type: "function",
stateMutability: "nonpayable",
inputs: [],
outputs: []
}
] as const
interface PerformanceMetrics {
optimisticTime?: number
blockTime?: number
startTime?: number
}
export default function UseOptimisticWriteContractDemo() {
const [metrics, setMetrics] = useState<PerformanceMetrics>({})
const [isConfirmed, setIsConfirmed] = useState(false)
const [currentOptimisticTime, setCurrentOptimisticTime] = useState(0)
const [currentBlockTime, setCurrentBlockTime] = useState(0)
const publicClient = usePublicClient()
const {
writeContract,
error,
isPending,
isSuccess,
} = useOptimisticWriteContract()
const pollForConfirmation = async (hash: string, startTime: number) => {
if (!publicClient) return
try {
const receipt = await publicClient.waitForTransactionReceipt({
hash: hash as `0x${string}`,
timeout: 30000
})
if (receipt) {
const blockTime = Date.now() - startTime
setMetrics(prev => ({
...prev,
blockTime
}))
setIsConfirmed(true)
}
} catch (error) {
console.error("❌ [Confirmation] Failed to get transaction receipt:", error)
}
}
// Timer effect for optimistic response
useEffect(() => {
if (metrics.startTime && !metrics.optimisticTime) {
const interval = setInterval(() => {
setCurrentOptimisticTime(Date.now() - metrics.startTime!)
}, 10)
return () => clearInterval(interval)
}
}, [metrics.startTime, metrics.optimisticTime])
// Timer effect for block confirmation
useEffect(() => {
if (isSuccess && !isConfirmed && metrics.startTime && !metrics.blockTime) {
const interval = setInterval(() => {
setCurrentBlockTime(Date.now() - metrics.startTime!)
}, 10)
return () => clearInterval(interval)
}
}, [isSuccess, isConfirmed, metrics.startTime, metrics.blockTime])
const handleSubmit = async () => {
setIsConfirmed(false)
setMetrics({})
setCurrentOptimisticTime(0)
setCurrentBlockTime(0)
try {
await writeContract({
address: EXAMPLE_CONTRACT_ADDRESS,
abi: EXAMPLE_ABI,
functionName: "increment",
onSuccess: (data, startTime) => {
const optimisticTime = Date.now() - startTime
setMetrics({
startTime,
optimisticTime
})
// Start polling for block confirmation
if (publicClient) {
pollForConfirmation(data.transactionHash, startTime)
}
},
onError: () => {
setMetrics({})
setIsConfirmed(false)
}
})
} catch (error) {
// Error already handled by onError callback
}
}
const formatTime = (ms?: number) => {
if (!ms) return "—"
return `${ms}ms`
}
return (
<div className="w-full max-w-2xl mx-auto">
<Card>
<CardHeader>
<CardTitle>Optimistic Transactions</CardTitle>
<CardDescription>
Experience instant transaction feedback with Abstract's optimistic execution
</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
{/* Action Button */}
<div className="flex justify-start -mt-2">
<Button
onClick={handleSubmit}
disabled={isPending}
size="lg"
className="min-w-[200px]"
>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Executing
</>
) : (
<>
<Zap className="mr-2 h-4 w-4" />
Execute Transaction
</>
)}
</Button>
</div>
{/* Error Message */}
{error && (
<div className="text-sm text-destructive bg-destructive/10 border border-destructive/20 p-4 rounded-lg text-center">
{error.message}
</div>
)}
{/* Performance Monitoring */}
<div className="space-y-6">
<div>
<h3 className="font-semibold text-sm">Performance Monitoring</h3>
<p className="text-xs text-muted-foreground">Track transaction confirmation speed</p>
</div>
<div className="grid grid-cols-2 gap-6">
{/* Optimistic Response */}
<div className="border rounded-lg p-4 bg-muted/20">
<div className="text-center space-y-3">
<div className="flex items-center justify-center gap-2">
{metrics.optimisticTime ? (
<CheckCircle className="w-4 h-4 text-foreground" />
) : metrics.startTime ? (
<Loader2 className="w-4 h-4 text-muted-foreground animate-spin" />
) : (
<Clock className="w-4 h-4 text-muted-foreground" />
)}
<span className="text-sm font-medium text-muted-foreground">Optimistic Response</span>
</div>
<div className="text-2xl font-mono font-semibold tabular-nums">
{metrics.optimisticTime
? formatTime(metrics.optimisticTime)
: metrics.startTime
? formatTime(currentOptimisticTime)
: formatTime(0)
}
</div>
</div>
</div>
{/* Block Confirmation */}
<div className="border rounded-lg p-4 bg-muted/20">
<div className="text-center space-y-3">
<div className="flex items-center justify-center gap-2">
{metrics.blockTime ? (
<CheckCircle className="w-4 h-4 text-foreground" />
) : (isSuccess && !isConfirmed) ? (
<Loader2 className="w-4 h-4 text-muted-foreground animate-spin" />
) : (
<Clock className="w-4 h-4 text-muted-foreground" />
)}
<span className="text-sm font-medium text-muted-foreground">Block Confirmation</span>
</div>
<div className="text-2xl font-mono font-semibold tabular-nums">
{metrics.blockTime
? formatTime(metrics.blockTime)
: (isSuccess && !isConfirmed)
? formatTime(currentBlockTime)
: formatTime(0)
}
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
pnpm dlx shadcn@latest add "https://build.abs.xyz/r/use-optimistic-write-contract.json"
"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>
);
}
This command installs the following files:
/hooks
use-optimistic-write-contract.ts
- Main hook for optimistic contract writes/types
abstract-api.ts
- Abstract API client for optimistic transactionstypes.ts
- TypeScript type definitionsuse-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
}
}