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 will receive USDC-backed stablecoins on the remote blockchain when that network supports automatic settlement.
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.
This quickstart covers the deposit into xReserve on Ethereum Sepolia. After the deposit confirms, Aleo requires you to mint USDCx before it appears in the recipient wallet. For details, see the official Aleo documentation

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+.
  • Created an Ethereum Sepolia wallet and an Aleo Testnet wallet and funded them with testnet tokens.
  • Installed the required dependencies:
    • viem - A TypeScript library that interfaces with Ethereum.
    • @provablehq/sdk - Official Aleo SDK for address encoding.

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-aleo-usdcx
cd xreserve-aleo-usdcx
npm init -y

# Install tools and dependencies
npm install viem dotenv @provablehq/sdk

1.2. Configure environment variables

Create a .env file in the project directory and add your wallet private key and the recipient address on Aleo.
If you use MetaMask, follow their guide for how to find and export your private key.
Shell
cat > .env << EOF
PRIVATE_KEY=<your_ethereum_wallet_private_key>
ALEO_RECIPIENT=<your_aleo_recipient_testnet_address>
EOF
This use of a private key is simplified for demonstration purposes. In production, store and access your private key securely and never share it.

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,
  toHex,
  pad,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { Address } from "@provablehq/sdk";

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 Aleo-specific parameters that the rest of the script relies on. The ALEO_RECIPIENT is loaded from your .env file.
You can review the 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 Aleo
  ALEO_DOMAIN: 10002, // Aleo domain ID
  ALEO_RECIPIENT: process.env.ALEO_RECIPIENT || "", // Shield wallet address to receive USDCx on Aleo
  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 background on contract ABIs, see QuickNode’s guide, 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 deposit

This step shows you how to implement the main() function which performs the deposit of USDC from your Ethereum Sepolia wallet into the xReserve contract. The xReserve contract mints an equivalent amount of USDCx on Aleo Testnet and sends it to your recipient wallet.

3.1. Implement the main function

Add the following code snippet to your index.ts file. It implements the main() function which uses viem to read balances, approve a USDC spend, call the xReserve deposit function, and console.log the transaction results.Aleo uses bech32m-encoded addresses and little-endian byte ordering for field elements. The script uses the Aleo SDK’s Address.toBytesLe() method to properly convert the address to a 32-byte field element, then uses viem toHex() and pad() utilities to format it as a bytes32 hex string. The resulting field element is used for both remoteRecipient and 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.ALEO_RECIPIENT) {
    throw new Error("ALEO_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);

  // Convert Aleo address to bytes using the Aleo SDK
  const aleoAddress = Address.from_string(config.ALEO_RECIPIENT);
  const fieldElementBytes = aleoAddress.toBytesLe();

  if (fieldElementBytes.length !== 32) {
    throw new Error(
      `Invalid Aleo address: expected 32 bytes, got ${fieldElementBytes.length}`,
    );
  }

  // Use field element bytes (little-endian) for both remoteRecipient and hookData
  const remoteRecipientBytes32 = toHex(
    pad(fieldElementBytes, { size: 32 }),
  ) as `0x${string}`;
  const hookData = remoteRecipientBytes32;

  console.log(
    `\nDepositing ${config.DEPOSIT_AMOUNT} USDC to Aleo recipient: ${config.ALEO_RECIPIENT}`,
  );
  console.log(`Aleo field element (little-endian): ${remoteRecipientBytes32}`);

  // 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.ALEO_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 Aleo Testnet.

3.2. Run the script

Execute the script in your terminal:
Shell
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.After the deposit confirms, Aleo requires you to mint USDCx before it appears in the recipient wallet. For details, see the official Aleo documentation.