← Back
β˜†
/** Swap native USDC β†’ USDC.e on the deposit wallet (Uniswap V3, fee=100 pool), gasless via
 *  the Polymarket relayer batch. Needed because the pUSD Onramp only accepts USDC.e, but
 *  the user topped up with native USDC. After this, run wrap-asset.mjs (WRAP_ASSET=USDC.e).
 *  Run on the main/DE server (relayer not geoblocked; builder-sign at localhost:3240).
 *
 *  Usage: [DRY=1] [SLIPPAGE_BPS=50] node --experimental-global-webcrypto swap-usdc.mjs */
import fs from 'node:fs';
import pkg from '@polymarket/builder-relayer-client';
import signingPkg from '@polymarket/builder-signing-sdk';
import { PrivyClient } from '@privy-io/server-auth';
import { createWalletClient, createPublicClient, http, encodeFunctionData, erc20Abi, maxUint256 } from 'viem';
import { toAccount } from 'viem/accounts';
import { polygon } from 'viem/chains';

const { RelayClient, RelayerTransactionState } = pkg;
const { BuilderConfig } = signingPkg;
const DW = process.env.DEPOSIT_WALLET || '0x50A8061e9448EB1e5d5e7aF07BE4E4F63C6F24Ff';
const DRY = process.env.DRY === '1';
const NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
const USDCE = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
const ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02 (Polygon)
const FEE = 100;
const SLIPPAGE_BPS = BigInt(process.env.SLIPPAGE_BPS || '50'); // 0.5%
const ROUTER_ABI = [{ name: 'exactInputSingle', type: 'function', stateMutability: 'payable', inputs: [{ type: 'tuple', components: [
  { name: 'tokenIn', type: 'address' }, { name: 'tokenOut', type: 'address' }, { name: 'fee', type: 'uint24' },
  { name: 'recipient', type: 'address' }, { name: 'amountIn', type: 'uint256' }, { name: 'amountOutMinimum', type: 'uint256' },
  { name: 'sqrtPriceLimitX96', type: 'uint160' } ] }], outputs: [{ name: 'amountOut', type: 'uint256' }] }];

const pub = createPublicClient({ chain: polygon, transport: http('https://polygon.drpc.org') });
const fix = (v) => typeof v === 'bigint' ? v.toString() : Array.isArray(v) ? v.map(fix) : (v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, fix(x)])) : v);

let relay = null;
async function getRelay() {
  if (relay) return relay;
  const env = {};
  const envUrl = fs.existsSync(new URL('../.env.privy', import.meta.url)) ? new URL('../.env.privy', import.meta.url) : new URL('./.env.privy', import.meta.url);
  for (const l of fs.readFileSync(envUrl, 'utf8').split('\n')) { const t = l.trim(); if (!t || t.startsWith('#')) continue; const i = t.indexOf('='); if (i > 0) env[t.slice(0, i).trim()] = t.slice(i + 1).trim(); }
  const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET, { walletApi: { authorizationPrivateKey: env.PRIVY_AUTHORIZATION_KEY } });
  const users = await privy.getUsers();
  // Multi-tenant EOA selection: require USER_EOA when >1 Privy user exists (the old
  // "last wins" loop could sign with the wrong user's key); auto-pick only when one exists.
  const wallets = [];
  for (const u of users) for (const a of u.linkedAccounts || []) if (a.type === 'wallet' && a.walletClientType === 'privy') wallets.push(a.address);
  const want = (process.env.USER_EOA || '').toLowerCase();
  const EOA = want ? (wallets.find((w) => w.toLowerCase() === want) || null)
    : (wallets.length === 1 ? wallets[0] : null);
  if (!EOA) throw new Error(want ? `USER_EOA ${process.env.USER_EOA} not among Privy embedded wallets` : `set USER_EOA β€” ${wallets.length} Privy embedded wallets exist, ambiguous`);
  const account = toAccount({
    address: EOA,
    async signMessage({ message }) { const m = typeof message === 'string' ? message : (message.raw ?? message); return (await privy.walletApi.ethereum.signMessage({ address: EOA, chainType: 'ethereum', message: m })).signature; },
    async signTypedData(td) { return (await privy.walletApi.ethereum.signTypedData({ address: EOA, chainType: 'ethereum', typedData: { ...td, message: fix(td.message) } })).signature; },
    async signTransaction() { throw new Error('n/a'); },
  });
  const walletClient = createWalletClient({ account, chain: polygon, transport: http('https://polygon.drpc.org') });
  const builderConfig = new BuilderConfig({ remoteBuilderConfig: { url: 'http://localhost:3240/api/polymarket/sign' } });
  relay = new RelayClient('https://relayer-v2.polymarket.com/', 137, walletClient, builderConfig);
  return relay;
}
async function submit(calls, tag) {
  const r = await getRelay();
  const deadline = (Math.floor(Date.now() / 1000) + 3600).toString();
  const resp = await r.executeDepositWalletBatch(calls, DW, deadline);
  console.log(`[${tag}] relayer tx:`, resp.transactionID, '| state:', resp.state);
  const result = await r.pollUntilState(resp.transactionID, [RelayerTransactionState.STATE_CONFIRMED, RelayerTransactionState.STATE_FAILED], '90', 3000);
  const ok = result?.state === RelayerTransactionState.STATE_CONFIRMED;
  console.log(ok ? `🎯 [${tag}] CONFIRMED` : `⚠️ [${tag}] not confirmed: ${JSON.stringify(result).slice(0, 200)}`);
  return ok;
}

const bal = await pub.readContract({ address: NATIVE, abi: erc20Abi, functionName: 'balanceOf', args: [DW] });
const e0 = Number(await pub.readContract({ address: USDCE, abi: erc20Abi, functionName: 'balanceOf', args: [DW] })) / 1e6;
console.log(`native USDC on DW: $${(Number(bal) / 1e6).toFixed(6)} | USDC.e before: $${e0.toFixed(4)}`);
if (bal <= 10000n) { console.log('nothing to swap (<$0.01)'); process.exit(0); }
const amountOutMin = bal - (bal * SLIPPAGE_BPS) / 10000n;
const calls = [];
const allowance = await pub.readContract({ address: NATIVE, abi: erc20Abi, functionName: 'allowance', args: [DW, ROUTER] });
if (allowance < bal) calls.push({ target: NATIVE, value: '0', data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [ROUTER, maxUint256] }) });
calls.push({ target: ROUTER, value: '0', data: encodeFunctionData({ abi: ROUTER_ABI, functionName: 'exactInputSingle', args: [{ tokenIn: NATIVE, tokenOut: USDCE, fee: FEE, recipient: DW, amountIn: bal, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0n }] }) });
if (DRY) { console.log(`DRY — would swap $${(Number(bal) / 1e6).toFixed(4)} native→USDC.e, minOut $${(Number(amountOutMin) / 1e6).toFixed(4)} (${calls.length} call(s))`); process.exit(0); }
const ok = await submit(calls, 'swap');
if (ok) {
  const e1 = Number(await pub.readContract({ address: USDCE, abi: erc20Abi, functionName: 'balanceOf', args: [DW] })) / 1e6;
  console.log(`USDC.e after: $${e1.toFixed(4)} (+$${(e1 - e0).toFixed(4)})`);
}