import 'dotenv/config'; import axios from "axios"; import { HttpsProxyAgent } from "https-proxy-agent"; import { BuilderConfig } from "@polymarket/builder-signing-sdk"; import { deriveProxyWallet, RelayerTxType, RelayClient, } from "@polymarket/builder-relayer-client"; import { createPublicClient, createWalletClient, encodeFunctionData, erc20Abi, formatUnits, http, maxUint256, parseUnits, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { polygon } from "viem/chains"; const CHAIN_ID = 137; const PUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"; const PUSD_DECIMALS = 6; const PROXY_FACTORY_ADDRESS = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"; const DEFAULT_CLOB_SPENDERS = [ "0xE111180000d2663C0091e4f400237545B87B996B", "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296", "0xe2222d279d744050d28e00520010520000310F59", ]; const NODE_HTTP_PROXY = process.env.NODE_HTTP_PROXY; const proxyAgent = NODE_HTTP_PROXY ? new HttpsProxyAgent(NODE_HTTP_PROXY) : undefined; if (NODE_HTTP_PROXY) { axios.defaults.proxy = false; axios.defaults.httpAgent = proxyAgent; axios.defaults.httpsAgent = proxyAgent; } const getRequiredEnv = (key) => { const value = process.env[key]; if (!value) { throw new Error(`${key} is required`); } return value; } const getOptionalEnv = (key) => { const value = process.env[key]; return value && value.trim() ? value.trim() : undefined; } const normalizePrivateKey = (privateKey) => { return privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`; } const getArgValue = (name) => { const prefix = `${name}=`; const arg = process.argv.slice(2).find(item => item.startsWith(prefix)); return arg ? arg.slice(prefix.length) : undefined; } const normalizeDirection = () => { const from = (getArgValue("--from") || process.env.TRANSFER_FROM || "proxy").toLowerCase(); const to = (getArgValue("--to") || process.env.TRANSFER_TO || "deposit").toLowerCase(); if (!["proxy", "deposit"].includes(from) || !["proxy", "deposit"].includes(to) || from === to) { throw new Error("Transfer direction must be proxy -> deposit or deposit -> proxy"); } return { from, to }; } const normalizeAction = () => { const action = (getArgValue("--action") || process.env.TRANSFER_ACTION || "transfer").toLowerCase(); if (!["transfer", "approve"].includes(action)) { throw new Error("action must be transfer or approve"); } return action; } const normalizeWalletMode = () => { const wallet = (getArgValue("--wallet") || process.env.APPROVE_WALLET || "deposit").toLowerCase(); if (!["proxy", "deposit", "both"].includes(wallet)) { throw new Error("wallet must be proxy, deposit, or both"); } return wallet; } const getClobSpenders = () => { const configured = getArgValue("--spenders") || process.env.CLOB_SPENDERS; if (!configured) { return DEFAULT_CLOB_SPENDERS; } const spenders = configured.split(",").map(item => item.trim()).filter(Boolean); if (!spenders.length) { throw new Error("CLOB spenders cannot be empty"); } return spenders; } const action = normalizeAction(); const amount = getArgValue("--amount") || process.env.TRANSFER_AMOUNT; if (action === "transfer" && (!amount || Number(amount) <= 0)) { throw new Error("Transfer amount is required. Example: npm run transfer -- --amount=10"); } const direction = action === "transfer" ? normalizeDirection() : undefined; const walletMode = action === "approve" ? normalizeWalletMode() : undefined; const clobSpenders = getClobSpenders(); const execute = process.argv.includes("--execute"); const relayerUrl = getOptionalEnv("POLYMARKET_RELAYER_URL") || "https://relayer-v2.polymarket.com"; const rpcUrl = getOptionalEnv("POLYGON_RPC_URL"); const account = privateKeyToAccount(normalizePrivateKey(getRequiredEnv("POLYMARKET_PRIVATE_KEY"))); const walletTransport = rpcUrl ? http(rpcUrl) : http(); const signer = createWalletClient({ account, chain: polygon, transport: walletTransport }); const publicClient = createPublicClient({ chain: polygon, transport: walletTransport }); const builderConfig = new BuilderConfig({ localBuilderCreds: { key: getRequiredEnv("POLYMARKET_BUILDER_API_KEY"), secret: getRequiredEnv("POLYMARKET_BUILDER_SECRET"), passphrase: getRequiredEnv("POLYMARKET_BUILDER_PASS_PHRASE"), }, }); const createRelayer = (relayTxType = RelayerTxType.PROXY) => { const relayer = new RelayClient(relayerUrl, CHAIN_ID, signer, builderConfig, relayTxType); if (proxyAgent) { relayer.httpClient.instance.defaults.proxy = false; relayer.httpClient.instance.defaults.httpAgent = proxyAgent; relayer.httpClient.instance.defaults.httpsAgent = proxyAgent; } return relayer; } const proxyRelayer = createRelayer(RelayerTxType.PROXY); const proxyWalletAddress = getOptionalEnv("POLYMARKET_PROXY_WALLET_ADDRESS") || deriveProxyWallet(account.address, PROXY_FACTORY_ADDRESS); const depositWalletAddress = getOptionalEnv("POLYMARKET_DEPOSIT_WALLET_ADDRESS") || await proxyRelayer.deriveDepositWalletAddress(); const [proxyBalance, depositBalance] = await Promise.all([ publicClient.readContract({ address: PUSD_ADDRESS, abi: erc20Abi, functionName: "balanceOf", args: [proxyWalletAddress], }), publicClient.readContract({ address: PUSD_ADDRESS, abi: erc20Abi, functionName: "balanceOf", args: [depositWalletAddress], }), ]); const baseResult = { owner: account.address, proxyWalletAddress, depositWalletAddress, pUSD: PUSD_ADDRESS, proxyBalance: formatUnits(proxyBalance, PUSD_DECIMALS), depositBalance: formatUnits(depositBalance, PUSD_DECIMALS), execute, }; const createApproveData = (spender) => { return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, maxUint256], }); } const runApprove = async () => { const wallets = []; if (walletMode === "proxy" || walletMode === "both") { wallets.push({ type: "proxy", address: proxyWalletAddress, calls: clobSpenders.map(spender => ({ to: PUSD_ADDRESS, data: createApproveData(spender), value: "0", })), }); } if (walletMode === "deposit" || walletMode === "both") { wallets.push({ type: "deposit", address: depositWalletAddress, calls: clobSpenders.map(spender => ({ target: PUSD_ADDRESS, data: createApproveData(spender), value: "0", })), }); } console.log(JSON.stringify({ ...baseResult, action, wallet: walletMode, spenders: clobSpenders, approvals: wallets.map(item => ({ type: item.type, address: item.address, spenderCount: item.calls.length, })), }, null, 2)); if (!execute) { console.log(`Dry run only. Add --execute to submit CLOB approvals for ${walletMode} wallet.`); return; } const results = []; for (const wallet of wallets) { const response = wallet.type === "proxy" ? await proxyRelayer.execute(wallet.calls, "approve pUSD for CLOB") : await createRelayer().executeDepositWalletBatch( wallet.calls, depositWalletAddress, String(Math.floor(Date.now() / 1000) + 600), ); const confirmed = await response.wait(); results.push({ type: wallet.type, address: wallet.address, transactionID: response.transactionID, transactionHash: response.transactionHash || response.hash, confirmed, }); } console.log(JSON.stringify({ results }, null, 2)); } const runTransfer = async () => { const transferAmount = parseUnits(amount, PUSD_DECIMALS); const result = { ...baseResult, action, from: direction.from, to: direction.to, sourceAddress: direction.from === "proxy" ? proxyWalletAddress : depositWalletAddress, destinationAddress: direction.to === "deposit" ? depositWalletAddress : proxyWalletAddress, amount, }; console.log(JSON.stringify(result, null, 2)); const sourceBalance = direction.from === "proxy" ? proxyBalance : depositBalance; if (sourceBalance < transferAmount) { throw new Error(`Insufficient ${direction.from} wallet pUSD balance: ${formatUnits(sourceBalance, PUSD_DECIMALS)}`); } if (!execute) { console.log(`Dry run only. Add --execute to submit the ${direction.from} -> ${direction.to} transfer.`); return; } const data = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [result.destinationAddress, transferAmount], }); const deadline = String(Math.floor(Date.now() / 1000) + 600); const depositRelayer = createRelayer(); const response = direction.from === "proxy" ? await proxyRelayer.execute( [{ to: PUSD_ADDRESS, data, value: "0", }], `transfer ${amount} pUSD from proxy to deposit wallet`, ) : await depositRelayer.executeDepositWalletBatch( [{ target: PUSD_ADDRESS, data, value: "0", }], depositWalletAddress, deadline, ); const confirmed = await response.wait(); console.log(JSON.stringify({ transactionID: response.transactionID, transactionHash: response.transactionHash || response.hash, confirmed, }, null, 2)); } if (action === "approve") { await runApprove(); } else { await runTransfer(); }