import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import {
createPublicClient,
createWalletClient,
formatUnits,
http,
isAddress,
isHex,
parseUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import {
arcTestnet,
avalancheFuji,
baseSepolia,
sepolia,
worldchainSepolia,
} from "viem/chains";
const EURC_DECIMALS = 6;
const EURC_ABI = [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
// Store each supported chain in one place so the transfer flow stays shared.
const CHAIN_CONFIGS = [
{
id: "arc-testnet",
name: "Arc Testnet",
chain: arcTestnet,
tokenAddress: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a",
},
{
id: "avalanche-fuji",
name: "Avalanche Fuji",
chain: avalancheFuji,
tokenAddress: "0x5E44db7996c682E92a960b65AC713a54AD815c6B",
},
{
id: "base-sepolia",
name: "Base Sepolia",
chain: baseSepolia,
tokenAddress: "0x808456652fdb597867f38412077A9182bf77359F",
},
{
id: "ethereum-sepolia",
name: "Ethereum Sepolia",
chain: sepolia,
tokenAddress: "0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4",
},
{
id: "world-chain-sepolia",
name: "World Chain Sepolia",
chain: worldchainSepolia,
tokenAddress: "0xe479EcA5740Ac65d6E1823bea2f1C08Bc14e954F",
},
] as const;
const { PRIVATE_KEY, RECIPIENT_ADDRESS } = process.env;
if (!PRIVATE_KEY || !isHex(PRIVATE_KEY) || PRIVATE_KEY.length !== 66) {
throw new Error(
"PRIVATE_KEY must be a 0x-prefixed 32-byte hex string (66 chars)",
);
}
if (!RECIPIENT_ADDRESS || !isAddress(RECIPIENT_ADDRESS)) {
throw new Error("RECIPIENT_ADDRESS must be a valid EVM address");
}
const privateKey: `0x${string}` = PRIVATE_KEY;
const recipientAddress: `0x${string}` = RECIPIENT_ADDRESS;
async function selectChain() {
// Prompt for a chain so one script can support every listed testnet.
const chainList = CHAIN_CONFIGS.map(
(chain, index) => `${index + 1}. ${chain.name}`,
).join("\n");
const readline = createInterface({ input, output });
try {
const answer = await readline.question(
"Select a chain for your EURC transfer:\n" +
chainList +
"\n\nEnter a number: ",
);
const selectedIndex = Number.parseInt(answer, 10) - 1;
const selectedChain = CHAIN_CONFIGS[selectedIndex];
if (!selectedChain) {
throw new Error("Invalid chain selection");
}
return selectedChain;
} finally {
readline.close();
}
}
async function main() {
try {
const selectedChain = await selectChain();
const account = privateKeyToAccount(privateKey);
// Create clients for the selected chain.
const publicClient = createPublicClient({
chain: selectedChain.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: selectedChain.chain,
transport: http(),
});
// Read the sender's token balance before attempting the transfer.
const balance = await publicClient.readContract({
address: selectedChain.tokenAddress,
abi: EURC_ABI,
functionName: "balanceOf",
args: [account.address],
});
const balanceFormatted = Number(
formatUnits(balance as bigint, EURC_DECIMALS),
);
const amount = 1; // Send 1 EURC in this example.
console.log("Chain:", selectedChain.name);
console.log("Sender:", account.address);
console.log("Recipient:", recipientAddress);
console.log("Balance:", balanceFormatted, "EURC");
if (amount > balanceFormatted) {
throw new Error("Insufficient balance");
}
const amountInDecimals = parseUnits(amount.toString(), EURC_DECIMALS);
// Submit the transfer transaction.
const hash = await walletClient.writeContract({
address: selectedChain.tokenAddress,
abi: EURC_ABI,
functionName: "transfer",
args: [recipientAddress, amountInDecimals],
});
console.log("Transaction submitted.");
console.log("Tx hash:", hash);
console.log(
"Explorer:",
`${selectedChain.chain.blockExplorers?.default.url}/tx/${hash}`,
);
// Wait for the transaction to be confirmed onchain.
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Transaction reverted");
}
console.log("Transfer confirmed!");
} catch (err) {
console.error("Transfer failed:", err instanceof Error ? err.message : err);
process.exit(1);
}
}
main();