Skip to main content
Use the Multicall3From contract to batch multiple USDC transfers into one Arc transaction. This tutorial shows the full flow with viem, ethers.js, Python, and curl. You will configure a client, encode two ERC-20 transfer(...) subcalls, submit them through Multicall3From.aggregate3(...), and verify the resulting Transfer events. To learn how Multicall3From preserves your wallet as the sender, see Batched transactions.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+ for the TypeScript examples, or Python 3.10+ for the Python example.
  • Created an Arc Testnet wallet.
  • Funded the wallet with testnet USDC from the Circle Faucet.
  • Chosen two recipient addresses on Arc Testnet.

Step 1. Set up the project

Create a new project and install the dependencies for the client library you want to use:
mkdir arc-batched-transfers
cd arc-batched-transfers
npm init -y
npm pkg set type=module
npm install dotenv ethers tsx typescript viem
Create an .env file:
Shell
touch .env
Add your configuration:
.env
PRIVATE_KEY=YOUR_PRIVATE_KEY
RECIPIENT_ONE_ADDRESS=RECIPIENT_ONE_ADDRESS
RECIPIENT_TWO_ADDRESS=RECIPIENT_TWO_ADDRESS
RPC_URL=https://rpc.testnet.arc.network
Replace YOUR_PRIVATE_KEY with the 0x-prefixed private key for the funded wallet. Replace each recipient value with an Arc Testnet address.

Step 2. Review the contract address and ABI

Arc Testnet uses the following predeployed contracts for this tutorial: For the full address list, see contract addresses. Create multicall3from-abi.json in your project:
multicall3from-abi.json
[
  {
    "type": "function",
    "name": "aggregate3",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "calls",
        "type": "tuple[]",
        "components": [
          { "name": "target", "type": "address" },
          { "name": "allowFailure", "type": "bool" },
          { "name": "callData", "type": "bytes" }
        ]
      }
    ],
    "outputs": [
      {
        "name": "returnData",
        "type": "tuple[]",
        "components": [
          { "name": "success", "type": "bool" },
          { "name": "returnData", "type": "bytes" }
        ]
      }
    ]
  }
]
The script you build in the next steps loads this ABI file from the project root.

Step 3. Configure the client connection

