Skip to main content
This quickstart is for developers who want to integrate xReserve into their apps. It helps you write a TypeScript script that deposits USDC into xReserve on Ethereum Sepolia. Your recipient wallet then receives an equivalent amount of USDC-backed stablecoin.
Need to transfer USDC from another chain first? Use Bridge Kit to move testnet funds to Ethereum Sepolia before making an xReserve deposit.
Select the tab below to view the steps for your blockchain.

Prerequisites

Before you begin, ensure you have:
  1. Installed Node.js v22+.
  2. Created an Ethereum Sepolia wallet and a Canton TestNet wallet and funded them with testnet tokens:
  3. Installed the required dependencies:
    • viem - A TypeScript library that interfaces with Ethereum

Step 1: Set up your project

This step shows you how to set 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 xreserve-canton-usdcx
cd xreserve-canton-usdcx
npm init -y

# Install tools and dependencies
npm install viem dotenv

1.2. Configure environment variables

Create a .env file in the project directory, add your wallet private key and the recipient address on Canton.
If your wallet provider is MetaMask, follow their tutorial for how to find and export your private key.
Shell
cat > .env << EOF
PRIVATE_KEY=<your_ethereum_wallet_private_key>
CANTON_RECIPIENT=<your_canton_recipient_testnet_address>
EOF
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.ts and add the following code to import the dependencies:
index.ts
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 following code snippet to your index.ts file. It specifies the RPC and wallet private key, contract addresses, and Canton-specific parameters that the rest of the script relies on. The CANTON_RECIPIENT is loaded from your .env file.
Inspect the onchain implementation of xReserve EVM contracts on GitHub.
index.ts
// ============ 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 as `0x${string}`,

  // Contract addresses on Ethereum Sepolia testnet
  X_RESERVE_CONTRACT:
    "0x008888878f94C0d87defdf0B07f46B93C1934442" as `0x${string}`,
  SEPOLIA_USDC_CONTRACT:
    "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as `0x${string}`,

  // Deposit parameters for Canton
  CANTON_DOMAIN: 10001, // Canton domain ID
  CANTON_RECIPIENT: process.env.CANTON_RECIPIENT || "", // Address to receive USDC-backed stablecoin on Canton
  DEPOSIT_AMOUNT: "5.00",
  MAX_FEE: "0",
};

2.3. Set up contract ABIs

Add the following code snippet to your index.ts file. It adds xReserve and ERC-20 ABI fragments which tell viem how to format and send the contract calls when the script runs.
For more information about ABIs, see QuickNode’s guide on What is an ABI?
index.ts
// ============ 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: [],
  },
] as const;

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" }],
  },
] as const;

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 TestNet can mint the corresponding USDC-backed stablecoin.

3.1. Add the main function

Add the following code snippet to your index.ts file. 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 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 USDC-backed stablecoin for the wallet specified as the CANTON_RECIPIENT
Canton uses a keccak256 hash of the recipient address for the remoteRecipient parameter. The original address is encoded as hex for hookData.
index.ts
// ============ Main function ============
async function main() {
  if (!config.PRIVATE_KEY) {
    throw new Error("PRIVATE_KEY must be set in your .env file");
  }

  if (!config.CANTON_RECIPIENT) {
    throw new Error("CANTON_RECIPIENT 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")) as `0x${string}`;

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

  // Check token balance
  const usdcBalance = await publicClient.readContract({
    address: config.SEPOLIA_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.SEPOLIA_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.SEPOLIA_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);
});
This step finalizes the transfer, depositing USDC into xReserve on Ethereum Sepolia to make USDCx available on Canton TestNet.

3.2. Run the script

Execute the script in your terminal:
Terminal
npx tsx index.ts

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.