ca6661cb94
Critical path closed: bridge conversion ($24.73→pUSD, fee-free), executor ported and round-trip proven ($5 FAK both legs), armed bot restarted clean. README gains gotcha 16 (pUSD stack: paused native-USDC onramp, bridge is the sanctioned conversion, builder-key revocation semantics) + bridge/ relayer data-source rows. Probe & wrap scripts atexit-revoke their builder keys (9 inert ones accumulated during bring-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
214 lines
9.7 KiB
Python
214 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""One-time bankroll conversion: native USDC (deposit wallet) → pUSD collateral.
|
|
|
|
WHY (2026-07-10, verified on-chain): the 2026 exchange's balance view counts
|
|
ONLY the pUSD CollateralToken (0xC011a7…). pUSD accepts BOTH native USDC and
|
|
USDC.e by contract (`onlyValidAsset`), but the public CollateralOnramp
|
|
(0x93070a…) has native USDC PAUSED (paused(0x3c499c…)=1 — the exact cause of
|
|
the 'batch would revert' on the direct wrap attempt). The sanctioned path for
|
|
native USDC is Polymarket's own Bridge (the UI deposit flow): it swaps native
|
|
USDC → USDC.e and delivers to the registered wallet; USDC.e wrapping is live.
|
|
|
|
Flow (stage-gated, aborts on any failed link — never guesses with live funds):
|
|
A. bridge a $3 test slice: gasless ERC-20 transfer → per-wallet bridge
|
|
deposit address → poll until USDC.e lands at the deposit wallet
|
|
B. bridge the remainder the same way
|
|
C. gasless approve(onramp) + onramp.wrap(USDC.e, wallet, all) → pUSD
|
|
D. success = get_balance_allowance(COLLATERAL) ≈ the bankroll
|
|
|
|
Run on the live Fly box (needs LIVE_PRIVATE_KEY; geo-gated venue):
|
|
python3 host/wrap_via_bridge.py
|
|
"""
|
|
import json
|
|
import os
|
|
import ssl
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
|
|
_SSL = ssl._create_unverified_context()
|
|
|
|
USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" # native (paused on onramp)
|
|
USDCE = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" # bridged (wrappable)
|
|
PUSD = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" # CollateralToken
|
|
ONRAMP = "0x93070a847efEf7F70739046A929D47a521F5B8ee" # CollateralOnramp
|
|
DW = "0x455e252e45Ee46d6C4cc1c8fAdD3899d68f245a1" # our deposit wallet
|
|
# bridge deposit address minted for DW on 2026-07-10 (bridge.polymarket.com);
|
|
# pinned so a drifting registration answer aborts instead of misdirecting funds
|
|
EXPECTED_BRIDGE_EVM = "0x66C1A4b43824CB0DDa54c94F118fc868A6270b91"
|
|
BRIDGE = "https://bridge.polymarket.com"
|
|
TEST_SLICE = 3_000_000 # $3 — above the bridge's $2 Polygon minimum
|
|
POLL_S, DELIVER_TIMEOUT_S = 15, 900
|
|
|
|
|
|
def http(url, payload=None):
|
|
data = json.dumps(payload).encode() if payload is not None else None
|
|
req = urllib.request.Request(url, data=data, headers={
|
|
"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"})
|
|
return json.load(urllib.request.urlopen(req, timeout=30, context=_SSL))
|
|
|
|
|
|
def rpc_call(to, data):
|
|
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call",
|
|
"params": [{"to": to, "data": data}, "latest"]}).encode()
|
|
url = os.environ.get("ALCHEMY_RPC_URL", "https://polygon.drpc.org").strip()
|
|
req = urllib.request.Request(url, data=body,
|
|
headers={"Content-Type": "application/json"})
|
|
return json.load(urllib.request.urlopen(req, timeout=20, context=_SSL))["result"]
|
|
|
|
|
|
def bal(token, holder=DW):
|
|
return int(rpc_call(token, "0x70a08231" + holder[2:].lower().rjust(64, "0")), 16)
|
|
|
|
|
|
def fmt(u):
|
|
return f"${u/1e6:,.2f}"
|
|
|
|
|
|
def wait_handle(label, h):
|
|
out = h.wait() if h is not None and hasattr(h, "wait") else h
|
|
print(f" {label}: {str(out)[:220]}")
|
|
return out
|
|
|
|
|
|
def delivered_units():
|
|
"""pUSD + USDC.e at the wallet — the $3 test slice (2026-07-10) proved the
|
|
bridge wraps on delivery (native → USDC.e swap, then pUSD credited), so
|
|
delivery must be measured on pUSD; USDC.e is counted in case a future
|
|
bridge change stops short of the wrap (the wrap step below picks it up)."""
|
|
return bal(PUSD) + bal(USDCE)
|
|
|
|
|
|
def bridge_leg(client, calls_mod, amount, bridge_addr, label):
|
|
"""Gasless-transfer `amount` native USDC to the bridge and wait until the
|
|
deposit wallet's pUSD/USDC.e rises by ~amount. Returns True on delivery."""
|
|
before = delivered_units()
|
|
print(f"\n[{label}] sending {fmt(amount)} native USDC → bridge {bridge_addr}")
|
|
t = calls_mod.erc20_transfer_call(token_address=USDC, recipient=bridge_addr,
|
|
amount=amount)
|
|
try:
|
|
h = client.execute_transaction(calls=[t], metadata=f"bridge deposit {label}")
|
|
wait_handle("relayer", h)
|
|
except Exception as e:
|
|
print(f" transfer FAILED before leaving the wallet: {type(e).__name__} {e}")
|
|
return False
|
|
deadline = time.time() + DELIVER_TIMEOUT_S
|
|
while time.time() < deadline:
|
|
time.sleep(POLL_S)
|
|
now = delivered_units()
|
|
try:
|
|
st = http(f"{BRIDGE}/status/{bridge_addr}")
|
|
txs = st.get("transactions") or []
|
|
latest = txs[0].get("status") if txs else "none"
|
|
except Exception as e:
|
|
latest = f"status err {e}"
|
|
print(f" pUSD+USDC.e {fmt(now)} (Δ{fmt(now-before)}) · bridge: {latest}", flush=True)
|
|
# bridge swap costs a few tenths of a cent — 1% tolerance is generous
|
|
if now - before >= amount * 0.99:
|
|
print(f" [{label}] DELIVERED")
|
|
return True
|
|
print(f" [{label}] NOT delivered within {DELIVER_TIMEOUT_S}s — ABORT. "
|
|
f"Track {BRIDGE}/status/{bridge_addr}; funds are at the bridge, "
|
|
f"recovery: https://matic-recovery.polymarket.com/")
|
|
return False
|
|
|
|
|
|
def main():
|
|
pk = os.environ["LIVE_PRIVATE_KEY"].strip()
|
|
from polymarket import SecureClient
|
|
from polymarket import calls as pmcalls
|
|
from web3 import Web3
|
|
|
|
native = bal(USDC)
|
|
print(f"deposit wallet {DW}\n native USDC {fmt(native)} · USDC.e "
|
|
f"{fmt(bal(USDCE))} · pUSD {fmt(bal(PUSD))}")
|
|
|
|
# pause-state preflight: if Polymarket unpauses native USDC later, the
|
|
# bridge becomes unnecessary — but also if they pause USDC.e we must stop.
|
|
paused_sel = Web3.keccak(text="paused(address)")[:4].hex()
|
|
usdce_paused = int(rpc_call(ONRAMP, "0x" + paused_sel
|
|
+ USDCE[2:].lower().rjust(64, "0")), 16)
|
|
if usdce_paused:
|
|
sys.exit("onramp has USDC.e PAUSED — nothing safe to do, aborting")
|
|
|
|
boot = SecureClient.create(private_key=pk)
|
|
with boot:
|
|
bk = boot.create_builder_api_key()
|
|
print("builder key minted (in-process only)")
|
|
client = SecureClient.create(private_key=pk, api_key=bk)
|
|
# revoke on ANY exit path — unrevoked keys are inert but pile up forever
|
|
# (revocation is HMAC-authed by the key's own secret; see order_probe_v2)
|
|
import atexit
|
|
atexit.register(lambda: (client.revoke_builder_api_key(),
|
|
print("builder key revoked")))
|
|
with client:
|
|
if str(client.wallet).lower() != DW.lower():
|
|
sys.exit(f"client.wallet {client.wallet} != expected {DW} — aborting")
|
|
|
|
if native > 0:
|
|
reg = http(f"{BRIDGE}/deposit", {"address": DW})
|
|
bridge_addr = (reg.get("address") or {}).get("evm", "")
|
|
print(f"bridge deposit address: {bridge_addr}")
|
|
if bridge_addr.lower() != EXPECTED_BRIDGE_EVM.lower():
|
|
sys.exit(f"bridge address drifted (expected {EXPECTED_BRIDGE_EVM})"
|
|
" — aborting, investigate before sending funds")
|
|
if native <= TEST_SLICE + 2_000_000: # too small to stage: one leg
|
|
legs = [(native, "all")]
|
|
else:
|
|
legs = [(TEST_SLICE, "test-slice"),
|
|
(native - TEST_SLICE, "remainder")]
|
|
for amount, label in legs:
|
|
if not bridge_leg(client, pmcalls, amount, bridge_addr, label):
|
|
sys.exit(1)
|
|
else:
|
|
print("no native USDC left to bridge — proceeding to wrap")
|
|
|
|
usdce = bal(USDCE)
|
|
if usdce == 0:
|
|
# normal: the bridge wraps on delivery, so everything is already pUSD
|
|
print(f"\nno USDC.e residue — bridge delivered wrapped. "
|
|
f"pUSD {fmt(bal(PUSD))}")
|
|
try:
|
|
b = client.get_balance_allowance(asset_type="COLLATERAL")
|
|
print("exchange-view COLLATERAL:", str(b)[:220])
|
|
except Exception as e:
|
|
print("balance check raised:", type(e).__name__, str(e)[:150])
|
|
return
|
|
print(f"\n[wrap] approve onramp + wrap {fmt(usdce)} USDC.e → pUSD")
|
|
appr = pmcalls.erc20_approval_call(token_address=USDCE, spender=ONRAMP,
|
|
amount=usdce)
|
|
wrap_sel = Web3.keccak(text="wrap(address,address,uint256)")[:4]
|
|
from eth_abi import encode as abi_encode
|
|
wrap_data = "0x" + wrap_sel.hex().replace("0x", "") + abi_encode(
|
|
["address", "address", "uint256"], [USDCE, DW, usdce]).hex()
|
|
wrap_call = pmcalls.TransactionCall(to=ONRAMP, data=wrap_data)
|
|
try:
|
|
h = client.execute_transaction(calls=[appr, wrap_call],
|
|
metadata="wrap USDC.e to pUSD")
|
|
wait_handle("batch approve+wrap", h)
|
|
except Exception as e:
|
|
print(f" batch failed ({type(e).__name__} {str(e)[:150]}) — "
|
|
"falling back to two sequential transactions")
|
|
try:
|
|
wait_handle("approve", client.execute_transaction(
|
|
calls=[appr], metadata="approve USDC.e to onramp"))
|
|
wait_handle("wrap", client.execute_transaction(
|
|
calls=[wrap_call], metadata="wrap USDC.e to pUSD"))
|
|
except Exception as e2:
|
|
print(f" sequential wrap FAILED too: {type(e2).__name__} {e2}")
|
|
sys.exit(1)
|
|
|
|
time.sleep(5)
|
|
print(f"\nfinal balances · native {fmt(bal(USDC))} · USDC.e "
|
|
f"{fmt(bal(USDCE))} · pUSD {fmt(bal(PUSD))}")
|
|
try:
|
|
b = client.get_balance_allowance(asset_type="COLLATERAL")
|
|
print("exchange-view COLLATERAL:", str(b)[:220])
|
|
except Exception as e:
|
|
print("balance check raised:", type(e).__name__, str(e)[:150])
|
|
print("\nDONE — success = exchange-view collateral ≈ the bankroll")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|