Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 33 additions & 277 deletions docs/base-account/improve-ux/batch-transactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,8 @@ With Base Account, you can send multiple onchain calls in a single transaction.

You can submit batch transactions by using the `wallet_sendCalls` RPC method, defined in [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792).


<Tip>
**Do you prefer video content?**

There is a video guide that covers the implementation in detail in the [last section of this page](#video-guide).

</Tip>

## Installation

Install the Base Account SDK:

<CodeGroup>
```bash npm
npm install @base-org/account
Expand All @@ -34,322 +24,88 @@ yarn add @base-org/account
```bash bun
bun add @base-org/account
```

</CodeGroup>

## Setup

### Initialize the SDK

Import and create the Base Account SDK instance:

```tsx batchTransactions.tsx
```tsx
import { createBaseAccountSDK } from "@base-org/account";

const sdk = createBaseAccountSDK({
appName: "Base Account SDK Demo",
appLogoUrl: "https://base.org/logo.png",
appLogoUrl: "https://avatars.githubusercontent.com/u/108554348?s=200&v=4",
});

const provider = sdk.getProvider();
```

## Basic Batch Transaction

### Simple Multiple Transfers

Send multiple ETH transfers in a single transaction:

```tsx batchTransactions.tsx expandable
import { createBaseAccountSDK, getCryptoKeyAccount } from "@base-org/account";
```tsx
import { createBaseAccountSDK, getCryptoKeyAccount, base } from "@base-org/account";
import { numberToHex, parseEther } from "viem";

const sdk = createBaseAccountSDK({
appName: "Batch Transaction Demo",
appLogoUrl: "https://base.org/logo.png",
appLogoUrl: "https://avatars.githubusercontent.com/u/108554348?s=200&v=4",
});

const provider = sdk.getProvider();

async function sendBatchTransfers() {
try {
// Get crypto account
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;

// Prepare batch calls
const calls = [
{
to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
value: numberToHex(parseEther("0.001")), // 0.001 ETH
data: "0x", // Empty data for simple transfer
},
{
to: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E456",
value: numberToHex(parseEther("0.001")), // 0.001 ETH
data: "0x", // Empty data for simple transfer
},
];
const calls = [
{ to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", value: numberToHex(parseEther("0.001")), data: "0x" },
{ to: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E456", value: numberToHex(parseEther("0.001")), data: "0x" },
];

// Send batch transaction
const result = await provider.request({
method: "wallet_sendCalls",
params: [
{
version: "2.0.0",
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: true, // All calls must succeed or all fail
calls: calls,
},
],
});
const result = await provider.request({
method: "wallet_sendCalls",
params: [{ version: "2.0.0", from: fromAddress, chainId: numberToHex(base.constants.CHAIN_IDS.base), atomicRequired: true, calls }],
});

console.log("Batch transaction sent:", result);
return result;
} catch (error) {
console.error("Batch transaction failed:", error);
throw error;
}
return result;
}
```

## Contract Interactions
## ERC-20 Approve + NFT Mint

### ERC-20 Approve and Mint an NFT (ERC-721)

A common pattern is to approve the NFT contract to move your ERC-20 and then mint an NFT (ERC-721):

```tsx batchTransactions.tsx expandable
import {
createBaseAccountSDK,
getCryptoKeyAccount,
base,
} from "@base-org/account";
```tsx
import { createBaseAccountSDK, getCryptoKeyAccount, base } from "@base-org/account";
import { numberToHex, parseUnits, encodeFunctionData } from "viem";

// ERC-20 ABI for approve
const erc20Abi = [
{
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "approve",
outputs: [{ name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
] as const;
const erc20Abi = [{ inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], name: "approve", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function" }] as const;
const erc721Abi = [{ inputs: [{ name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], name: "mint", outputs: [], stateMutability: "nonpayable", type: "function" }] as const;

// ERC721 ABI for the mint function
const erc721Abi = [
{
inputs: [
{ name: "to", type: "address" },
{ name: "tokenId", type: "uint256" },
],
name: "mint",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
] as const;

// USDC contract address on Base Sepolia
const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const NFT_CONTRACT_ADDRESS = "0x82039e7C37D7aAac98D0F4d0A762F4E0d8c8DC273";

// NFT contract address on Base Sepolia
const NFT_CONTRACT_ADDRESS = "0x82039e7C37D7aAc98D0F4d0A762F4E0d8c8DC273";

async function approveAndTransfer() {
async function approveAndMint() {
const sdk = createBaseAccountSDK({
appName: "ERC-20 Batch Demo",
appLogoUrl: "https://base.org/logo.png",
appLogoUrl: "https://avatars.githubusercontent.com/u/108554348?s=200&v=4",
});

const provider = sdk.getProvider();
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;

// Encode the first approve call - approve USDC to NFT contract
const call1Data = encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
NFT_CONTRACT_ADDRESS,
parseUnits("1000", 6), // USDC has 6 decimals
],
});

// Encode the second call - mint NFT to the user's address
const call2Data = encodeFunctionData({
abi: erc721Abi,
functionName: "mint",
args: [fromAddress as `0x${string}`, BigInt("1")],
});

const result = await provider.request({
method: "wallet_sendCalls",
params: [
{
version: "2.0.0",
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia),
atomicRequired: true,
calls: [
{
to: USDC_ADDRESS,
data: call1Data,
},
{
to: NFT_CONTRACT_ADDRESS,
data: call2Data,
},
],
},
],
});

return result;
}
```

## Advanced Features

### Checking Wallet Capabilities

Before sending batch transactions, you can check if the wallet supports atomic batching:

```tsx batchTransactions.tsx expandable
async function checkCapabilities() {
const provider = sdk.getProvider();

try {
const cryptoAccount = await getCryptoKeyAccount();
const address = cryptoAccount?.account?.address;

const capabilities = await provider.request({
method: "wallet_getCapabilities",
params: [address],
});

const baseCapabilities = capabilities[base.constants.CHAIN_IDS.base];

if (baseCapabilities?.atomicBatch?.supported) {
console.log("Atomic batching is supported");
return true;
} else {
console.log("Atomic batching is not supported");
return false;
}
} catch (error) {
console.error("Failed to check capabilities:", error);
return false;
}
}
```

### Non-Atomic Batching

Sometimes you want calls to execute sequentially, even if some fail:

```tsx batchTransactions.tsx expandable
const result = await provider.request({
method: "wallet_sendCalls",
params: [
{
params: [{
version: "2.0.0",
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: false, // Allow partial execution
calls: calls,
},
],
});
```

## Getting the Batch Transaction Result

`wallet_getCallsStatus` returns the execution status for a batch you previously submitted with `wallet_sendCalls`. Capture the `callsId` returned by `wallet_sendCalls`, then poll for the batch status until it is confirmed or fails.

```tsx batchTransactions.tsx lines expandable
async function trackBatchTransaction(
calls: Array<{
to: `0x${string}`;
data: `0x${string}`;
value?: `0x${string}`;
}>
) {
const cryptoAccount = await getCryptoKeyAccount();
const fromAddress = cryptoAccount?.account?.address;

const callsId = await provider.request({
method: "wallet_sendCalls",
params: [
{
version: "2.0.0",
from: fromAddress,
chainId: numberToHex(base.constants.CHAIN_IDS.base),
atomicRequired: true,
calls,
},
],
chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia),
atomicRequired: true,
calls: [
{ to: USDC_ADDRESS, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [NFT_CONTRACT_ADDRESS, parseUnits("1000", 6)] }) },
{ to: NFT_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: erc721Abi, functionName: "mint", args: [fromAddress as `0x${string}`, BigInt("1")] }) },
],
}],
});

try {
const status = await provider.request({
method: "wallet_getCallsStatus",
params: [callsId],
});

if (status.status === 200) {
console.log("Batch completed successfully", status.receipts);
} else if (status.status === 100) {
console.log("Batch still pending", status.id);
} else {
console.error("Batch failed", status.status);
}

return status;
} catch (error: any) {
if (error.code === 4200) {
throw new Error("No batch found for the provided callsId.");
}

if (error.code === 4100) {
throw new Error(
"The connected wallet does not support wallet_getCallsStatus."
);
}

if (error.code === -32602) {
throw new Error("The callsId parameter is invalid.");
}

throw error;
}
return result;
}
```

You can learn more about `wallet_getCallsStatus` in the [reference documentation](/base-account/reference/core/provider-rpc-methods/wallet_getCallsStatus).

<Tip>
**Need more control over gas?**

You can override the gas limit for individual calls in a batch using the [`gasLimitOverride`](/base-account/reference/core/capabilities/gasLimitOverride) capability. This is useful for calls with nondeterministic gas consumption, such as swaps. See the [capabilities overview](/base-account/reference/core/capabilities/overview) for the full list of supported capabilities.

</Tip>

## Video Guide

<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/UGB0j_eHsNU?si=vfxwKY63Y9vhQpgS"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
Loading