Skip to main content
This quickstart is for developers who want to integrate xReserve into their apps. It helps you write a JavaScript script that deposits USDC into xReserve on Ethereum Sepolia to receive an equivalent amount of USDCx on Canton TestNet.
Tip: Need to transfer USDC from another chain first? Use Bridge Kit to move testnet funds to Ethereum Sepolia before making an xReserve deposit.

Prerequisites

Before you begin, ensure that you’ve:
  1. Installed Node.js v18+ and npm.
  2. Created an Ethereum Sepolia wallet and funded it with testnet USDC and native tokens:

Step 1. Set up your project

This step shows you how to step up a fresh Node.js workspace, install dependencies, configure your environment variables, and prepare your Ethereum Sepolia wallet.

1.1. Set up your development environment

Create a new directory and install dependencies:
Shell
# Set up your directory and initialize the project
mkdir deposit-usdc-into-xreserve-for-usdcx-on-canton-quickstart
cd deposit-usdc-into-xreserve-for-usdcx-on-canton-quickstart
npm init -y

# Install tools and dependencies
npm install viem dotenv

1.2 Configure environment variables

Create a .env file in the project directory and add your wallet private key, replacing {YOUR_PRIVATE_KEY} with the private key for your Ethereum Sepolia Wallet.
Tip: If your wallet provider is MetaMask, follow their tutorial for how to find and export your private key.
Shell
echo "PRIVATE_KEY={YOUR_PRIVATE_KEY}" > .env
Warning: This is strictly for testing purposes. Never share your private key.

Step 2. Set up your script

This step shows you how to build the script by importing its dependencies and defining the configuration constants and ABI fragments that the script uses.

2.1. Import dependencies

Create a file called index.mjs and add the following code to it. This code imports the dependencies that the script uses.
index.mjs
import "dotenv/config";
import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  keccak256,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

2.2. Define configuration constants

Add the highlighted code snippet to index.mjs. It specifies the RPC and wallet private key, contract addresses, and Canton-specific parameters that the rest of the script relies on. Replace your_canton_wallet_address with the wallet that should receive minted USDCx on Canton TestNet.
Tip: You can inspect the onchain implementation of xReserve EVM contracts on GitHub.
index.mjs
import "dotenv/config";
import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  keccak256,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// ============ Configuration constants ============
const config = {
  // Public Ethereum Sepolia RPC and your private wallet key
  ETH_RPC_URL: process.env.RPC_URL || "https://ethereum-sepolia.publicnode.com",
  PRIVATE_KEY: process.env.PRIVATE_KEY,

  // Contract addresses on testnet
  X_RESERVE_CONTRACT: "0x008888878f94C0d87defdf0B07f46B93C1934442",
  ETH_USDC_CONTRACT: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
  CANTON_USDC:
    "0x74ed63088c070c8fd5d8ad71f2a1cef868c63d00e0ac6dc2a6722d171691a422",

  // Deposit parameters for Canton
  CANTON_DOMAIN: 10001, // Canton domain ID
  CANTON_RECIPIENT: "your_canton_wallet_address", // Address to receive minted USDCx on Canton
  DEPOSIT_AMOUNT: "5.00",
  MAX_FEE: "0",
};

2.3. Set up contract ABIs

Add the highlighted code snippet to index.mjs. It adds xReserve and ERC-20 ABI fragments which tell Viem how to format and send the contract calls when the script runs.
Tip: For more information about ABIs, see QuickNode’s guide on What is an ABI?
index.mjs
import "dotenv/config";
import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  keccak256,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// ============ Configuration constants ============
const config = {
  // Public Ethereum Sepolia RPC and your private wallet key
  ETH_RPC_URL: process.env.RPC_URL || "https://ethereum-sepolia.publicnode.com",
  PRIVATE_KEY: process.env.PRIVATE_KEY,

  // Contract addresses on testnet
  X_RESERVE_CONTRACT: "0x008888878f94C0d87defdf0B07f46B93C1934442",
  ETH_USDC_CONTRACT: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
  CANTON_USDC:
    "0x74ed63088c070c8fd5d8ad71f2a1cef868c63d00e0ac6dc2a6722d171691a422",

  // Deposit parameters for Canton
  CANTON_DOMAIN: 10001, // Canton domain ID
  CANTON_RECIPIENT: "your_canton_wallet_address", // Address to receive minted USDCx on Canton
  DEPOSIT_AMOUNT: "5.00",
  MAX_FEE: "0",
};

// ============ Contract ABIs ============
const X_RESERVE_ABI = [
  {
    name: "depositToRemote",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "value", type: "uint256" },
      { name: "remoteDomain", type: "uint32" },
      { name: "remoteRecipient", type: "bytes32" },
      { name: "localToken", type: "address" },
      { name: "maxFee", type: "uint256" },
      { name: "hookData", type: "bytes" },
    ],
    outputs: [],
  },
];

const ERC20_ABI = [
  {
    name: "approve",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "success", type: "bool" }],
  },
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
];

Step 3. Execute the xReserve deposit

This step shows you how to implement the main logic that checks balances, approves USDC, and calls xReserve on Ethereum Sepolia so Canton can mint the corresponding USDCx.

3.1. Add the main function

Add the highlighted code snippet to index.mjs. This code flows through the following actions:
  • Verifies that PRIVATE_KEY is present before continuing.
  • Creates an Ethereum Sepolia wallet client and logs the originating address.
  • Checks native ETH balance to ensure there is enough gas for transactions.
  • Computes the deposit value, maximum fee, and Canton recipient payload (USDC uses 6 decimals).
  • Confirms that the wallet’s USDC balance covers the configured deposit amount.
  • Approves the xReserve smart contract to spend USDC on the wallet’s behalf.
  • Calls depositToRemote to submit the deposit and tell Canton to mint USDCx for the wallet specified as the CANTON_RECIPIENT.
