Files
2026-05-31 13:49:36 +08:00

111 lines
5.0 KiB
TypeScript

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); });