Send Userops on Multiple Chains
Execute multiple operations across multiple chains in a single atomic transaction using the Compose sequencer.
import { useSmartAccount } from '@ssv-labs/compose-sdk/react';
import { composePreparedUserOps } from '@ssv-labs/compose-sdk';
import { prepareUserOperation } from 'viem/account-abstraction';
import { erc20Abi, encodeFunctionData } from 'viem';
import { rollupA, rollupB } from '@/wagmi/config';
import { useAccount } from 'wagmi';
import { useMutation } from '@tanstack/react-query';
function BatchOperationsMultiChain() {
const { isConnected } = useAccount();
// Get smart accounts for both chains
const smartAccountAQuery = useSmartAccount({
chainId: rollupA.id,
multiChainIds: [rollupA.id, rollupB.id],
});
const smartAccountBQuery = useSmartAccount({
chainId: rollupB.id,
multiChainIds: [rollupA.id, rollupB.id],
});
// Get smart accounts and public clients from query data
const smartAccountA = smartAccountAQuery.data?.account;
const publicClientA = smartAccountAQuery.data?.publicClient;
const smartAccountB = smartAccountBQuery.data?.account;
const publicClientB = smartAccountBQuery.data?.publicClient;
const batchMutation = useMutation({
mutationFn: async () => {
if (!smartAccountA || !publicClientA || !smartAccountB || !publicClientB) {
throw new Error('Smart accounts not ready');
}
const tokenAddress = '0x969b0ad5ffa2376E8C0f5e413D510a056416D627';
const spenderAddress = '0x1388C9619aCCcd1dfff0234626EDDA61413Be74e';
const recipientAddress = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
// Step 1: Create user operations for Chain A with multiple calls
const { userOp: userOpA } = await smartAccountA.createUserOp([
// Approve spender on Chain A
{
to: tokenAddress,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 10000000000000000000n]
})
},
// Transfer tokens on Chain A
{
to: tokenAddress,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, 5000000000000000000n]
})
}
]);
// Step 2: Create user operations for Chain B with multiple calls
const { userOp: userOpB } = await smartAccountB.createUserOp([
// Approve spender on Chain B
{
to: tokenAddress,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 20000000000000000000n]
})
},
// Transfer tokens on Chain B
{
to: tokenAddress,
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, 10000000000000000000n]
})
}
]);
// Step 3: Prepare user operations for both chains
const preparedUserOpA = await prepareUserOperation(publicClientA, userOpA);
const preparedUserOpB = await prepareUserOperation(publicClientB, userOpB);
// Step 4: Compose user operations from both chains
const { send, builds, explorerUrls } = await composePreparedUserOps([
{
account: smartAccountA,
publicClient: publicClientA,
userOp: preparedUserOpA,
},
{
account: smartAccountB,
publicClient: publicClientB,
userOp: preparedUserOpB,
},
]);
// Step 5: Send to sequencer and wait for receipts
const { wait } = await send();
const [receiptA, receiptB] = await wait();
return {
hashes: builds.map((b) => b.hash),
explorerUrls,
receipts: [receiptA, receiptB]
};
}
});
const handleBatch = () => {
batchMutation.mutate();
};
if (!isConnected) {
return <div>Please connect your wallet</div>;
}
if (smartAccountAQuery.isLoading || smartAccountBQuery.isLoading) {
return <div>Loading smart accounts...</div>;
}
return (
<div>
<button
onClick={handleBatch}
disabled={!smartAccountAQuery.data?.account || !smartAccountBQuery.data?.account || batchMutation.isPending}
>
{batchMutation.isPending ? 'Executing...' : 'Execute Multi-Chain Batch Operations'}
</button>
{batchMutation.isSuccess && (
<div>
<p>Multi-chain batch operations completed!</p>
<p>Transaction hashes:</p>
<ul>
{batchMutation.data.hashes.map((hash, index) => (
<li key={index}>
Chain {index === 0 ? 'A' : 'B'}: {hash}
</li>
))}
</ul>
{batchMutation.data.explorerUrls.map((url, index) => (
<a
key={index}
href={url}
target="_blank"
rel="noopener noreferrer"
>
View transaction on Chain {index === 0 ? 'A' : 'B'}
</a>
))}
</div>
)}
{batchMutation.isError && (
<div>
<p>Error: {batchMutation.error?.message}</p>
</div>
)}
</div>
);
}
Notes
- Both chains must have the same multi-chain validator configuration
- Operations execute atomically - if one fails, both fail
- The sequencer coordinates execution across chains
- Public clients must have Compose RPC schema support (handled automatically by the SDK)