Create the script file for your client library. The first chunk reads the wallet and recipient configuration from .env, sets the contract addresses, loads the Multicall3From ABI, and creates the clients that read chain state and submit transactions.
Create viem-batch.ts:
TypeScript
import "dotenv/config";
import { readFileSync } from "node:fs";
import {
  type Address,
  createPublicClient,
  createWalletClient,
  defineChain,
  encodeFunctionData,
  erc20Abi,
  getAddress,
  http,
  parseEventLogs,
  parseUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";

const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.network";
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const recipients = [
  getAddress(process.env.RECIPIENT_ONE_ADDRESS as Address),
  getAddress(process.env.RECIPIENT_TWO_ADDRESS as Address),
];

const arcTestnet = defineChain({
  id: 5042002,
  name: "Arc Testnet",
  nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
  rpcUrls: { default: { http: [rpcUrl] } },
  blockExplorers: {
    default: { name: "ArcScan", url: "https://testnet.arcscan.app" },
  },
  testnet: true,
});

const multicall3FromAddress = "0x522fAf9A91c41c443c66765030741e4AaCe147D0";
const usdcAddress = "0x3600000000000000000000000000000000000000";
const multicall3FromAbi = JSON.parse(
  readFileSync("multicall3from-abi.json", "utf8"),
);

const account = privateKeyToAccount(privateKey);
const publicClient = createPublicClient({
  chain: arcTestnet,
  transport: http(rpcUrl),
});
const walletClient = createWalletClient({
  account,
  chain: arcTestnet,
  transport: http(rpcUrl),
});
The public clients read chain state, the wallet or account objects sign transactions, and the contract interfaces encode the Multicall3From and ERC-20 calls.

Step 4. Encode the transfer calls

Add a chunk that sets the USDC amount and builds one ERC-20 transfer(...) subcall for each recipient. Each subcall targets the USDC contract, sets allowFailure to false, and includes the encoded transfer calldata.
TypeScript
const amount = parseUnits("1", 6);

const calls = recipients.map((recipient) => ({
  target: usdcAddress,
  allowFailure: false,
  callData: encodeFunctionData({
    abi: erc20Abi,
    functionName: "transfer",
    args: [recipient, amount],
  }),
}));
1000000 is 1 USDC in base units because the USDC ERC-20 interface uses 6 decimals.

Step 5. Confirm Multicall3From is deployed

Before sending a transaction, check that the configured Multicall3From address has deployed bytecode.
TypeScript
const multicall3FromCode = await publicClient.getCode({
  address: multicall3FromAddress,
});
if (!multicall3FromCode || multicall3FromCode === "0x") {
  throw new Error(`Multicall3From is not deployed at ${multicall3FromAddress}`);
}
If the bytecode check returns empty code, confirm that your RPC_URL points to Arc Testnet and that the address matches the table in Step 2.

Step 6. Simulate and send the batch

Simulate the aggregate3(...) call before sending the transaction. The simulation confirms that each encoded subcall can succeed from your wallet.
TypeScript
const simulation = await publicClient.simulateContract({
  account,
  address: multicall3FromAddress,
  abi: multicall3FromAbi,
  functionName: "aggregate3",
  args: [calls],
});

const simulatedResults = simulation.result as {
  success: boolean;
  returnData: `0x${string}`;
}[];
if (!simulatedResults.every((result) => result.success)) {
  throw new Error(
    `Expected all simulated subcalls to succeed: ${JSON.stringify(
      simulatedResults,
    )}`,
  );
}

const hash = await walletClient.writeContract(simulation.request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
  throw new Error(`Batched transaction reverted: ${hash}`);
}
The receipt is reused in the next step to check the emitted USDC Transfer events.

Step 7. Verify the transfer events

Decode the USDC logs from the receipt and confirm that each expected transfer is present.
TypeScript
const transferLogs = parseEventLogs({
  abi: erc20Abi,
  eventName: "Transfer",
  logs: receipt.logs.filter(
    (log) => log.address.toLowerCase() === usdcAddress.toLowerCase(),
  ),
});

for (const recipient of recipients) {
  const transfer = transferLogs.find(
    (log) =>
      getAddress(log.args.from) === account.address &&
      getAddress(log.args.to) === recipient &&
      log.args.value === amount,
  );

  if (!transfer) {
    throw new Error(`Missing expected USDC Transfer log to ${recipient}`);
  }
}

console.log(
  "Transaction:",
  `${arcTestnet.blockExplorers.default.url}/tx/${hash}`,
);
console.log("Block:", receipt.blockNumber.toString());
console.log("Sender:", account.address);
console.log(
  "Transfers:",
  recipients.map((recipient) => ({
    to: recipient,
    amount: amount.toString(),
  })),
);
The from value in each expected Transfer event is your wallet address. That check confirms that the target USDC contract saw your wallet as the sender for each subcall.

Step 8. Run the script

Run the command for the script you created:
npx tsx --env-file=.env viem-batch.ts
Successful output includes the transaction URL, block number, sender, and both recipient transfers:
Transaction: https://testnet.arcscan.app/tx/0x...
Block: 123456
Sender: 0xYourWallet...
Transfers: [
  { to: "0xRecipientOne...", amount: "1000000" },
  { to: "0xRecipientTwo...", amount: "1000000" }
]

Step 9. Check JSON-RPC directly with curl

Use curl for read-only JSON-RPC checks, such as verifying deployed bytecode or reading a transaction receipt. Use a client library such as viem, ethers.js, or web3.py to sign and submit the transaction itself.
MULTICALL3FROM_ADDRESS=0x522fAf9A91c41c443c66765030741e4AaCe147D0
RPC_URL=https://rpc.testnet.arc.network

curl --request POST "$RPC_URL" \
  --header "content-type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "method": "eth_getCode",
    "params": ["'"$MULTICALL3FROM_ADDRESS"'", "latest"],
    "id": 1
  }'