This step finalizes the transfer, depositing USDC into xReserve on Ethereum Sepolia to make USDCx available on Canton TestNet.
index.mjs
import "dotenv/config";
import {
  createWalletClient,
  createPublicClient,
  http,
  parseUnits,
  keccak256,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// ============ Configuration constants ============
const config = {
  // Public Ethereum Sepolia RPC and your private wallet key
  ETH_RPC_URL: process.env.RPC_URL || "https://ethereum-sepolia.publicnode.com",
  PRIVATE_KEY: process.env.PRIVATE_KEY,

  // Contract addresses on testnet
  X_RESERVE_CONTRACT: "0x008888878f94C0d87defdf0B07f46B93C1934442",
  ETH_USDC_CONTRACT: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
  CANTON_USDC:
    "0x74ed63088c070c8fd5d8ad71f2a1cef868c63d00e0ac6dc2a6722d171691a422",

  // Deposit parameters for Canton
  CANTON_DOMAIN: 10001, // Canton domain ID
  CANTON_RECIPIENT: "your_canton_wallet_address", // Address to receive minted USDCx on Canton
  DEPOSIT_AMOUNT: "5.00",
  MAX_FEE: "0",
};

// ============ Contract ABIs ============
const X_RESERVE_ABI = [
  {
    name: "depositToRemote",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "value", type: "uint256" },
      { name: "remoteDomain", type: "uint32" },
      { name: "remoteRecipient", type: "bytes32" },
      { name: "localToken", type: "address" },
      { name: "maxFee", type: "uint256" },
      { name: "hookData", type: "bytes" },
    ],
    outputs: [],
  },
];

const ERC20_ABI = [
  {
    name: "approve",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "success", type: "bool" }],
  },
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
];

// ============ Main function ============
async function main() {
  if (!config.PRIVATE_KEY) {
    throw new Error("PRIVATE_KEY must be set in your .env file");
  }

  // Set up wallet and wallet provider
  const account = privateKeyToAccount(config.PRIVATE_KEY);
  const client = createWalletClient({
    account,
    chain: sepolia,
    transport: http(config.ETH_RPC_URL),
  });

  const publicClient = createPublicClient({
    chain: sepolia,
    transport: http(config.ETH_RPC_URL),
  });

  console.log(`Ethereum wallet address: ${account.address}`);

  // Check native ETH balance
  const nativeBalance = await publicClient.getBalance({
    address: account.address,
  });
  console.log(
    `Native balance: ${nativeBalance.toString()} wei (${(
      Number(nativeBalance) / 1e18
    ).toFixed(6)} ETH)`,
  );
  if (nativeBalance === 0n)
    throw new Error("Insufficient native balance for gas fees");

  // Prepare deposit params (USDC has 6 decimals)
  const value = parseUnits(config.DEPOSIT_AMOUNT, 6);
  const maxFee = parseUnits(config.MAX_FEE, 6);
  const encoder = new TextEncoder();
  const recipientBytes = encoder.encode(config.CANTON_RECIPIENT);
  const remoteRecipientBytes32 = keccak256(recipientBytes);
  const hookData = "0x" + Buffer.from(recipientBytes).toString("hex");

  console.log(
    `\nDepositing ${config.DEPOSIT_AMOUNT} USDC to Canton recipient: ${config.CANTON_RECIPIENT}`,
  );

  // Check token balance
  const usdcBalance = await publicClient.readContract({
    address: config.ETH_USDC_CONTRACT,
    abi: ERC20_ABI,
    functionName: "balanceOf",
    args: [account.address],
  });
  console.log(
    `USDC balance: ${usdcBalance.toString()} (${(
      Number(usdcBalance) / 1e6
    ).toFixed(6)} USDC)`,
  );
  if (usdcBalance < value) {
    throw new Error(
      `Insufficient USDC balance. Required: ${(Number(value) / 1e6).toFixed(
        6,
      )} USDC`,
    );
  }

  // Approve xReserve to spend USDC
  const approveTxHash = await client.writeContract({
    address: config.ETH_USDC_CONTRACT,
    abi: ERC20_ABI,
    functionName: "approve",
    args: [config.X_RESERVE_CONTRACT, value],
  });
  console.log("Approval tx hash:", approveTxHash);
  console.log("Waiting for approval confirmation...");

  await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
  console.log("✅ Approval confirmed");

  // Deposit transaction
  const depositTxHash = await client.writeContract({
    address: config.X_RESERVE_CONTRACT,
    abi: X_RESERVE_ABI,
    functionName: "depositToRemote",
    args: [
      value,
      config.CANTON_DOMAIN,
      remoteRecipientBytes32,
      config.ETH_USDC_CONTRACT,
      maxFee,
      hookData,
    ],
  });

  console.log("Deposit tx hash:", depositTxHash);
  console.log(
    "✅ Transaction submitted. You can track this on Sepolia Etherscan.",
  );
}

// ============ Call the main function ============
main().catch((error) => {
  console.error("❌ Error:", error);
  process.exit(1);
});

3.2. Run the script

Save the index.mjs file and execute the script in your terminal:
Shell
node index.mjs

3.3. Verify the deposit

After the script finishes, find the Deposit tx hash in the terminal output and paste it into Sepolia Etherscan to confirm your deposit transaction was successful. On Canton TestNet, the recipient wallet will receive the minted testnet USDCx.