Batch Transactions
Batched transactions allow you to perform multiple transactions in one single on-chain transaction.
Try Retropool app, a no-loss donation tool that leverages batch transactions
Overview
If you are building a dapp, you communicate the actions to the wallet that the user performs. Instead of sending a single transaction one after the other, you can send an array
of transactions, and the wallet will execute the bundle.
EIP-5792 allows us to standardize JSON-RPC methods for Dapps to communicate bundle calls to wallets using:
wallet_sendFunctionCallBundle
wallet_getBundleStatus
wallet_showBundleStatus
Most smart contract wallets will batch transactions atomilly; meaning that all transactions in the batch must succeed. Otherwise, if any of the batched transactions fails then they are all canceled.
Quickstart
This guide shows you how to build a dapp that leverages batch transactions. We will show you how to communicate bundle calls to a Smart Wallet and build a dApp that levearge on click interfaces.
This is what we're going to do: Build a react dApp that send 1 wei to two different addresses in a single transction
Prerequisites
- Download a Smart Wallet to test with.
- Node and a package manager (yarn or npm)
- A account with some Göerli ETH
Try a simple live demo using Candide Wallet and WalletConnect.
Source code can be run under a minute on Github
Step 1: Get setup
- Install create-react-app:
npx create-react-app batch-transaction-dapp-demo
cd batch-transaction-dapp-demo
npm start
- Install dependencies. We will be using web3-onboard for this demo
npm install @web3-onboard/react @web3-onboard/walletconnect
- Create the Connect Wallet button component in a new file
ConnectWallet.js
import { useConnectWallet } from "@web3-onboard/react";
// create Connect Button for web3onboard
export default function ConnectWallet() {
const [{ wallet, connecting }, connect] = useConnectWallet();
if (wallet?.provider) {
return null;
}
return (
<div>
<button
disabled={connecting}
onClick={() => connect()}
className="button-connect"
>
Connect Wallet
</button>
<div>
<p>Connect with a Smart Wallet</p>
</div>
<p>⚠️ This demo is on Goerli ⚠️</p>
</div>
);
}
- Wrap your App with
Web3OnboardProvider
inApp.js
and add your ConnectWallet Button
import { Web3OnboardProvider, init } from "@web3-onboard/react";
import walletConnectModule from "@web3-onboard/walletconnect";
import ConnectWallet from "./ConnectWallet";
// define walletconnect init options
const wcV2InitOptions = {
projectId: "db8f68cd5b030a694622fb4b4ffc2647", // walletconnect project ID
requiredChains: [5], // goerli
additionalOptionalMethods: [
"wallet_sendFunctionCallBundle",
"wallet_showBundleStatus",
"wallet_getBundleStatus",
], // here we are telling the wallet we will be using these methods
dappUrl: "https://github.com/candidelabs/",
};
const walletConnect = walletConnectModule(wcV2InitOptions);
// web3onboard init
const web3Onboard = init({
theme: "dark",
apiKey: "1730eff0-9d50-4382-a3fe-89f0d34a2070", // blocknative api key
wallets: [walletConnect],
chains: [
{
id: "5",
token: "ETH",
label: "Goerli",
rpcUrl: "https://goerli.infura.io/v3/7ce42d92316d41c7a60100949be6adad",
},
],
appMetadata: {
name: "Batch Transaction Demo",
icon: "https://www.iconbolt.com/iconsets/ant-design-fill/code-sandbox-square.svg",
description: "Batch multiple transactions into a single one",
},
});
export default function App() {
return (
<>
<Web3OnboardProvider web3Onboard={web3Onboard}>
<ConnectWallet />
</Web3OnboardProvider>
</>
);
}
Step2: Batch Transaction
- Create a new component called BatchTransactions.js. This is where we will include all our logic.
import React from "react";
import { useConnectWallet } from "@web3-onboard/react";
const BatchTransactions = () => {
const [{ wallet }] = useConnectWallet();
const account = wallet?.accounts[0].address;
return (
<>
<div>
<h1>Batch Transactions Demo</h1>
<h3>
This demo demonstractes how to send a batch of transaction to a smart
wallet
</h3>
<div>
<p>{`Account: ${account}`}</p>
</div>
{account && (
<div>
<button>Send Bundled Txs</button>
</div>
)}
</div>
</>
);
};
export default BatchTransactions;
- Create the function to send the bundle of transactions. We will be sending a self-transfer of 1 wei, 2 times.
import React from "react";
import { useConnectWallet } from "@web3-onboard/react";
const BatchTransactions = () => {
const [{ wallet }] = useConnectWallet();
const [userOpHash, setuserOpHash] = useState();
const [error, setError] = useState("");
const account = wallet?.accounts[0].address;
const sendBundledCalls = async () => {
if (!wallet?.provider) return;
const value = toHex(1);
const calls = [
{
chainId: 5,
from: account,
calls: [
{
to: account,
value,
data: "0x",
gas: "0x76c0",
},
{
to: account,
value,
data: "0x",
gas: "0x76c0",
},
],
},
];
console.log(calls);
try {
const userOpHash = await wallet.provider.request({
method: "wallet_sendFunctionCallBundle",
params: calls,
});
setuserOpHash(userOpHash);
} catch (error) {
setError(error);
}
};
return (
<>
<div>
...
{account && (
<div>
<button onClick={sendBundledCalls}>Send Bundled Txs</button>
</div>
<p>{error ? error.message : null}</p>
)}
</div>
</>
);
};
export default BatchTransactions;
Import BatchTransaction
inside App.js
...
export default function App() {
return (
<>
<Web3OnboardProvider web3Onboard={web3Onboard}>
<ConnectWallet />
<BatchTransactions />
</Web3OnboardProvider>
</>
);
}
Test your app by connecting your wallet, and hitting the Send Bundled Tx button. Did it work? Congratulations! You have just send your first batch transactions 🎉
The next steps involves tracking the status of the Bundle so you can display to the user once their transaction is succesfully submitted onchain.
Step3: Track Batch Status
- Create a function that checks for the Bundle status using
wallet_getBundleStatus
import React from "react";
import { useConnectWallet } from "@web3-onboard/react";
const BatchTransactions = () => {
...
const [status, setStatus] = useState();
...
// function to ask the wallet to return the bundle status
const getBundleStatus = async (userOpHash) => {
if (!wallet?.provider) return;
try {
const status = await wallet.provider.request({
method: "wallet_getBundleStatus",
params: [userOpHash],
});
if (status) {
const response = JSON.parse(status).calls[0].status;
setStatus(response);
}
} catch (error) {
setError(error);
}
};
return (
<>
<div>
...
{account && (
<div>
<button onClick={sendBundledCalls}>Send Bundled Txs</button>
<button onClick={() => getBundleStatus(userOpHash)} disabled={!userOpHash}>
Check Bundle Status
</button>
{status !== undefined ? <p>Status: {status}</p> : null}
</div>
)}
</div>
</>
);
};
export default BatchTransactions;
- Now, if you lost track of the transaction, you can also delegate showing the status inside the wallet by using
wallet_showBundleStatus
import React from "react";
import { useState } from "react";
import { toHex, truncateAddress } from "./utils";
import { useConnectWallet } from "@web3-onboard/react";
const BatchTransactions = () => {
..
// function to delegate showing the bundle status to the wallet
const showBundleStatusInWallet = async (userOpHash) => {
if (!wallet?.provider) return;
try {
const status = await wallet.provider.request({
method: "wallet_showBundleStatus",
params: [userOpHash],
});
if (status) {
}
} catch (error) {
setError(error);
}
};
return (
<>
...
{account && (
<div>
...
{status !== undefined ? <p>Status: {status}</p> : null}
<button
onClick={() => showBundleStatusInWallet(userOpHash)}
disabled={!userOpHash}
>
Show Status in Wallet
</button>
</div>
)}
<p>{error ? error.message : null}</p>
</div>
</>
);
};
export default BatchTransactions;
Wrap up
That's it! You are now able to send batch transactions to any supported wallet
RPC methods Spec
These requests are sent to Wallets, not to nodes. These RPC requests are communicated directly to the wallet through a standard EIP-1193 custom requests.
wallet_sendFunctionCallBundle
Requests that the wallet deliver a group of function calls on-chain from the user’s wallet.
Invocation
{ "method": "wallet_sendFunctionCallBundle", "params": [{ chainId, from, calls }] }
Return
{ "result": { userOperationHash } }
- Example Request
- Example Response
{
"jsonrpc": "2.0",
"id": 0,
"method": "wallet_sendFunctionCallBundle",
"params": [
{
"chainId": 1,
"from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"calls": [
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0x76c0",
"value": "0x9184e72a",
"data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
},
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0xdefa",
"value": "0x182183",
"data": "0xfbadbaf01"
}
]
}
]
}
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe67asds.."
}
wallet_getBundleStatus
Returns the status of a bundle that was sent via wallet_sendFunctionCallBundle
. The identifier of the bundle is the value returned from the wallet_sendFunctionCallBundle
RPC.
Invocation
{ "method": "wallet_getBundleStatus", "params": [userOperationHash] }
Return
{ "result": { calls } }
- Example Request
- Example Response
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe67asds.."
}
{
"calls": [
{
"status": "CONFIRMED",
"receipt": {
"logs": [
{
"address": "0xa922b54716264130634d6ff183747a8ead91a40b",
"topics": [
"0x5a2a90727cc9d000dd060b1132a5c977c9702bb3a52afe360c9c22f0e9451a68"
],
"data": "0xabcd"
}
],
"success": true,
"blockHash": "0xf19bbafd9fd0124ec110b848e8de4ab4f62bf60c189524e54213285e7f540d4a",
"blockNumber": "0xabcd",
"blockTimestamp": "0xabcdef",
"gasUsed": "0xdef",
"transactionHash": "0x9b7bb827c2e5e3c1a0a44dc53e573aa0b3af3bd1f9f5ed03071b100bb039eaff"
}
},
{
"status": "PENDING"
}
]
}
wallet_showBundleStatus
Requests that the wallet present UI showing the status of the given bundle. This allows dapps to delegate the display of the function call status to the wallet, which can most accurately render the current status of the bundle. This RPC is intended to replace the typical user experience of a dapp linking to a block explorer for a given transaction hash.
Invocation
{ "method": "wallet_showBundleStatus", "params": [userOperationHash] }
Return
{ "result": call }
- Example Request
- Example Response
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe67asds.."
}
{
status: "CONFIRMED"
receipt: {
logs: [..]
success: true
blockHash: "0x84b9f48e922c6b500fa07a1e94144cb1452f2466199fc726e807d31bae6b4e52"
blockNumber: "0x7dbc3c"
gasUsed: "0x25c8e"
transactionHash: "0xcb8c43372800586bffc7521e0ca09f8828a87d4f0003f36ea68f9cbcf3e229f9"
}
}