> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arc.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send batch USDC transfers

> Send multiple USDC transfers in one Arc transaction with Multicall3From and sender-preserving subcalls.

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](/arc/concepts/batched-transactions).

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/) for the TypeScript examples, or
  Python 3.10+ for the Python example.
* Created an [Arc Testnet wallet](/arc/references/connect-to-arc).
* Funded the wallet with testnet USDC from the
  [Circle Faucet](https://faucet.circle.com/).
* 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:

<CodeGroup>
  ```bash Node.js theme={null}
  mkdir arc-batched-transfers
  cd arc-batched-transfers
  npm init -y
  npm pkg set type=module
  npm install dotenv ethers tsx typescript viem
  ```

  ```bash Python theme={null}
  mkdir arc-batched-transfers
  cd arc-batched-transfers
  python3 -m venv .venv
  source .venv/bin/activate
  pip install web3 python-dotenv
  ```
</CodeGroup>

Create an `.env` file:

```bash Shell theme={null}
touch .env
```

Add your configuration:

```text .env theme={null}
PRIVATE_KEY=YOUR_PRIVATE_KEY
RECIPIENT_ONE_ADDRESS=RECIPIENT_ONE_ADDRESS
RECIPIENT_TWO_ADDRESS=RECIPIENT_TWO_ADDRESS
RPC_URL=https://rpc.testnet.arc.io
```

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:

| Contract         | Address                                                                                                                        |
| :--------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `Multicall3From` | [`0x522fAf9A91c41c443c66765030741e4AaCe147D0`](https://testnet.arcscan.app/address/0x522fAf9A91c41c443c66765030741e4AaCe147D0) |
| `USDC`           | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) |

For the full address list, see
[contract addresses](/arc/references/contract-addresses#transaction-extensions).

Create `multicall3from-abi.json` in your project:

```json multicall3from-abi.json theme={null}
[
  {
    "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.

<Tabs>
  <Tab title="Viem">
    Create `viem-batch.ts`:

    ```typescript TypeScript theme={null}
    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.io";
    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),
    });
    ```
  </Tab>

  <Tab title="ethers.js">
    Create `ethers-batch.ts`:

    ```typescript TypeScript theme={null}
    import "dotenv/config";
    import { readFileSync } from "node:fs";
    import { ethers } from "ethers";

    const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.io";
    const privateKey = process.env.PRIVATE_KEY as string;
    const recipients = [
      ethers.getAddress(process.env.RECIPIENT_ONE_ADDRESS as string),
      ethers.getAddress(process.env.RECIPIENT_TWO_ADDRESS as string),
    ];

    const multicall3FromAddress = "0x522fAf9A91c41c443c66765030741e4AaCe147D0";
    const usdcAddress = "0x3600000000000000000000000000000000000000";
    const explorerUrl = "https://testnet.arcscan.app";

    const multicall3FromAbi = JSON.parse(
      readFileSync("multicall3from-abi.json", "utf8"),
    );
    const erc20Abi = [
      "function transfer(address to, uint256 amount) returns (bool)",
      "event Transfer(address indexed from, address indexed to, uint256 value)",
    ];

    const provider = new ethers.JsonRpcProvider(rpcUrl, {
      chainId: 5042002,
      name: "arc-testnet",
    });
    const wallet = new ethers.Wallet(privateKey, provider);
    const multicall3FromInterface = new ethers.Interface(multicall3FromAbi);
    const erc20Interface = new ethers.Interface(erc20Abi);
    ```
  </Tab>

  <Tab title="Python">
    Create `python-batch.py`:

    ```python Python theme={null}
    import json
    import os

    from dotenv import load_dotenv
    from web3 import Web3
    from web3.logs import DISCARD

    load_dotenv()

    rpc_url = os.getenv("RPC_URL", "https://rpc.testnet.arc.io")
    private_key = os.environ["PRIVATE_KEY"]
    recipients = [
        Web3.to_checksum_address(os.environ["RECIPIENT_ONE_ADDRESS"]),
        Web3.to_checksum_address(os.environ["RECIPIENT_TWO_ADDRESS"]),
    ]

    multicall3_from_address = Web3.to_checksum_address(
        "0x522fAf9A91c41c443c66765030741e4AaCe147D0"
    )
    usdc_address = Web3.to_checksum_address("0x3600000000000000000000000000000000000000")
    explorer_url = "https://testnet.arcscan.app"

    with open("multicall3from-abi.json", encoding="utf-8") as abi_file:
        multicall3_from_abi = json.load(abi_file)
    erc20_abi = [
        {
            "type": "function",
            "name": "transfer",
            "stateMutability": "nonpayable",
            "inputs": [
                {"name": "to", "type": "address"},
                {"name": "amount", "type": "uint256"},
            ],
            "outputs": [{"name": "", "type": "bool"}],
        },
        {
            "type": "event",
            "name": "Transfer",
            "anonymous": False,
            "inputs": [
                {"name": "from", "type": "address", "indexed": True},
                {"name": "to", "type": "address", "indexed": True},
                {"name": "value", "type": "uint256", "indexed": False},
            ],
        },
    ]

    w3 = Web3(Web3.HTTPProvider(rpc_url))
    account = w3.eth.account.from_key(private_key)
    multicall3_from = w3.eth.contract(
        address=multicall3_from_address,
        abi=multicall3_from_abi,
    )
    usdc = w3.eth.contract(address=usdc_address, abi=erc20_abi)
    ```
  </Tab>
</Tabs>

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.

<Tabs>
  <Tab title="Viem">
    ```typescript TypeScript theme={null}
    const amount = parseUnits("1", 6);

    const calls = recipients.map((recipient) => ({
      target: usdcAddress,
      allowFailure: false,
      callData: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: [recipient, amount],
      }),
    }));
    ```
  </Tab>

  <Tab title="ethers.js">
    ```typescript TypeScript theme={null}
    const amount = ethers.parseUnits("1", 6);

    const calls = recipients.map((recipient) => ({
      target: usdcAddress,
      allowFailure: false,
      callData: erc20Interface.encodeFunctionData("transfer", [recipient, amount]),
    }));
    const aggregateData = multicall3FromInterface.encodeFunctionData("aggregate3", [
      calls,
    ]);
    ```
  </Tab>

  <Tab title="Python">
    ```python Python theme={null}
    amount = 1_000_000

    calls = []
    for recipient in recipients:
        transfer_data = usdc.functions.transfer(
            recipient,
            amount,
        )._encode_transaction_data()
        calls.append(
            (
                usdc_address,
                False,
                bytes.fromhex(transfer_data[2:]),
            )
        )
    ```
  </Tab>
</Tabs>

`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.

