Initial commit

This commit is contained in:
doge-8
2026-05-31 13:49:36 +08:00
commit cd988d9c3a
55 changed files with 27001 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const RPC = 'https://polygon-bor-rpc.publicnode.com';
const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!;
const ADAPTER = '0xADa100874d00e3331D00F2007a9c336a65009718';
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const PUSD = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const ctfRO = new ethers.Contract(CTF, [
'function getCollectionId(bytes32, bytes32, uint256) view returns (bytes32)',
'function getPositionId(address, bytes32) view returns (uint256)',
'function balanceOf(address, uint256) view returns (uint256)',
'function payoutDenominator(bytes32) view returns (uint256)',
'function payoutNumerators(bytes32, uint256) view returns (uint256)',
], provider);
async function main() {
console.log('Safe:', SAFE);
console.log('CtfCollateralAdapter:', ADAPTER);
// Fetch all claimable (do not filter curPrice, see the full set)
const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`);
const arr: any[] = await r.json();
console.log(`data-api reports: ${arr.length} candidates\n`);
// For each conditionId, test both collateralToken types: CTF direct PUSD vs adapter
// But actually the V2 token uses adapter as the collateralToken to create the positionId, so:
// Old positions (V1, pre-migration) use PUSD as collateralToken
// New positions (V2 post-migration) use adapter as collateralToken
for (const p of arr.slice(0, 5)) {
console.log(`\n=== ${p.title} (cond=${p.conditionId.slice(0,12)}...) ===`);
const denom = await ctfRO.payoutDenominator(p.conditionId);
if (denom === 0n) { console.log('Not resolved'); continue; }
const num0 = await ctfRO.payoutNumerators(p.conditionId, 0);
const num1 = await ctfRO.payoutNumerators(p.conditionId, 1);
console.log(`payout: [${num0}, ${num1}] denom=${denom}`);
// Compute positionId using PUSD as collateralToken
for (const collat of [PUSD, ADAPTER]) {
console.log(` collateral=${collat === PUSD ? 'PUSD' : 'ADAPTER'}`);
for (const idx of [1, 2]) {
const collId = await ctfRO.getCollectionId(ethers.ZeroHash, p.conditionId, idx);
const posId = await ctfRO.getPositionId(collat, collId);
const bal = await ctfRO.balanceOf(SAFE, posId);
console.log(` indexSet=${idx} balance=${ethers.formatUnits(bal, 6)}`);
}
}
}
}
main().catch(e => console.error(e));
+64
View File
@@ -0,0 +1,64 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const RPC = 'https://polygon-bor-rpc.publicnode.com';
const SAFE = '0xeCbD41A018cAD2BdD3Fd560b40b472f6ff54c336';
const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const CONDITION_ID = '0x104f27e82a923cf3832854ae080bd8838ad288ee719e5cb1ac140e3b982d8f3d';
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const erc20 = new ethers.Contract(COLLATERAL, [
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
], provider);
const ctfIface = new ethers.Interface([
'function getPositionId(address collateralToken, bytes32 collectionId) view returns (uint256)',
'function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint256 indexSet) view returns (bytes32)',
'function balanceOf(address account, uint256 id) view returns (uint256)',
'function payoutDenominator(bytes32 conditionId) view returns (uint256)',
'function payoutNumerators(bytes32 conditionId, uint256 index) view returns (uint256)',
]);
const ctf = new ethers.Contract(CTF, ctfIface, provider);
async function main() {
const sym = await erc20.symbol();
const dec = await erc20.decimals();
const bal = await erc20.balanceOf(SAFE);
console.log(`Safe ${SAFE}`);
console.log(`${sym} balance: ${ethers.formatUnits(bal, dec)} (decimals=${dec})`);
// Query the token balance on each outcome (real on-chain data)
for (const indexSet of [1, 2]) {
const collId = await ctf.getCollectionId(ethers.ZeroHash, CONDITION_ID, indexSet);
const posId = await ctf.getPositionId(COLLATERAL, collId);
const tokBal = await ctf.balanceOf(SAFE, posId);
const human = ethers.formatUnits(tokBal, 6);
console.log(`indexSet=${indexSet} (${indexSet === 1 ? 'No' : 'Yes/Down?'}) tokenBalance=${human}`);
}
// payouts
const denom = await ctf.payoutDenominator(CONDITION_ID);
console.log(`payoutDenominator: ${denom}`);
for (let i = 0; i < 2; i++) {
const num = await ctf.payoutNumerators(CONDITION_ID, i);
console.log(`payoutNumerators[${i}]: ${num} (${num > 0n ? 'win' : 'lose'})`);
}
// Check whether data-api still lists it as redeemable
console.log('\n--- data-api positions ---');
const r1 = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`);
const a1: any[] = await r1.json();
console.log('redeemable=true returned:', a1.length, 'items');
a1.forEach(p => console.log(` curPrice=${p.curPrice} size=${p.size} value=${p.currentValue} outcome=${p.outcome} title=${p.title}`));
console.log('\n--- Recent activity ---');
const r2 = await fetch(`https://data-api.polymarket.com/activity?user=${SAFE}&limit=10`);
const a2: any[] = await r2.json();
a2.forEach(a => console.log(` ${a.type} ${a.title || ''} size=${a.size || ''} ts=${a.timestamp}`));
}
main().catch(e => console.error(e));
+84
View File
@@ -0,0 +1,84 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const RPC = 'https://polygon-bor-rpc.publicnode.com';
const TX = '0x098206326325e231fc5068c46fb7fabd5930b3789f0ed3a99cf2e0415a97a4ef';
const SAFE = '0xeCbD41A018cAD2BdD3Fd560b40b472f6ff54c336';
const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const CONDITION_ID = '0x104f27e82a923cf3832854ae080bd8838ad288ee719e5cb1ac140e3b982d8f3d';
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const ctfIface = new ethers.Interface([
'event PayoutRedemption(address indexed redeemer, address indexed collateralToken, bytes32 indexed parentCollectionId, bytes32 conditionId, uint256[] indexSets, uint256 payout)',
'function payoutNumerators(bytes32 conditionId, uint256 index) view returns (uint256)',
'function payoutDenominator(bytes32 conditionId) view returns (uint256)',
'function getOutcomeSlotCount(bytes32 conditionId) view returns (uint256)',
'function getPositionId(address collateralToken, bytes32 collectionId) view returns (uint256)',
'function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint256 indexSet) view returns (bytes32)',
'function balanceOf(address account, uint256 id) view returns (uint256)',
]);
const safeIface = new ethers.Interface([
'event ExecutionSuccess(bytes32 txHash, uint256 payment)',
'event ExecutionFailure(bytes32 txHash, uint256 payment)',
]);
const erc20Iface = new ethers.Interface([
'event Transfer(address indexed from, address indexed to, uint256 value)',
]);
async function main() {
const receipt = await provider.getTransactionReceipt(TX);
if (!receipt) { console.error('Could not fetch receipt'); return; }
console.log('Block:', receipt.blockNumber, 'Status:', receipt.status, 'Logs count:', receipt.logs.length);
// Parse all logs
for (const log of receipt.logs) {
console.log('\n---');
console.log('addr:', log.address);
// Try to parse PayoutRedemption
try {
const parsed = ctfIface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed) { console.log('CTF event:', parsed.name, parsed.args); continue; }
} catch {}
try {
const parsed = safeIface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed) { console.log('Safe event:', parsed.name, parsed.args); continue; }
} catch {}
try {
const parsed = erc20Iface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed) {
console.log('ERC20 Transfer:', parsed.args.from, '→', parsed.args.to, ethers.formatUnits(parsed.args.value, 6), '(assuming 6 decimals)');
continue;
}
} catch {}
console.log('Unknown event topics[0]:', log.topics[0]);
}
// Query condition status on-chain
console.log('\n--- Condition status ---');
const ctf = new ethers.Contract(CTF, ctfIface, provider);
const denom = await ctf.payoutDenominator(CONDITION_ID);
console.log('payoutDenominator:', denom.toString());
if (denom > 0n) {
const slot = await ctf.getOutcomeSlotCount(CONDITION_ID);
console.log('outcomeSlotCount:', slot.toString());
for (let i = 0; i < Number(slot); i++) {
const num = await ctf.payoutNumerators(CONDITION_ID, i);
console.log(` slot[${i}] payout:`, num.toString());
}
} else {
console.log('⚠️ payoutDenominator=0 means the market has not resolved / has not reportPayouts yet');
}
// Query the Safe's previous token balances on both outcomes (after redeem they should theoretically both be 0)
console.log('\n--- Safe current token balances ---');
for (const indexSet of [1, 2]) {
const collId = await ctf.getCollectionId(ethers.ZeroHash, CONDITION_ID, indexSet);
const posId = await ctf.getPositionId(COLLATERAL, collId);
const bal = await ctf.balanceOf(SAFE, posId);
console.log(`indexSet=${indexSet} positionId=${posId.toString().slice(0,20)}... balance=${bal}`);
}
}
main().catch(e => console.error(e));
+54
View File
@@ -0,0 +1,54 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const RPC = 'https://polygon-bor-rpc.publicnode.com';
const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!;
const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const ctf = new ethers.Contract(CTF, [
'function payoutDenominator(bytes32) view returns (uint256)',
'function payoutNumerators(bytes32, uint256) view returns (uint256)',
'function getCollectionId(bytes32, bytes32, uint256) view returns (bytes32)',
'function getPositionId(address, bytes32) view returns (uint256)',
'function balanceOf(address, uint256) view returns (uint256)',
], provider);
async function main() {
console.log('Safe:', SAFE);
// data-api candidates
const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`);
const arr: any[] = await r.json();
console.log(`data-api reports: ${arr.length} redeemable, of which curPrice=1: ${arr.filter(p=>p.curPrice===1).length}\n`);
let realTotal = 0;
let fakeCount = 0;
for (const p of arr) {
const denom = await ctf.payoutDenominator(p.conditionId);
if (denom === 0n) {
console.log(`${p.title} | market not resolved`);
continue;
}
let realPayout = 0n;
for (const idx of [1, 2]) {
const collId = await ctf.getCollectionId(ethers.ZeroHash, p.conditionId, idx);
const posId = await ctf.getPositionId(COLLATERAL, collId);
const bal = await ctf.balanceOf(SAFE, posId);
const num = await ctf.payoutNumerators(p.conditionId, idx - 1);
realPayout += bal * num / denom;
}
const real = Number(ethers.formatUnits(realPayout, 6));
if (real > 0) {
console.log(`✓ Claimable $${real.toFixed(4)} | ${p.title} | data-api reports $${p.currentValue.toFixed(4)}`);
realTotal += real;
} else {
console.log(`✗ Already claimed $0 | ${p.title} | data-api still reports $${p.currentValue.toFixed(4)} (index not refreshed)`);
fakeCount++;
}
}
console.log(`\n=== On-chain real claimable: $${realTotal.toFixed(4)} | data-api stale data: ${fakeCount} items ===`);
}
main().catch(e => console.error(e));
+45
View File
@@ -0,0 +1,45 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!;
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const provider = new ethers.JsonRpcProvider('https://polygon-bor-rpc.publicnode.com', 137, { staticNetwork: true });
const ctf = new ethers.Contract(CTF, [
'function balanceOfBatch(address[], uint256[]) view returns (uint256[])',
], provider);
async function main() {
console.log('Safe:', SAFE);
const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`);
const arr: any[] = await r.json();
const allTokens = new Set<string>();
for (const p of arr) {
if (p.asset) allTokens.add(p.asset);
if (p.oppositeAsset) allTokens.add(p.oppositeAsset);
}
const ids = [...allTokens].map(s => BigInt(s));
console.log(`Checking ${ids.length} tokenIds (from data-api asset/oppositeAsset fields)\n`);
const accounts = ids.map(() => SAFE);
const balances: bigint[] = await ctf.balanceOfBatch(accounts, ids);
let nonZero = 0;
let totalRedeemable = 0;
for (let i = 0; i < ids.length; i++) {
if (balances[i] > 0n) {
nonZero++;
const p = arr.find(x => BigInt(x.asset) === ids[i] || (x.oppositeAsset && BigInt(x.oppositeAsset) === ids[i]));
const isMain = p && BigInt(p.asset) === ids[i];
const outcome = isMain ? p?.outcome : p?.oppositeOutcome;
const bal = Number(ethers.formatUnits(balances[i], 6));
console.log(`✓ balance=${bal.toFixed(4)} | ${p?.title} | ${outcome} | conditionId=${p?.conditionId.slice(0,12)}...`);
// Is this outcome the winner? data-api does not give payout directly, infer from curPrice
if (isMain && p.curPrice === 1) totalRedeemable += bal;
if (!isMain && p.curPrice === 0) totalRedeemable += bal; // opposite is the winner
}
}
console.log(`\n=== Safe holds ${nonZero} non-zero tokens, total winning shares $${totalRedeemable.toFixed(4)} ===`);
}
main().catch(e => console.error(e));
+110
View File
@@ -0,0 +1,110 @@
import 'dotenv/config';
import { ethers } from 'ethers';
import { getContractConfig } from '@polymarket/clob-client-v2';
const RPC = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com';
const PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY!;
const PROXY = process.env.POLYMARKET_PROXY_ADDRESS!;
const DRY_RUN = process.argv.includes('--dry-run');
if (!PRIVATE_KEY || !PROXY) { console.error('Missing env'); process.exit(1); }
async function main() {
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const contracts = getContractConfig(137);
const CTF = contracts.conditionalTokens;
const COLLATERAL = contracts.collateral;
// V2: redeem goes through CtfCollateralAdapter, which internally redeems CTF + auto-wraps pUSD
const ADAPTER = '0xADa100874d00e3331D00F2007a9c336a65009718';
const ZERO_BYTES32 = '0x' + '0'.repeat(64);
console.log('EOA:', wallet.address);
console.log('Safe:', PROXY);
console.log('CTF:', CTF);
console.log('Adapter:', ADAPTER);
console.log('Collateral:', COLLATERAL);
console.log('RPC:', RPC);
console.log('DRY_RUN:', DRY_RUN);
const ctfIface = new ethers.Interface([
'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)'
]);
const safeIface = new ethers.Interface([
'function nonce() view returns (uint256)',
'function getOwners() view returns (address[])',
'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce) view returns (bytes32)',
'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures) public payable returns (bool)',
]);
const safe = new ethers.Contract(PROXY, safeIface, wallet);
const owners: string[] = await safe.getOwners();
console.log('Safe owners:', owners);
const isOwner = owners.map(o => o.toLowerCase()).includes(wallet.address.toLowerCase());
console.log('EOA is owner?:', isOwner);
if (!isOwner) { console.error('❌ EOA is not a Safe owner'); process.exit(1); }
const bal = await provider.getBalance(wallet.address);
console.log('EOA POL balance:', ethers.formatEther(bal));
// Query claimable positions
const res = await fetch(`https://data-api.polymarket.com/positions?user=${PROXY}&sizeThreshold=.01&redeemable=true&limit=100&offset=0`);
const arr: any[] = await res.json();
const claimable = arr.filter(p => p.curPrice === 1);
console.log(`\nClaimable position count: ${claimable.length}`);
claimable.forEach(p => console.log(` - ${p.title} | $${p.currentValue.toFixed(4)} | ${p.conditionId}`));
if (!claimable.length) { console.log('Nothing to claim, exiting'); return; }
for (let i = 0; i < claimable.length; i++) {
const p = claimable[i];
console.log(`\n[${i+1}/${claimable.length}] ${p.title}`);
try {
const calldata = ctfIface.encodeFunctionData('redeemPositions', [
COLLATERAL, ZERO_BYTES32, p.conditionId, [1, 2]
]);
const nonce: bigint = await safe.nonce();
console.log(` nonce: ${nonce}`);
const txHash: string = await safe.getTransactionHash(
ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce
);
const sig = await wallet.signMessage(ethers.getBytes(txHash));
const v = parseInt(sig.slice(-2), 16) + 4;
const adjustedSig = sig.slice(0, -2) + v.toString(16).padStart(2, '0');
// First verify with estimateGas / staticCall
try {
await safe.execTransaction.staticCall(
ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig
);
console.log(' ✓ staticCall passed');
} catch (e: any) {
console.error(` ✗ staticCall failed: ${e.shortMessage || e.message}`);
continue;
}
const gasEst = await safe.execTransaction.estimateGas(
ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig
);
console.log(` estimateGas: ${gasEst}`);
if (DRY_RUN) {
console.log(' (DRY_RUN, skipping send)');
continue;
}
const tx = await safe.execTransaction(
ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig
);
console.log(` Sent txHash: ${tx.hash}`);
const receipt = await Promise.race([
tx.wait(),
new Promise((_, reject) => setTimeout(() => reject(new Error('On-chain timeout 60s')), 60000))
]) as any;
console.log(` ✓ Success block:${receipt.blockNumber} gasUsed:${receipt.gasUsed}`);
} catch (e: any) {
console.error(` ✗ Failed: ${e.shortMessage || e.message}`);
}
}
}
main().catch(e => { console.error(e); process.exit(1); });
+151
View File
@@ -0,0 +1,151 @@
import 'dotenv/config';
import { ethers } from 'ethers';
import { getContractConfig } from '@polymarket/clob-client-v2';
const PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY!;
const PROXY = process.env.POLYMARKET_PROXY_ADDRESS!;
const RPC = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com';
const RELAYER = 'https://relayer-v2.polymarket.com';
const MULTISEND = '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761';
const DRY_RUN = process.argv.includes('--dry-run');
if (!PRIVATE_KEY || !PROXY) { console.error('Missing env'); process.exit(1); }
const ZERO32 = '0x' + '0'.repeat(64);
const ctfIface = new ethers.Interface([
'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)'
]);
const safeIface = new ethers.Interface([
'function nonce() view returns (uint256)',
'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce) view returns (bytes32)',
]);
// Pack redeemPositions into a single multiSend item: [op(1)][to(20)][value(32)][dataLen(32)][data]
function encodeMultiSendItem(to: string, data: string): string {
const op = '00'; // CALL
const toHex = to.toLowerCase().replace(/^0x/, '').padStart(40, '0');
const valueHex = '0'.repeat(64);
const dataBytes = ethers.getBytes(data);
const lenHex = dataBytes.length.toString(16).padStart(64, '0');
const dataHex = data.replace(/^0x/, '');
return op + toHex + valueHex + lenHex + dataHex;
}
async function main() {
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const contracts = getContractConfig(137);
const CTF = contracts.conditionalTokens;
const COLLATERAL = contracts.collateral;
console.log('EOA:', wallet.address);
console.log('Proxy(Safe):', PROXY);
console.log('CTF:', CTF, ' Collateral:', COLLATERAL);
// 1. Query claimable
const res = await fetch(`https://data-api.polymarket.com/positions?user=${PROXY}&sizeThreshold=.01&redeemable=true&limit=100&offset=0`);
const arr: any[] = await res.json();
const claimable = arr.filter(p => p.curPrice === 1);
console.log(`Claimable position count: ${claimable.length}`);
claimable.forEach(p => console.log(` - ${p.title} | $${p.currentValue.toFixed(4)}`));
if (!claimable.length) { console.log('Nothing to claim, exiting'); return; }
const total = claimable.reduce((s, p) => s + p.currentValue, 0);
console.log(`Total: $${total.toFixed(4)}`);
// 2. Single redeemPositions (per the official docs example, operation=0 CALL sent directly to CTF)
const p = claimable[0];
const data = ctfIface.encodeFunctionData('redeemPositions', [
COLLATERAL, ZERO32, p.conditionId, [1, 2]
]);
console.log('redeemPositions data length:', data.length / 2 - 1, 'bytes');
// 3. Compute Safe txHash + signature
const safe = new ethers.Contract(PROXY, safeIface, provider);
const nonce: bigint = await safe.nonce();
console.log('Safe nonce:', nonce.toString());
const txHash: string = await safe.getTransactionHash(
CTF, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce
);
console.log('Safe txHash:', txHash);
const sig = await wallet.signMessage(ethers.getBytes(txHash));
const v = parseInt(sig.slice(-2), 16) + 4;
const adjustedSig = sig.slice(0, -2) + v.toString(16).padStart(2, '0');
// 4. Build the request body (per the official docs: operation=0 CALL, to=CTF, no metadata)
const body = {
from: wallet.address,
to: CTF,
proxyWallet: PROXY,
data,
nonce: nonce.toString(),
signature: adjustedSig,
signatureParams: {
gasPrice: '0',
operation: '0',
safeTxnGas: '0',
baseGas: '0',
gasToken: ethers.ZeroAddress,
refundReceiver: ethers.ZeroAddress,
},
type: 'SAFE',
};
if (DRY_RUN) {
console.log('\n--- DRY_RUN body ---');
console.log(JSON.stringify(body, null, 2));
return;
}
// 5. Submit
const RELAYER_API_KEY = process.env.RELAYER_API_KEY!;
const RELAYER_API_KEY_ADDRESS = process.env.RELAYER_API_KEY_ADDRESS!;
if (!RELAYER_API_KEY || !RELAYER_API_KEY_ADDRESS) {
console.error('Missing RELAYER_API_KEY / RELAYER_API_KEY_ADDRESS env');
process.exit(1);
}
console.log('\nSubmitting to relayer...');
// Use undici fetch to explicitly preserve header case
const { fetch: undiciFetch } = await import('undici');
const submitRes: any = await undiciFetch(`${RELAYER}/submit`, {
method: 'POST',
headers: [
['Content-Type', 'application/json'],
['RELAYER_API_KEY', RELAYER_API_KEY],
['RELAYER_API_KEY_ADDRESS', RELAYER_API_KEY_ADDRESS],
],
body: JSON.stringify(body),
});
const submitText = await submitRes.text();
console.log('Status:', submitRes.status);
console.log('Body:', submitText);
if (submitRes.status === 401) {
// Retry with curl to confirm whether the server really cannot see our headers
console.log('\n[debug] Retrying with curl');
const curlCmd = `curl -s -i -X POST '${RELAYER}/submit' -H 'Content-Type: application/json' -H 'RELAYER_API_KEY: ${RELAYER_API_KEY}' -H 'RELAYER_API_KEY_ADDRESS: ${RELAYER_API_KEY_ADDRESS}' -d '${JSON.stringify(body)}'`;
const { execSync } = await import('child_process');
try {
const out = execSync(curlCmd, { encoding: 'utf8' });
console.log(out);
} catch (e: any) {
console.error(e.message);
}
return;
}
if (!submitRes.ok) { process.exit(1); }
const { transactionID, transactionHash, state } = JSON.parse(submitText);
console.log(`✓ Submitted successfully id:${transactionID} txHash:${transactionHash} state:${state}`);
// 6. Poll
for (let i = 0; i < 20; i++) {
await new Promise(r => setTimeout(r, 2000));
const r = await fetch(`${RELAYER}/transaction?id=${transactionID}`);
const j = await r.json();
console.log(`[${i+1}] state:${j.state} txHash:${j.transactionHash}`);
if (j.state && j.state !== 'STATE_NEW' && j.state !== 'STATE_PENDING') {
console.log('Final state:', j);
break;
}
}
}
main().catch(e => { console.error('Error:', e); process.exit(1); });
+66
View File
@@ -0,0 +1,66 @@
import 'dotenv/config';
import { ethers } from 'ethers';
const RPC = 'https://polygon-bor-rpc.publicnode.com';
const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!;
const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true });
// Scan all tokens held by the Safe via TransferSingle/TransferBatch events
const ctfIface = new ethers.Interface([
'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)',
'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)',
'function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])',
]);
const ctf = new ethers.Contract(CTF, ctfIface, provider);
async function main() {
console.log('Safe:', SAFE);
const latest = await provider.getBlockNumber();
console.log('Current block:', latest);
// Scan the last 50000 blocks (~30 hours, polygon ~2.3s/block)
const fromBlock = latest - 50000;
console.log(`Scanning ${fromBlock}${latest}`);
const safePadded = ethers.zeroPadValue(SAFE, 32);
const TRANSFER_SINGLE = ctfIface.getEvent('TransferSingle')!.topicHash;
// Scan in segments
const tokenIds = new Set<string>();
const STEP = 10000;
for (let from = fromBlock; from <= latest; from += STEP) {
const to = Math.min(from + STEP - 1, latest);
// to = SAFE
const logsIn = await provider.getLogs({
address: CTF,
topics: [TRANSFER_SINGLE, null, null, safePadded],
fromBlock: from, toBlock: to,
});
for (const log of logsIn) {
const parsed = ctfIface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed) tokenIds.add(parsed.args.id.toString());
}
process.stdout.write(`.`);
}
console.log(`\nFound ${tokenIds.size} distinct tokenIds (received within the last 30h)`);
if (tokenIds.size === 0) return;
// Batch query balances
const ids = [...tokenIds];
const accounts = ids.map(() => SAFE);
const balances: bigint[] = await ctf.balanceOfBatch(accounts, ids);
let nonZero = 0;
for (let i = 0; i < ids.length; i++) {
if (balances[i] > 0n) {
nonZero++;
console.log(`tokenId=${ids[i]} balance=${ethers.formatUnits(balances[i], 6)}`);
}
}
console.log(`\n=== Total ${nonZero} tokens with balance > 0 ===`);
}
main().catch(e => console.error(e));
+198
View File
@@ -0,0 +1,198 @@
/**
* Polymarket Proxy Wallet withdraw test script
*
* Uses Gnosis Safe execTransaction: EOA signs SafeTx EIP-712 hash -> call Proxy to transfer out USDC
* Does not depend on the official Polymarket API, interacts directly with the on-chain Safe + USDC contracts
*
* Usage:
* 1. Fill in CONFIG below (private key / Proxy / target address / withdraw amount)
* 2. tsx scripts/test-withdraw.ts
*
* Safety: DRY_RUN=true by default, only prints, does not send transactions. Change to false after confirming the params are correct.
*/
import { ethers } from "ethers";
import { readFileSync, existsSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
// Simple .env loader (no third-party package)
const __dirname = dirname(fileURLToPath(import.meta.url));
const ENV_FILE = resolve(__dirname, "..", ".env");
if (existsSync(ENV_FILE)) {
for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) {
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
if (!m) continue;
let val = m[2];
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
if (process.env[m[1]] === undefined) process.env[m[1]] = val;
}
}
// ── Config ──────────────────────────────────────────────────
// Private key and Proxy are read from .env (POLYMARKET_PRIVATE_KEY / POLYMARKET_PROXY_ADDRESS)
// This test script only tests one wallet; for production batch withdraws write a separate script that reads multi-wallet config
const CONFIG = {
// Withdraw target wallet (required)
TO_ADDRESS: "0x__TARGET_ADDRESS__",
// Withdraw amount (USDC, 6 decimals). Use 0.01 for testing
AMOUNT_USDC: "0.01",
// true = only print, do not send on-chain; false = actually send the transaction
DRY_RUN: true,
} as const;
const EOA_PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY || "";
const PROXY_ADDRESS = process.env.POLYMARKET_PROXY_ADDRESS || "";
if (!EOA_PRIVATE_KEY || !PROXY_ADDRESS) {
console.error("✗ .env is missing POLYMARKET_PRIVATE_KEY or POLYMARKET_PROXY_ADDRESS");
process.exit(1);
}
// ── Constants ────────────────────────────────────────────────────
const POLYGON_RPC = "https://polygon-bor-rpc.publicnode.com";
const POLYGON_CHAIN_ID = 137;
// Polymarket uses USDC.e (PoS bridge USDC), not native USDC
const USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const USDC_DECIMALS = 6;
// Gnosis Safe ABI (only the parts used)
const SAFE_ABI = [
"function nonce() view returns (uint256)",
"function getThreshold() view returns (uint256)",
"function getOwners() view returns (address[])",
"function execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures) payable returns (bool)",
"function VERSION() view returns (string)",
];
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address,uint256)",
"function decimals() view returns (uint8)",
];
// SafeTx EIP-712 type
const EIP712_SAFE_TX_TYPE = {
SafeTx: [
{ type: "address", name: "to" },
{ type: "uint256", name: "value" },
{ type: "bytes", name: "data" },
{ type: "uint8", name: "operation" },
{ type: "uint256", name: "safeTxGas" },
{ type: "uint256", name: "baseGas" },
{ type: "uint256", name: "gasPrice" },
{ type: "address", name: "gasToken" },
{ type: "address", name: "refundReceiver" },
{ type: "uint256", name: "nonce" },
],
};
async function main() {
console.log("─── Polymarket Proxy withdraw test ───");
console.log(`DRY_RUN: ${CONFIG.DRY_RUN}`);
const provider = new ethers.JsonRpcProvider(POLYGON_RPC, POLYGON_CHAIN_ID);
const eoa = new ethers.Wallet(EOA_PRIVATE_KEY, provider);
console.log(`EOA: ${eoa.address}`);
console.log(`Proxy: ${PROXY_ADDRESS}`);
console.log(`To: ${CONFIG.TO_ADDRESS}`);
// 1. Check Safe status
const safe = new ethers.Contract(PROXY_ADDRESS, SAFE_ABI, provider);
const [owners, threshold, safeNonce] = await Promise.all([
safe.getOwners() as Promise<string[]>,
safe.getThreshold() as Promise<bigint>,
safe.nonce() as Promise<bigint>,
]);
console.log(`Owners: ${owners.join(", ")}`);
console.log(`Threshold: ${threshold} Nonce: ${safeNonce}`);
if (!owners.map(o => o.toLowerCase()).includes(eoa.address.toLowerCase())) {
throw new Error("EOA is not an owner of the Proxy, misconfigured");
}
if (threshold !== 1n) {
throw new Error(`Proxy threshold=${threshold}, this script only supports 1/1 Safe`);
}
// 2. Check USDC balance
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
const balance = await usdc.balanceOf(PROXY_ADDRESS) as bigint;
const balanceHuman = ethers.formatUnits(balance, USDC_DECIMALS);
console.log(`Proxy USDC balance: ${balanceHuman}`);
const amountWei = ethers.parseUnits(CONFIG.AMOUNT_USDC, USDC_DECIMALS);
if (amountWei > balance) {
throw new Error(`Insufficient balance: trying to withdraw ${CONFIG.AMOUNT_USDC}, only have ${balanceHuman}`);
}
console.log(`Withdraw amount: ${CONFIG.AMOUNT_USDC} USDC (${amountWei} wei)`);
// 3. Check EOA MATIC (pays gas)
const maticBalance = await provider.getBalance(eoa.address);
console.log(`EOA MATIC: ${ethers.formatEther(maticBalance)}`);
if (maticBalance < ethers.parseEther("0.01")) {
console.warn("⚠ EOA MATIC < 0.01, may not have enough gas");
}
// 4. Build the callData for USDC.transfer(to, amount)
const usdcIface = new ethers.Interface(ERC20_ABI);
const transferData = usdcIface.encodeFunctionData("transfer", [CONFIG.TO_ADDRESS, amountWei]);
console.log(`transferData: ${transferData}`);
// 5. Assemble SafeTx
const safeTx = {
to: USDC_ADDRESS,
value: 0n,
data: transferData,
operation: 0, // CALL
safeTxGas: 0n,
baseGas: 0n,
gasPrice: 0n,
gasToken: ethers.ZeroAddress,
refundReceiver: ethers.ZeroAddress,
nonce: safeNonce,
};
// 6. EIP-712 signature
const domain = {
chainId: POLYGON_CHAIN_ID,
verifyingContract: PROXY_ADDRESS,
};
const signature = await eoa.signTypedData(domain, EIP712_SAFE_TX_TYPE, safeTx);
console.log(`SafeTx signature: ${signature}`);
if (CONFIG.DRY_RUN) {
console.log("\n✓ DRY_RUN: all params OK, no transaction sent");
console.log(" Set CONFIG.DRY_RUN = false and run again to actually withdraw");
return;
}
// 7. Send on-chain: call Proxy.execTransaction(...)
const safeWithSigner = safe.connect(eoa) as ethers.Contract;
console.log("\nSending execTransaction…");
const tx = await safeWithSigner.execTransaction(
safeTx.to,
safeTx.value,
safeTx.data,
safeTx.operation,
safeTx.safeTxGas,
safeTx.baseGas,
safeTx.gasPrice,
safeTx.gasToken,
safeTx.refundReceiver,
signature
);
console.log(`tx hash: ${tx.hash}`);
console.log("Waiting for confirmation…");
const rec = await tx.wait();
if (rec?.status === 1) {
console.log(`✓ Success block=${rec.blockNumber} gas=${rec.gasUsed}`);
const newBal = await usdc.balanceOf(PROXY_ADDRESS) as bigint;
console.log(`Proxy new balance: ${ethers.formatUnits(newBal, USDC_DECIMALS)} USDC`);
} else {
console.log("✗ tx mined but status=0 (execTransaction reverted internally)");
}
}
main().catch(err => {
console.error("✗ Failed:", err instanceof Error ? err.message : err);
process.exit(1);
});