Need to transfer USDC from another chain first? Use Bridge Kit
to move testnet funds to Ethereum Sepolia before making an xReserve deposit.
- Canton
- Stacks
- Aleo
Prerequisites
Before you begin, ensure you have:- Installed Node.js v22+.
- Created an Ethereum Sepolia wallet and a Canton TestNet wallet and funded
them with testnet tokens:
- Use a wallet provider such as MetaMask to create your Ethereum Sepolia wallet
- Get Testnet USDC from the Circle Faucet
- Get Testnet ETH from a public Ethereum Sepolia faucet
- 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
Report incorrect code
Copy
# 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
Report incorrect code
Copy
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 calledindex.ts and add the following code to import the
dependencies:index.ts
Report incorrect code
Copy
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 yourindex.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
Report incorrect code
Copy
// ============ 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 yourindex.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
Report incorrect code
Copy
// ============ 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 yourindex.ts file. This code flows through
the following actions:- Verifies that
PRIVATE_KEYis 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
depositToRemoteto submit the deposit and tell Canton to mint USDC-backed stablecoin for the wallet specified as theCANTON_RECIPIENT
remoteRecipient
parameter. The original address is encoded as hex for hookData.index.ts
Report incorrect code
Copy
// ============ 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);
});
3.2. Run the script
Execute the script in your terminal:Terminal
Report incorrect code
Copy
npx tsx index.ts
3.3. Verify the deposit
After the script finishes, find theDeposit 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.Prerequisites
Before you begin, ensure you have:- Installed Node.js v22+.
- Created an Ethereum Sepolia wallet and a Stacks Testnet wallet and funded
them with testnet tokens:
- Use a wallet provider such as MetaMask to create your Ethereum Sepolia wallet
- Use Leather Wallet or Xverse for your Stacks wallet
- Get Testnet USDC from the Circle Faucet
- Get Testnet ETH from a public Ethereum Sepolia faucet
- Installed the required dependencies:
viem- A TypeScript library that interfaces with Ethereum@stacks/transactions- A JavaScript library for Stacks address creation and transaction handlingmicro-packed- A library for encoding Stacks addresses to bytes32 format
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
Report incorrect code
Copy
# Set up your directory and initialize the project
mkdir xreserve-stacks-usdcx
cd xreserve-stacks-usdcx
npm init -y
# Install tools and dependencies
npm install viem dotenv @stacks/transactions micro-packed @scure/base
1.2. Configure environment variables
Create a.env file in the project directory, add your wallet private key and
the recipient address on Stacks.If your wallet provider is MetaMask, follow their tutorial for how to find
and export your private
key.
Shell
Report incorrect code
Copy
cat > .env << EOF
PRIVATE_KEY=<your_ethereum_wallet_private_key>
STACKS_RECIPIENT=<your_stacks_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. Create helpers
Create ahelpers.ts file and add the following code to it. This code encodes
Stacks addresses into the bytes32 format required by xReserve:helpers.ts
Report incorrect code
Copy
import * as P from "micro-packed";
import {
createAddress,
addressToString,
AddressVersion,
StacksWireType,
} from "@stacks/transactions";
import { hex } from "@scure/base";
import { type Hex, pad, toHex } from "viem";
export const remoteRecipientCoder = P.wrap<string>({
encodeStream(w, value: string) {
const address = createAddress(value);
// Left pad with 11 zero bytes
P.bytes(11).encodeStream(w, new Uint8Array(11).fill(0));
// 1 version byte
P.U8.encodeStream(w, address.version);
// 20 hash bytes
P.bytes(20).encodeStream(w, hex.decode(address.hash160));
},
decodeStream(r) {
// Left pad (11 bytes)
P.bytes(11).decodeStream(r);
// 1 version byte
const version = P.U8.decodeStream(r);
// 20 hash bytes
const hash = P.bytes(20).decodeStream(r);
return addressToString({
hash160: hex.encode(hash),
version: version as AddressVersion,
type: StacksWireType.Address,
});
},
});
export function bytes32FromBytes(bytes: Uint8Array): Hex {
return toHex(pad(bytes, { size: 32 }));
}
2.2. Import dependencies
Create a file calledindex.ts and add the following code to import the
dependencies:index.ts
Report incorrect code
Copy
import "dotenv/config";
import { createWalletClient, createPublicClient, http, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { bytes32FromBytes, remoteRecipientCoder } from "./helpers";
2.3. Define configuration constants
Add the following code snippet to yourindex.ts file. It specifies the RPC and
wallet private key, contract addresses, and Stacks-specific parameters that the
rest of the script relies on. The STACKS_RECIPIENT is loaded from your .env
file.Inspect the onchain implementation of xReserve EVM contracts on
GitHub.
index.ts
Report incorrect code
Copy
// ============ 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 Stacks
STACKS_DOMAIN: 10003, // Stacks domain ID
STACKS_RECIPIENT: process.env.STACKS_RECIPIENT || "", // Address to receive USDC-backed stablecoin on Stacks
DEPOSIT_AMOUNT: "5.00",
MAX_FEE: "0",
};
2.4. Set up contract ABIs
Add the following code snippet to yourindex.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
Report incorrect code
Copy
// ============ 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 Stacks can mint the corresponding USDC-backed stablecoin.3.1. Add the main function
Add the following code snippet to yourindex.ts file. This code flows through
the following actions:- Verifies that
PRIVATE_KEYis 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
depositToRemoteto submit the deposit and tell Stacks to mint USDC-backed stablecoin for the wallet specified as theSTACKS_RECIPIENT
remoteRecipient parameter. The original address is encoded as
hex for hookData.index.ts
Report incorrect code
Copy
// ============ Main function ============
async function main() {
if (!config.PRIVATE_KEY) {
throw new Error("PRIVATE_KEY must be set in your .env file");
}
if (!config.STACKS_RECIPIENT) {
throw new Error("STACKS_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.STACKS_RECIPIENT);
const remoteRecipient = bytes32FromBytes(
remoteRecipientCoder.encode(config.STACKS_RECIPIENT),
);
const hookData = ("0x" +
Buffer.from(recipientBytes).toString("hex")) as `0x${string}`;
console.log(
`\nDepositing ${config.DEPOSIT_AMOUNT} USDC to Stacks recipient: ${config.STACKS_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.STACKS_DOMAIN,
remoteRecipient,
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);
});
3.2. Run the script
Execute the script in your terminal:Shell
Report incorrect code
Copy
npx tsx index.ts
3.3. Verify the deposit
After the script finishes, find theDeposit tx hash in the terminal output and
paste it into Sepolia Etherscan to confirm your
deposit transaction was successful. On Stacks Testnet, the recipient wallet will
receive the minted testnet USDCx.Prerequisites
Before you begin, ensure you have:- Installed Node.js v22+.
- Created an Ethereum Sepolia wallet and an Aleo Testnet wallet and funded them
with testnet tokens:
- Use a wallet provider such as MetaMask to create your Ethereum Sepolia wallet
- Use Shield Wallet for your Aleo wallet
- Get Testnet USDC from the Circle Faucet
- Get Testnet ETH from a public Ethereum Sepolia faucet
- 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
Report incorrect code
Copy
# 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, add your wallet private key and
the recipient address on Aleo.If your wallet provider is MetaMask, follow their tutorial for how to find
and export your private
key.
Shell
Report incorrect code
Copy
cat > .env << EOF
PRIVATE_KEY=<your_ethereum_wallet_private_key>
ALEO_RECIPIENT=<your_aleo_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 calledindex.ts and add the following code to import the
dependencies:index.ts
Report incorrect code
Copy
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 yourindex.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.Inspect the onchain implementation of xReserve EVM contracts on
GitHub.
index.ts
Report incorrect code
Copy
// ============ 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 USDC-backed stablecoin on Aleo
DEPOSIT_AMOUNT: "5.00",
MAX_FEE: "0",
};
2.3. Set up contract ABIs
Add the following code snippet to yourindex.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
Report incorrect code
Copy
// ============ 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 themain() 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 yourindex.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
Report incorrect code
Copy
// ============ 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);
});
3.2. Run the script
Execute the script in your terminal:Shell
Report incorrect code
Copy
npx tsx index.ts
3.3. Verify the deposit
After the script finishes, find theDeposit tx hash in the terminal output and
paste it into Sepolia Etherscan to confirm your
deposit transaction was successful.Aleo requires developers to mint USDCx on their side after the deposit is
confirmed. For more information, refer to the Aleo
documentation.