Initial commit
This commit is contained in:
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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); });
|
||||
@@ -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); });
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user