Skip to main content

Batch Transactions

Batched transactions allow you to perform multiple transactions in one single on-chain transaction.

Live Example

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
Did you know?

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 contract 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 contract wallet to test with.
    1. Candide Wallet
  • Node and a package manager (yarn or npm)
  • A account with some Göerli ETH
Try it Live

Try a simple live demo using Candide Wallet and WalletConnect.

Source code can be run under a minute on Github

Step 1: Get setup

  1. Install create-react-app:
npx create-react-app batch-transaction-dapp-demo
cd batch-transaction-dapp-demo
npm start
  1. Install dependencies. We will be using web3-onboard for this demo
npm install @web3-onboard/react @web3-onboard/walletconnect
  1. Create the Connect Wallet button component in a new file ConnectWallet.js
/src/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 contract wallet</p>
</div>
<p>⚠️ This demo is on Goerli ⚠️</p>
</div>
);
}
  1. Wrap your App with Web3OnboardProvider in App.js and add your ConnectWallet Button
/src/App.js
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

  1. Create a new component called BatchTransactions.js. This is where we will include all our logic.
/src/BatchTransactions.js
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;
  1. Create the function to send the bundle of transactions. We will be sending a self-transfer of 1 wei, 2 times.
/src/BatchTransactions.js
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

/src/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

  1. Create a function that checks for the Bundle status using wallet_getBundleStatus
/src/BatchTransactions.js
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;
  1. Now, if you lost track of the transaction, you can also delegate showing the status inside the wallet by using wallet_showBundleStatus
/src/BatchTransactions.js
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

Did you know?

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 } }
{
"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"
}
]
}
]
}

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 } }
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe67asds.."
}

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 }
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe67asds.."
}