<Tabs>
  <Tab title="Viem">
    ```typescript TypeScript theme={null}
    const multicall3FromCode = await publicClient.getCode({
      address: multicall3FromAddress,
    });
    if (!multicall3FromCode || multicall3FromCode === "0x") {
      throw new Error(`Multicall3From is not deployed at ${multicall3FromAddress}`);
    }
    ```
  </Tab>

  <Tab title="ethers.js">
    ```typescript TypeScript theme={null}
    const multicall3FromCode = await provider.getCode(multicall3FromAddress);
    if (multicall3FromCode === "0x") {
      throw new Error(`Multicall3From is not deployed at ${multicall3FromAddress}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python Python theme={null}
    multicall3_from_code = w3.eth.get_code(multicall3_from_address)
    if multicall3_from_code == b"":
        raise RuntimeError(
            f"Multicall3From is not deployed at {multicall3_from_address}"
        )
    ```
  </Tab>
</Tabs>

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.

<Tabs>
  <Tab title="Viem">
    ```typescript TypeScript theme={null}
    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}`);
    }
    ```
  </Tab>

  <Tab title="ethers.js">
    ```typescript TypeScript theme={null}
    const [simulatedResults] = multicall3FromInterface.decodeFunctionResult(
      "aggregate3",
      await provider.call({
        from: wallet.address,
        to: multicall3FromAddress,
        data: aggregateData,
      }),
    ) as unknown as [{ success: boolean; returnData: string }[]];
    if (!simulatedResults.every((result) => result.success)) {
      throw new Error(
        `Expected all simulated subcalls to succeed: ${JSON.stringify(
          simulatedResults,
        )}`,
      );
    }

    const tx = await wallet.sendTransaction({
      to: multicall3FromAddress,
      data: aggregateData,
    });
    const receipt = await tx.wait();
    if (!receipt) {
      throw new Error("Transaction was not mined");
    }
    if (receipt.status !== 1) {
      throw new Error(`Batched transaction reverted: ${tx.hash}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python Python theme={null}
    simulated_results = multicall3_from.functions.aggregate3(calls).call(
        {"from": account.address}
    )
    if not all(result[0] for result in simulated_results):
        raise RuntimeError(f"Expected all simulated subcalls to succeed: {simulated_results}")

    latest_block = w3.eth.get_block("latest")
    priority_fee = w3.to_wei(1, "gwei")
    base_fee = latest_block["baseFeePerGas"]

    transaction = multicall3_from.functions.aggregate3(calls).build_transaction(
        {
            "from": account.address,
            "nonce": w3.eth.get_transaction_count(account.address),
            "chainId": w3.eth.chain_id,
            "maxPriorityFeePerGas": priority_fee,
            "maxFeePerGas": (base_fee * 2) + priority_fee,
        }
    )

    signed = account.sign_transaction(transaction)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    if receipt["status"] != 1:
        raise RuntimeError(f"Batched transaction reverted: {Web3.to_hex(tx_hash)}")
    ```
  </Tab>
</Tabs>

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.

<Tabs>
  <Tab title="Viem">
    ```typescript TypeScript theme={null}
    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(),
      })),
    );
    ```
  </Tab>

  <Tab title="ethers.js">
    ```typescript TypeScript theme={null}
    const transferEvents: ethers.LogDescription[] = [];
    for (const log of receipt.logs) {
      if (log.address.toLowerCase() !== usdcAddress.toLowerCase()) continue;

      const parsed = erc20Interface.parseLog(log);
      if (parsed?.name === "Transfer") transferEvents.push(parsed);
    }

    for (const recipient of recipients) {
      const transfer = transferEvents.find(
        (event) =>
          event.args.from.toLowerCase() === wallet.address.toLowerCase() &&
          event.args.to.toLowerCase() === recipient.toLowerCase() &&
          event.args.value === amount,
      );

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

    console.log("Transaction:", `${explorerUrl}/tx/${tx.hash}`);
    console.log("Block:", receipt.blockNumber);
    console.log("Sender:", wallet.address);
    console.log(
      "Transfers:",
      recipients.map((recipient) => ({
        to: recipient,
        amount: amount.toString(),
      })),
    );
    ```
  </Tab>

  <Tab title="Python">
    ```python Python theme={null}
    transfer_events = usdc.events.Transfer().process_receipt(receipt, errors=DISCARD)
    for recipient in recipients:
        transfer = next(
            (
                event
                for event in transfer_events
                if Web3.to_checksum_address(event["args"]["from"]) == account.address
                and Web3.to_checksum_address(event["args"]["to"]) == recipient
                and event["args"]["value"] == amount
            ),
            None,
        )
        if transfer is None:
            raise RuntimeError(f"Missing expected USDC Transfer log to {recipient}")

    print("Transaction:", f"{explorer_url}/tx/{Web3.to_hex(tx_hash)}")
    print("Block:", receipt["blockNumber"])
    print("Sender:", account.address)
    print(
        "Transfers:",
        [{"to": recipient, "amount": str(amount)} for recipient in recipients],
    )
    ```
  </Tab>
</Tabs>

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:

<CodeGroup>
  ```bash viem theme={null}
  npx tsx --env-file=.env viem-batch.ts
  ```

  ```bash ethers.js theme={null}
  npx tsx --env-file=.env ethers-batch.ts
  ```

  ```bash Python theme={null}
  python python-batch.py
  ```
</CodeGroup>

Successful output includes the transaction URL, block number, sender, and both
recipient transfers:

```text theme={null}
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.

<CodeGroup>
  ```bash eth_getCode theme={null}
  MULTICALL3FROM_ADDRESS=0x522fAf9A91c41c443c66765030741e4AaCe147D0
  RPC_URL=https://rpc.testnet.arc.io

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

  ```bash eth_getTransactionReceipt theme={null}
  TRANSACTION_HASH=0xYOUR_TRANSACTION_HASH
  RPC_URL=https://rpc.testnet.arc.io

  curl --request POST "$RPC_URL" \
    --header "content-type: application/json" \
    --data '{
      "jsonrpc": "2.0",
      "method": "eth_getTransactionReceipt",
      "params": ["'"$TRANSACTION_HASH"'"],
      "id": 1
    }'
  ```
</CodeGroup>
