Skip to main content

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.

Detect USDC deposits on Arc by generating addresses, monitoring the unified Transfer event for both native USDC sends and ERC-20 transfers, crediting after a single block confirmation (deterministic finality guarantees no reorgs), and sweeping funds into a hot wallet.

Prerequisites

Before you begin:
  • Access to an Arc RPC endpoint (https://rpc.testnet.arc.network) or WebSocket (wss://rpc.testnet.arc.network)
  • An HD wallet library for generating deposit addresses (for example, ethers or viem)
  • Familiarity with Ethereum JSON-RPC methods and event log filtering
  • A database to track processed deposits and prevent double-crediting

Steps

Step 1. Generate deposit addresses

Arc uses standard Ethereum addresses (0x-prefixed, 20 bytes, EIP-55 checksum). Derive deposit addresses using the same HD wallet approach as Ethereum—one unique address per user.
import { HDNodeWallet, Mnemonic } from "ethers";

// Derive a deposit address for a given user index
function getDepositAddress(mnemonic: string, userIndex: number): string {
  const hdNode = HDNodeWallet.fromMnemonic(
    Mnemonic.fromPhrase(mnemonic),
    `m/44'/60'/0'/0/${userIndex}`,
  );
  return hdNode.address;
}
Store the mapping between user IDs and their derived address index. Never expose the mnemonic or private keys in client-side code.

Step 2. Subscribe to new blocks

Use eth_subscribe("newHeads") over WebSocket for real-time block notifications, or poll eth_blockNumber over HTTP as a fallback.
import { WebSocketProvider, JsonRpcProvider } from "ethers";

// Option A: WebSocket subscription (recommended)
const wsProvider = new WebSocketProvider("wss://rpc.testnet.arc.network");

wsProvider.on("block", async (blockNumber: number) => {
  console.log(`New block: ${blockNumber}`);
  await processBlock(blockNumber);
});

// Option B: HTTP polling fallback
const httpProvider = new JsonRpcProvider("https://rpc.testnet.arc.network");

let lastProcessedBlock = await httpProvider.getBlockNumber();

setInterval(async () => {
  const currentBlock = await httpProvider.getBlockNumber();
  for (let block = lastProcessedBlock + 1; block <= currentBlock; block++) {
    await processBlock(block);
  }
  lastProcessedBlock = currentBlock;
}, 2000);

Step 3. Detect incoming transfers using the unified Transfer event

Arc emits a single Transfer event for both native USDC sends and ERC-20 transfers on the USDC contract. Monitor this one event to detect all deposits without double-counting. Event signature:
Transfer(address indexed from, address indexed to, uint256 value)
Topic0: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef USDC contract: 0x3600000000000000000000000000000000000000 Use eth_getLogs to filter for transfers to your deposit addresses:
import { Contract, Interface, Log, JsonRpcProvider } from "ethers";

const USDC_ADDRESS = "0x3600000000000000000000000000000000000000";
const TRANSFER_TOPIC =
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

const provider = new JsonRpcProvider("https://rpc.testnet.arc.network");
const erc20Interface = new Interface([
  "event Transfer(address indexed from, address indexed to, uint256 value)",
]);

async function processBlock(blockNumber: number): Promise<void> {
  const logs = await provider.getLogs({
    address: USDC_ADDRESS,
    topics: [
      TRANSFER_TOPIC,
      null, // any sender
      null, // any recipient—filter client-side for your addresses
    ],
    fromBlock: blockNumber,
    toBlock: blockNumber,
  });

  for (const log of logs) {
    const parsed = erc20Interface.parseLog({
      topics: log.topics as string[],
      data: log.data,
    });

    if (!parsed) continue;

    const to = parsed.args.to as string;
    const value = parsed.args.value as bigint;

    // Check if the recipient is one of your deposit addresses
    if (isDepositAddress(to)) {
      await creditDeposit({
        txHash: log.transactionHash,
        logIndex: log.index,
        to,
        amount: value, // 6 decimals
        blockNumber,
      });
    }
  }
}
Do not count eth_getBalance results AND Transfer events separately. They represent the same balance. Use Transfer events exclusively for deposit detection.

Step 4. Confirm the deposit

Arc provides deterministic finality—once a transaction is included in a block, it is final with no possibility of reorg. You can safely credit deposits after 1 confirmation.
import { JsonRpcProvider } from "ethers";

const provider = new JsonRpcProvider("https://rpc.testnet.arc.network");

async function isConfirmed(txHash: string): Promise<boolean> {
  const receipt = await provider.getTransactionReceipt(txHash);

  if (!receipt || receipt.status === 0) {
    return false; // Transaction failed or not yet mined
  }

  // On Arc, 1 confirmation = final. No reorgs possible.
  const currentBlock = await provider.getBlockNumber();
  return currentBlock >= receipt.blockNumber;
}
Since Arc has deterministic finality, you do not need to wait for multiple confirmations. Credit the user once the block containing their transaction is produced.

Step 5. Handle decimal conversion

The unified Transfer event emits values with 6 decimals (the ERC-20 interface). Use these values directly for crediting.
// Transfer event value uses 6 decimals—use directly
function formatDeposit(eventValue: bigint): string {
  // eventValue is already in 6-decimal USDC
  const whole = eventValue / 1_000_000n;
  const fractional = (eventValue % 1_000_000n).toString().padStart(6, "0");
  return `${whole}.${fractional}`;
}

// CAUTION: eth_getBalance returns 18-decimal native USDC
// If you ever query native balance, convert to 6 decimals:
function nativeBalanceTo6Decimals(nativeBalance: bigint): bigint {
  return nativeBalance / 10n ** 12n;
}
eth_getBalance returns native USDC with 18 decimals. The Transfer event uses 6 decimals. Never mix these representations—always convert native balances by dividing by 10^12 before displaying to users.

Step 6. Sweep deposits to a hot wallet

Consolidate deposited funds from individual user addresses into your hot wallet. Use EIP-1559 transactions with Arc’s minimum base fee of 20 Gwei.
import { Wallet, JsonRpcProvider, parseUnits } from "ethers";

const provider = new JsonRpcProvider("https://rpc.testnet.arc.network");

async function sweepDeposit(
  depositPrivateKey: string,
  hotWalletAddress: string,
  amount: bigint,
): Promise<string> {
  const wallet = new Wallet(depositPrivateKey, provider);

  const feeData = await provider.getFeeData();
  const maxFeePerGas = feeData.maxFeePerGas ?? parseUnits("30", "gwei");
  const maxPriorityFeePerGas =
    feeData.maxPriorityFeePerGas ?? parseUnits("1", "gwei");

  // Estimate gas for an ERC-20 transfer (approximately 21,000 for native sends)
  const gasLimit = 65_000n;

  const tx = await wallet.sendTransaction({
    to: hotWalletAddress,
    value: amount * 10n ** 12n, // Convert 6-decimal amount to 18-decimal native value
    type: 2, // EIP-1559
    maxFeePerGas,
    maxPriorityFeePerGas,
    gasLimit,
  });

  const receipt = await tx.wait();
  return receipt!.hash;
}
When sweeping via a native USDC send, convert from 6-decimal amounts to 18-decimal value fields. Alternatively, call the ERC-20 transfer function on the USDC contract, which accepts 6-decimal amounts directly.

Common mistakes

Avoid these pitfalls when implementing deposits:
  • Double-counting: Monitoring both eth_getBalance and Transfer events for the same address counts deposits twice. Use Transfer events only.
  • Decimal mismatch: Treating Transfer event values (6 decimals) as 18-decimal amounts inflates displayed balances by 10^12.
  • Waiting for multiple confirmations: Arc has deterministic finality. Waiting for 6+ confirmations adds unnecessary latency with no security benefit.
  • Missing the unified event: Native USDC sends and ERC-20 transfers both emit the same Transfer event on the USDC contract. You only need one log filter.
  • Hardcoded keys: Never embed private keys in source code. Use environment variables or a secrets manager for sweep wallet credentials.