← Back
import { createPublicClient, http, encodeFunctionData, erc20Abi, maxUint256 } from 'viem';
import { polygon } from 'viem/chains';

/**
 * Token approvals for the DEPOSIT-WALLET trading path (pUSD collateral, V2 exchange).
 * Distinct from utils/approvals.ts (old Safe path: USDC.e + V1 exchange).
 * The exact set below is the GROUND TRUTH from Rick's working DW on-chain and matches
 * server-side approve-dw.mjs + approve-dw-ctf.mjs:
 *   pUSD → ExchangeV2 (BUY settle) | CTF setApprovalForAll → ExchangeV2 + NegRisk (SELL)
 */
const PUSD = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';   // native (Circle) USDC on Polygon
const USDC_E = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';        // bridged USDC.e
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const EXCHANGE_V2 = '0xE111180000d2663C0091e4f400237545B87B996B';
const NEG_RISK = '0xC5d563A36AE78145C45a50134d48A1215220f80a';
// V2 neg-risk settlement (tournament/sports "who wins" markets). Without these,
// neg-risk BUYs — manual AND copies — reject "allowance is not enough" (the CLOB
// names 0xe2222 / 0xd91E as the spender).
const NEG_RISK_V2 = '0xe2222d279d744050d28e00520010520000310F59';
const NEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296';
// Full standard + neg-risk spender set: pUSD approve + CTF setApprovalForAll to each.
const ALL_SPENDERS = [EXCHANGE_V2, NEG_RISK, NEG_RISK_V2, NEG_RISK_ADAPTER];

const setApprovalForAllAbi = [
  { inputs: [{ name: 'operator', type: 'address' }, { name: 'approved', type: 'bool' }], name: 'setApprovalForAll', outputs: [], stateMutability: 'nonpayable', type: 'function' },
  { inputs: [{ name: 'account', type: 'address' }, { name: 'operator', type: 'address' }], name: 'isApprovedForAll', outputs: [{ name: '', type: 'bool' }], stateMutability: 'view', type: 'function' },
] as const;

const pub = createPublicClient({ chain: polygon, transport: http('https://polygon.drpc.org') });

export type DwCall = { target: string; value: string; data: `0x${string}` };

/** The 3 approval calls a deposit wallet needs to trade (executeDepositWalletBatch format). */
export function createDwApprovalCalls(): DwCall[] {
  return [
    ...ALL_SPENDERS.map((sp) => ({
      target: PUSD, value: '0',
      data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [sp as `0x${string}`, maxUint256] }),
    })),
    ...ALL_SPENDERS.map((op) => ({
      target: CTF, value: '0',
      data: encodeFunctionData({ abi: setApprovalForAllAbi, functionName: 'setApprovalForAll', args: [op as `0x${string}`, true] }),
    })),
  ];
}

/** Check whether the deposit wallet already has all 3 trading approvals on-chain. */
export async function checkDwApprovals(dw: string): Promise<{ allApproved: boolean }> {
  try {
    const threshold = 1000000000000n; // $1M of 6-decimal pUSD — effectively "max approved"
    const pusdAllowances = await Promise.all(ALL_SPENDERS.map((sp) => pub.readContract({
      address: PUSD, abi: erc20Abi, functionName: 'allowance',
      args: [dw as `0x${string}`, sp as `0x${string}`],
    })));
    const ctfApprovals = await Promise.all(ALL_SPENDERS.map((op) => pub.readContract({
      address: CTF, abi: setApprovalForAllAbi, functionName: 'isApprovedForAll',
      args: [dw as `0x${string}`, op as `0x${string}`],
    })));
    return { allApproved: pusdAllowances.every((a) => a >= threshold) && ctfApprovals.every(Boolean) };
  } catch {
    return { allApproved: false };
  }
}

/** Read the deposit wallet's pUSD cash balance (6 decimals, base units). */
export async function readPusdBalance(dw: string): Promise<bigint> {
  try {
    return await pub.readContract({
      address: PUSD, abi: erc20Abi, functionName: 'balanceOf', args: [dw as `0x${string}`],
    });
  } catch {
    return 0n;
  }
}

/** The native-USDC contract address (shown so depositors can verify before sending). */
export const USDC_NATIVE_ADDRESS = USDC_NATIVE;

/** Read the DW's deposit-relevant token balances (all 6-decimal base units). `incoming` =
 *  USDC the server watcher still has to convert; `pusd` = ready-to-trade collateral. */
export async function readDepositBalances(dw: string): Promise<{ nativeUsdc: bigint; usdce: bigint; pusd: bigint; incoming: bigint }> {
  try {
    const [nativeUsdc, usdce, pusd] = await Promise.all(
      [USDC_NATIVE, USDC_E, PUSD].map((token) => pub.readContract({
        address: token as `0x${string}`, abi: erc20Abi, functionName: 'balanceOf', args: [dw as `0x${string}`],
      })),
    );
    return { nativeUsdc, usdce, pusd, incoming: nativeUsdc + usdce };
  } catch {
    return { nativeUsdc: 0n, usdce: 0n, pusd: 0n, incoming: 0n };
  }
}

/** Withdraw call: the DW transfers `amountRaw` (6-dec base units) of pUSD to `to`. */
export function createWithdrawCall(to: string, amountRaw: bigint): DwCall {
  return {
    target: PUSD, value: '0',
    data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to as `0x${string}`, amountRaw] }),
  };
}

📜 Git History

b9c19bfchore(poli): reconcile local Flow/Insider/manual-trade work with deployed state3 days ago
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...