Prerequisites
Setup Smart Account Client
Send User Operations
- Alchemy
- ZeroDev
- Gelato
- Pimlico / Safe
- Biconomy
- Thirdweb
- Rhinestone
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { ModularAccountV2Client } from "@account-kit/smart-contracts";
import type { Address, Hash, Hex } from "viem";
interface UserOperationCall {
target: Address;
data?: Hex;
value?: bigint;
}
export function useSendUserOperation(client: ModularAccountV2Client | null) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (calls: UserOperationCall | UserOperationCall[]): Promise<Hash> => {
if (!client) throw new Error("Client not initialized");
setIsPending(true);
setError(null);
try {
const callsArray = Array.isArray(calls) ? calls : [calls];
const formattedCalls = callsArray.map((call) => ({
target: call.target,
data: call.data ?? "0x",
value: call.value ?? BigInt(0),
}));
const userOpHash = await client.sendUserOperation({
uo: formattedCalls.length === 1 ? formattedCalls[0] : formattedCalls,
});
const hash = await client.waitForUserOperationTransaction(userOpHash);
setTxHash(hash);
return hash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[client]
);
return { sendUserOperation, isPending, txHash, error };
}
Usage
Copy
Ask AI
import { useSmartAccountClient } from "./useSmartAccountClient";
import { useSendUserOperation } from "./useSendUserOperation";
import { parseEther } from "viem";
function SendTransaction() {
const { client, address } = useSmartAccountClient();
const { sendUserOperation, isPending, txHash } = useSendUserOperation(client);
const handleSend = async () => {
const hash = await sendUserOperation({
target: "0x...",
value: parseEther("0.001"),
});
console.log("Transaction:", hash);
};
return (
<div>
<p>Smart Account: {address}</p>
<button onClick={handleSend} disabled={isPending}>
{isPending ? "Sending..." : "Send 0.001 ETH"}
</button>
{txHash && <p>Tx: {txHash}</p>}
</div>
);
}
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { KernelAccountClient } from "@zerodev/sdk";
import type { Address, Hash, Hex } from "viem";
interface UserOperationCall {
to: Address;
data?: Hex;
value?: bigint;
}
export function useSendUserOperation(client: KernelAccountClient | null) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (calls: UserOperationCall | UserOperationCall[]): Promise<Hash> => {
if (!client) throw new Error("Client not initialized");
setIsPending(true);
setError(null);
try {
const callsArray = Array.isArray(calls) ? calls : [calls];
const hash = await client.sendTransaction({
calls: callsArray.map((call) => ({
to: call.to,
data: call.data ?? "0x",
value: call.value ?? BigInt(0),
})),
});
setTxHash(hash);
return hash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[client]
);
return { sendUserOperation, isPending, txHash, error };
}
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { GelatoSmartWalletClient } from "@gelatonetwork/smartwallet";
import type { Address, Hash, Hex } from "viem";
interface TransactionCall {
to: Address;
data?: Hex;
value?: bigint;
}
export function useSendUserOperation(client: GelatoSmartWalletClient | null) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (call: TransactionCall): Promise<Hash> => {
if (!client) throw new Error("Client not initialized");
setIsPending(true);
setError(null);
try {
const hash = await client.sendTransaction({
to: call.to,
value: call.value ?? BigInt(0),
data: call.data ?? "0x",
});
const receipt = await client.waitForTransactionReceipt({ hash });
setTxHash(receipt.transactionHash);
return receipt.transactionHash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[client]
);
return { sendUserOperation, isPending, txHash, error };
}
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { Address, Hash, Hex } from "viem";
interface UserOperationCall {
to: Address;
data?: Hex;
value?: bigint;
}
export function useSendUserOperation(client: any) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (calls: UserOperationCall | UserOperationCall[]): Promise<Hash> => {
if (!client) throw new Error("Client not initialized");
setIsPending(true);
setError(null);
try {
const callsArray = Array.isArray(calls) ? calls : [calls];
const userOpHash = await client.sendUserOperation({
calls: callsArray.map((call) => ({
to: call.to,
data: call.data ?? "0x",
value: call.value ?? BigInt(0),
})),
});
const receipt = await client.waitForUserOperationReceipt({ hash: userOpHash });
setTxHash(receipt.receipt.transactionHash);
return receipt.receipt.transactionHash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[client]
);
return { sendUserOperation, isPending, txHash, error };
}
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { Address, Hash, Hex } from "viem";
interface TransactionCall {
to: Address;
data?: Hex;
value?: string;
}
export function useSendUserOperation(client: any) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (call: TransactionCall): Promise<Hash> => {
if (!client) throw new Error("Client not initialized");
setIsPending(true);
setError(null);
try {
const userOpResponse = await client.sendTransaction(
{
to: call.to,
data: call.data ?? "0x",
value: call.value ?? "0",
},
{ paymasterServiceData: { mode: "SPONSORED" } }
);
const receipt = await userOpResponse.wait();
setTxHash(receipt.receipt.transactionHash);
return receipt.receipt.transactionHash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[client]
);
return { sendUserOperation, isPending, txHash, error };
}
useSendUserOperation.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import { sendTransaction } from "thirdweb";
import type { Address, Hash, Hex } from "viem";
interface TransactionCall {
to: Address;
data?: Hex;
value?: bigint;
}
export function useSendUserOperation(smartAccount: any, thirdwebClient: any, chain: any) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendUserOperation = useCallback(
async (call: TransactionCall): Promise<Hash> => {
if (!smartAccount) throw new Error("Smart account not initialized");
setIsPending(true);
setError(null);
try {
const result = await sendTransaction({
transaction: {
chain,
client: thirdwebClient,
to: call.to,
value: call.value ?? BigInt(0),
data: call.data ?? "0x",
},
account: smartAccount,
});
setTxHash(result.transactionHash as Hash);
return result.transactionHash as Hash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[smartAccount, thirdwebClient, chain]
);
return { sendUserOperation, isPending, txHash, error };
}
Rhinestone supports cross-chain transactions with automatic token bridging.
useSendTransaction.ts
Copy
Ask AI
import { useState, useCallback } from "react";
import type { Address, Hash, Hex } from "viem";
interface CrossChainCall {
to: Address;
data?: Hex;
value?: bigint;
}
interface TokenRequest {
address: Address;
amount: bigint;
chainId: number;
}
export function useSendTransaction(rhinestoneAccount: any) {
const [isPending, setIsPending] = useState(false);
const [txHash, setTxHash] = useState<Hash | null>(null);
const [error, setError] = useState<Error | null>(null);
const sendTransaction = useCallback(
async (
targetChain: number,
calls: CrossChainCall[],
tokenRequests?: TokenRequest[],
sourceChains?: number[]
): Promise<Hash> => {
if (!rhinestoneAccount) throw new Error("Account not initialized");
setIsPending(true);
setError(null);
try {
const transaction = await rhinestoneAccount.sendTransaction({
sourceChains: sourceChains || [],
targetChain: { chainId: targetChain },
calls: calls.map((call) => ({
to: call.to,
data: call.data ?? "0x",
value: call.value ?? BigInt(0),
})),
tokenRequests: tokenRequests || [],
sponsored: true,
});
const result = await rhinestoneAccount.waitForExecution(transaction);
const hash = (result?.fillTransactionHash || result?.transactionHash) as Hash;
setTxHash(hash);
return hash;
} catch (err) {
const error = err instanceof Error ? err : new Error("Failed to send");
setError(error);
throw error;
} finally {
setIsPending(false);
}
},
[rhinestoneAccount]
);
return { sendTransaction, isPending, txHash, error };
}
Cross-Chain Usage
Copy
Ask AI
import { useGlobalWallet } from "./useGlobalWallet";
import { useSendTransaction } from "./useSendTransaction";
import { parseUnits } from "viem";
function CrossChainDemo() {
const { client: rhinestoneAccount, address } = useGlobalWallet();
const { sendTransaction, isPending, txHash } = useSendTransaction(rhinestoneAccount);
const handleCrossChainSwap = async () => {
// Send USDC from Arbitrum to Base for a swap
const hash = await sendTransaction(
8453, // Target: Base
[{ to: "0xSwapContract...", data: "0x..." }],
[{ address: "0xUSDC...", amount: parseUnits("100", 6), chainId: 42161 }], // Source: Arbitrum USDC
[42161] // Source chains
);
console.log("Cross-chain tx:", hash);
};
return (
<div>
<p>Global Wallet: {address}</p>
<button onClick={handleCrossChainSwap} disabled={isPending}>
{isPending ? "Processing..." : "Cross-Chain Swap"}
</button>
{txHash && <p>Tx: {txHash}</p>}
</div>
);
}
Batched Transactions
Most 4337 providers support batching multiple calls into a single UserOperation:Copy
Ask AI
const hash = await sendUserOperation([
{ target: "0xToken...", data: approveCalldata },
{ target: "0xDex...", data: swapCalldata },
]);