import { randomBytes } from "node:crypto";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
const USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_CHAIN_ID = 84532;
const POLYGON_AMOY_DOMAIN = 7;
const ECO_BASE_URL = "https://deposit-addresses-preproduction.eco.com/api/v1";
const GATEWAY_API_URL = "https://gateway-api-testnet.circle.com/v1/balances";
const DEPOSIT_AMOUNT = 1000000n; // 1 USDC
async function requestDepositAddress(depositor: `0x${string}`) {
const response = await fetch(
`${ECO_BASE_URL}/depositAddresses/gateway/polygon`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: BASE_SEPOLIA_CHAIN_ID,
depositor,
evmDestinationAddress: depositor,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to get deposit address: ${await response.text()}`);
}
const { data } = await response.json();
return data.evmDepositAddress as `0x${string}`;
}
async function signTransferAuthorization(
walletClient: ReturnType<typeof createWalletClient>,
account: ReturnType<typeof privateKeyToAccount>,
from: `0x${string}`,
to: `0x${string}`,
) {
const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600);
const signature = await walletClient.signTypedData({
account,
domain: {
name: "USDC",
version: "2",
chainId: BASE_SEPOLIA_CHAIN_ID,
verifyingContract: USDC_BASE_SEPOLIA as `0x${string}`,
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: {
from,
to,
value: DEPOSIT_AMOUNT,
validAfter,
validBefore,
nonce,
},
});
return {
nonce,
validAfter,
validBefore,
signature,
};
}
async function submitGaslessDeposit(params: {
from: `0x${string}`;
to: `0x${string}`;
nonce: `0x${string}`;
validAfter: bigint;
validBefore: bigint;
signature: `0x${string}`;
}) {
const response = await fetch(
`${ECO_BASE_URL}/gasless/transferWithAuthorization`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: BASE_SEPOLIA_CHAIN_ID,
from: params.from,
to: params.to,
value: DEPOSIT_AMOUNT.toString(),
validAfter: params.validAfter.toString(),
validBefore: params.validBefore.toString(),
nonce: params.nonce,
signature: params.signature,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to submit transfer: ${await response.text()}`);
}
const { data } = await response.json();
return data.id as string;
}
async function waitForCompletion(jobId: string) {
let status = "PENDING";
let attempts = 0;
const maxAttempts = 30;
while (status === "PENDING" && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`${ECO_BASE_URL}/gasless/jobs/${jobId}`);
if (!response.ok) {
throw new Error(`Failed to poll status: ${await response.text()}`);
}
const { data } = await response.json();
status = data.status as string;
attempts++;
console.log(`Status: ${status} (${attempts * 2}s elapsed)`);
}
if (status !== "COMPLETED") {
throw new Error("Deposit failed or timed out");
}
}
async function checkGatewayBalance(depositor: `0x${string}`) {
const response = await fetch(GATEWAY_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "USDC",
sources: [{ domain: POLYGON_AMOY_DOMAIN, depositor }],
}),
});
if (!response.ok) {
throw new Error(`Failed to fetch balance: ${await response.text()}`);
}
const { balances } = await response.json();
const polygonBalance = balances.find(
(balance: { domain: number; balance: string }) =>
balance.domain === POLYGON_AMOY_DOMAIN,
);
if (!polygonBalance) {
throw new Error("No Polygon PoS Amoy Gateway balance returned");
}
return polygonBalance.balance as string;
}
async function main() {
const rawPrivateKey = process.env.PRIVATE_KEY?.trim();
if (!rawPrivateKey) {
throw new Error("Missing PRIVATE_KEY in .env");
}
const privateKey = (
rawPrivateKey.startsWith("0x") ? rawPrivateKey : `0x${rawPrivateKey}`
) as `0x${string}`;
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
});
console.log(`\nWallet: ${account.address}`);
console.log(`Amount: ${Number(DEPOSIT_AMOUNT) / 1e6} USDC\n`);
console.log("Step 1: Requesting deposit address...");
const depositAddress = await requestDepositAddress(account.address);
console.log(`Deposit address: ${depositAddress}\n`);
console.log("Step 2: Signing authorization...");
const authorization = await signTransferAuthorization(
walletClient,
account,
account.address,
depositAddress,
);
console.log("Signature generated\n");
console.log("Step 3: Submitting to Eco...");
const jobId = await submitGaslessDeposit({
from: account.address,
to: depositAddress,
...authorization,
});
console.log(`Job ID: ${jobId}\n`);
console.log("Step 4: Waiting for completion...");
await waitForCompletion(jobId);
console.log("\nDeposit successful\n");
console.log("Step 5: Checking Gateway balance...");
const gatewayBalance = await checkGatewayBalance(account.address);
console.log(`Gateway balance: ${gatewayBalance} USDC\n`);
}
main().catch((error) => {
if (error instanceof Error) {
console.error("Error:", error.message);
return;
}
console.error("Error:", error);
});