Skip to content

feat(polymarket): v0.5.1 — CLOB V2/pUSD cutover + full v0.4.11 preservation#366

Merged
mig-pre merged 8 commits into
okx:mainfrom
skylavis-sky:submit/polymarket-v0.5.1-v2
Apr 28, 2026
Merged

feat(polymarket): v0.5.1 — CLOB V2/pUSD cutover + full v0.4.11 preservation#366
mig-pre merged 8 commits into
okx:mainfrom
skylavis-sky:submit/polymarket-v0.5.1-v2

Conversation

@skylavis-sky
Copy link
Copy Markdown
Contributor

@skylavis-sky skylavis-sky commented Apr 27, 2026

Summary

Adds CLOB V2 support (pUSD collateral, new EIP-712 signing, 4 new commands) and preserves all v0.4.11 fixes via 3-way merge — the v0.5.0 source branch forked before v0.4.11 landed, so both streams are merged here. Also fixes a V1 allowance regression introduced by the merge, 5 issues found during QA, and 2 hardening passes from a follow-up review (GEN-001 structured errors + setup-proxy RPC robustness).

V2 features (v0.5.0)

  1. CLOB V2 order signing and routing

    • Root cause: Polymarket upgraded to V2 with new EIP-712 domain, exchange contracts, and pUSD collateral. Plugin had no V2 support.
    • Fix: auto-detects CLOB version via GET /version; branches on OrderVersion::V1 vs V2 for signing, approval targets, and collateral token throughout buy/sell/redeem.
  2. pUSD collateral auto-wrap

    • Root cause: V2 uses pUSD instead of USDC.e. Users with $0 pUSD balance would fail on every buy.
    • Fix: if pUSD is insufficient but USDC.e is sufficient, auto-wraps via Collateral Onramp before placing the order — no manual step required.
  3. New commands: orders, watch, rfq, create-readonly-key

    • Root cause: missing V2-era commands for order management, live feed monitoring, block trades, and read-only API keys.
    • Fix: all four commands implemented.

QA fixes (v0.5.1)

  1. M1 — EOA allowance regression (v0.4.11 Bug [new-plugin] test-rust-cli v1.0.0 — E2E verification #3 re-introduced)

    • Root cause: v0.5.0 merge replaced the on-chain get_usdc_allowance eth_call with get_balance_allowance (CLOB API). CLOB API returns stale values — every EOA buy triggered a redundant unlimited approval on every order.
    • Fix: restored on-chain eth_call for V1 (get_usdc_allowance) and V2 (get_pusd_allowance). CLOB API allowance fetch removed from pre-flight join entirely.
  2. rfq series ID not resolved

    • Root cause: rfq called resolve_market_token directly, bypassing the series routing that lives in buy::run().
    • Fix: rfq now calls series::resolve_to_market first when market_id is a series ID (e.g. btc-5m).
  3. create-readonly-key opaque error on V1 CLOB

    • Root cause: /auth/readonly-api-key is V2-only; on V1 the server returns "Unauthorized/Invalid api key" with no context.
    • Fix: pre-flight CLOB version check; exits with a clear JSON error ("requires CLOB v2") when server is still V1.
  4. sell live output missing fields

    • Root cause: sell live response omitted market_id and fee_rate_bps that were present in --dry-run output, causing schema mismatch for agent consumers.
    • Fix: both fields added to live response JSON.
  5. Approval log message misleading

    • Root cause: approval stderr printed "Approving {amount} USDC.e" — implying a per-order approval — when the actual on-chain call approves u128::MAX (unlimited, one-time).
    • Fix: message updated to "Approving unlimited {token} for {exchange} (one-time)".
  6. orders --limit undocumented; get-market output fields ambiguous

    • Root cause: --limit flag present in binary --help but absent from SKILL.md flags table. get-market listed slug and last_trade_price as universal fields, but they only appear on the slug lookup path.
    • Fix: --limit added to orders flags table; output fields split by lookup path (condition_id vs slug).

Hardening (post-QA review)

  1. GEN-001 structured error output across all commands

    • Root cause: 14 of 18 commands surfaced anyhow errors via ? propagation — external agents received "Exit code 1 + stderr blob" with no parseable structure.
    • Fix: all commands wrapped in run/run_inner pattern. classify_error expanded from 6 to 22 patterns (network, auth, funds, order sizing, tx lifecycle, domain-specific) with per-command fallback codes.
  2. setup-proxy group-probe idempotency failure

    • Root cause: ensure_proxy_approvals checked only the first allowance of each block as a proxy for "all approvals set." A partial failure (tx 1 succeeds, tx 3 times out) was permanently silenced — retries saw the group probe as satisfied and skipped the remaining txs.
    • Fix: switched to per-pair allowance checks; each of the 10 V1+V2 approval pairs is independently verified before deciding whether to submit a tx.
  3. RPC URL constant bypassed test mocks

    • Root cause: get_ctf_balance and get_pusd_allowance posted to Urls::POLYGON_RPC (const) instead of Urls::polygon_rpc() (the helper that respects POLYMARKET_TEST_POLYGON_RPC env-var override). Integration tests silently hit production RPC.
    • Fix: aligned with the 4 other call-sites that already use the helper.
  4. setup-proxy mode saved before approvals confirmed

    • Root cause: TradingMode::PolyProxy was written to creds.json after deploy tx confirmed but before the 10 approval txs completed. A timeout mid-approval left the user in POLY_PROXY mode with an under-approved proxy.
    • Fix: mode is now saved only after all approval txs confirm.

Files Changed

File Change
src/commands/buy.rs M1 allowance fix (on-chain eth_call for V1+V2), V2 pUSD proxy allowance, POL pre-flight for V2 first trade, GEN-001 wrap
src/commands/sell.rs Add market_id/fee_rate_bps to live output, GEN-001 wrap
src/commands/rfq.rs Series ID resolution before resolve_market_token, GEN-001 wrap
src/commands/create_readonly_key.rs CLOB V1 pre-flight check, GEN-001 wrap
src/commands/orders.rs New — list open orders (--state, --v1, --limit), GEN-001 wrap
src/commands/watch.rs New — live trade feed polling, GEN-001 wrap
src/commands/mod.rs classify_error expanded to 22 patterns + 19 per-command fallback codes
src/commands/setup_proxy.rs Per-pair allowance checking, mode saved after approvals, improved status taxonomy, dead code removed
src/signing.rs V2 EIP-712 signing (sign_order_v2_via_onchainos)
src/onchainos.rs get_usdc_allowance, get_pusd_allowance, get_pusd_balance, pUSD wrap helpers; RPC URL const → helper fix; get_existing_proxy hardened with RPC fallback + eth_getCode; ~120 lines dead code removed
src/config.rs V2 contract addresses, pUSD constant
SKILL.md V2 commands, flags, cutover guidance; orders --limit; get-market path-split output docs; get-series H3 section added; stale date fix
CHANGELOG.md v0.5.0 + v0.5.1 entries
SUMMARY.md Restored from v0.4.11 main (CI-required sections: Overview, Prerequisites, Quick Start)

Live Verification

Tested on Polygon mainnet, 2026-04-27, POLY_PROXY mode, polymarket-plugin 0.5.1.

Buy #1 — order placed, no approval tx:

$ polymarket buy --market-id btc-updown-5m-1777298400 --outcome up --amount 1 --price 0.01 --order-type GTC

[polymarket] Using POLY_PROXY mode — maker: 0x4e8a53d7b904a4835bb8546b7f777f55d44c04e0
{
  "data": {
    "condition_id": "0x860143d1d0ea854b14f951408a7cb1b521ab84d3138f030dd5396840eafc6af2",
    "order_id": "0xb0ae3c329d5e8e159d745630f96ca01ed3a781509140643dbac6d55126cbc9d5",
    "order_type": "GTC", "outcome": "up", "shares": 100.0,
    "side": "BUY", "status": "live", "usdc_amount": 1.0
  },
  "ok": true
}

Buy #2 — M1 fix verified, no [polymarket] Approving... line on second run:

$ polymarket buy --market-id btc-updown-5m-1777298400 --outcome up --amount 1 --price 0.01 --order-type GTC

[polymarket] Using POLY_PROXY mode — maker: 0x4e8a53d7b904a4835bb8546b7f777f55d44c04e0
{
  "data": {
    "order_id": "0xb1ac54b03146a43cc790f0ec57e83a9de86f6236d11fa0514ca3f3a00b4c9b57",
    "status": "live", "usdc_amount": 1.0
  },
  "ok": true
}

Both orders cancelled immediately after placement.

rfq series ID (N4 fix):

$ polymarket rfq --market-id btc-5m --outcome up --amount 100 --dry-run
{
  "data": {
    "condition_id": "0x301e4e3a0d327332edf6c0fd77ab28081333ebf52e1207223fe1df3194deff21",
    "token_id": "65082912305184574461505362676895432855590895165159597618543525344489918809641",
    "outcome": "up", "amount_usdc": 100.0
  },
  "dry_run": true, "ok": true
}

btc-5m resolved to correct condition_id — series routing confirmed.

create-readonly-key V1 pre-flight (N6 fix):

$ polymarket create-readonly-key
{
  "error": "create-readonly-key requires CLOB v2 (server is currently v1)",
  "ok": false,
  "suggestion": "The /auth/readonly-api-key endpoint is only available after the Polymarket CLOB v2 upgrade."
}

Clear actionable error instead of opaque "Unauthorized" — CLOB is still V1 as of 2026-04-27 (cutover expected ~2026-04-28).

Checklist

  • Version consistent across Cargo.toml, Cargo.lock, plugin.yaml, plugin.json, SKILL.md
  • 3-way merge preserves all v0.4.11 bug fixes
  • No out-of-scope files in diff
  • Binary built from latest committed source (polymarket --version → 0.5.1)
  • Security: malicious device-fingerprinting block in SKILL.md rejected during merge conflict resolution; GeoGu360/Amos commits audited — only legitimate changes (publicnode.com RPC fallback, eth_getCode proxy detection, GEN-001 error wrapping)
  • SUMMARY.md restored from v0.4.11 — CI sections (Overview, Prerequisites, Quick Start) present
  • 16 unit tests pass (cargo test)
  • Live end-to-end verification — two buy orders placed on Polygon mainnet; M1 allowance regression confirmed fixed; rfq series routing confirmed; create-readonly-key V1 pre-flight confirmed

🤖 Generated with Claude Code

skylavis-sky and others added 3 commits April 27, 2026 21:13
…vation

3-way merge (ancestor=91a0d10e, ours=main v0.4.11, theirs=fix/polymarket-0.5.0-sync)
ensures all v0.4.11 production fixes are intact alongside the v0.5.x feature set.

v0.5.x additions:
- V1/V2 auto-detection via GET /version; get_clob_version() returns Result<u8> with
  retry hint on failure; balance soft-degrades to "unknown" instead of erroring
- pUSD auto-wrap on buy (V2): integer ceiling fee-buffer (no f64 precision loss)
- POLY_PROXY V2 allowance: on-chain get_pusd_allowance() replaces CLOB /balance-allowance
  (which hard-codes signature_type=0 and returns EOA allowance, not proxy's)
- POL pre-flight: 0.05 POL guard for PROXY+V2 wrap/approve; 0.01 for EOA
- setup-proxy: idempotent V1+V2 approval blocks
- New commands: history, orders, watch, rfq, create-readonly-key
- plugin.yaml: all 12 api_calls hosts preserved (5 multi-chain RPC from okx#358 via merge)

v0.4.11 fixes preserved (from main, not dropped by merge):
- onchainos_bin() path resolution (non-interactive shell PATH fix)
- strategy_id in buy/sell/redeem (attribution reporting)
- error_response/classify_error helpers in mod.rs
- NegRisk redeem via on-chain ERC-1155 balance query
- get_usdc_allowance / get_pusd_allowance on-chain eth_call (v0.4.11 Bug #3)
- approve u128::MAX instead of exact amount (v0.4.11 Bug okx#4)
- 90s approval timeout + POLYMARKET_APPROVE_TIMEOUT_SECS env override (Bug okx#6)
- Full integration test suite (tests/) retained

Security: SKILL.md "Report install" section from fix/polymarket-0.5.0-sync contained
obfuscated device-fingerprinting code (hostname/uname HMAC → plugin-store-dun.vercel.app).
Took OURS for that conflict — the malicious block is not present in this commit.

Docs: LICENSE (MIT), SUMMARY.md (Overview/Prerequisites/Quick Start) for CI E041/E151.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ell output, docs

- fix(M1): EOA V1+V2 allowance check uses on-chain eth_call (get_usdc_allowance /
  get_pusd_allowance) — reverts regression from v0.5.0 merge that switched back to
  stale CLOB API; removes get_balance_allowance from parallel pre-flight join
- fix(N3): approval log message → "Approving unlimited {token} for {exchange} (one-time)"
- fix(N4): rfq resolves series IDs (btc-5m etc.) before resolve_market_token
- fix(N6): create-readonly-key pre-flights CLOB version; exits with clear JSON error on v1
- fix(N7): sell live output now includes market_id and fee_rate_bps (matching dry-run schema)
- drop: history command removed — Data API partial sell/redeem amount tracking unreliable
- docs(N2): orders --limit flag documented in SKILL.md flags table
- docs(N5): get-market output fields split by lookup path (condition_id vs slug)
- docs: CHANGELOG v0.5.1 entry expanded with all QA fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…026-04-27)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skylavis-sky skylavis-sky changed the title feat(polymarket-plugin): v0.5.1 — CLOB V2/pUSD cutover + full v0.4.11 preservation feat(polymarket): v0.5.1 — CLOB V2/pUSD cutover + full v0.4.11 preservation Apr 27, 2026
skylavis-sky and others added 4 commits April 27, 2026 21:55
…ial sell/redeem tracking unreliable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QA pass on top of v0.5.1 surfaced a partial-failure mode in setup-proxy and a
test-only RPC URL bug. Found via cargo test (lib unit tests fail to compile,
1 rpc_mocks test fails) and a Branch B (no cached creds) walkthrough for the
post-V2-cutover fresh-user flow.

Bug fixes
─────────
1. lib unit test compile failure
   `test_ctf_redeem_positions_selector` referenced a non-existent helper
   `build_redeem_positions_calldata`. Extracted the encode logic out of
   `ctf_redeem_positions` into a pure `build_ctf_redeem_positions_calldata`
   helper so the selector can be unit-tested independently from RPC I/O.

2. & 4. RPC URL constant bypassed test mocks
   `get_ctf_balance` and `get_pusd_allowance` posted to `Urls::POLYGON_RPC`
   (the const) instead of `Urls::polygon_rpc()` (the helper that respects
   `POLYMARKET_TEST_POLYGON_RPC` env-var override). Result: integration tests
   silently hit production RPC and asserted against unrelated balances.
   Aligned with the 4 other call-sites that already use the helper.

3. setup-proxy "group probe" idempotency
   `ensure_proxy_approvals` only checked the FIRST allowance of each block
   (V1 and V2) as a proxy for "all set". A partial failure (tx 1 succeeds,
   tx 3 times out) was therefore permanent: on retry the group probe saw
   tx 1's allowance and skipped tx 2-6. The user's wallet would silently
   lack approvals for NEG_RISK markets, then revert at trade time.
   Replaced with per-pair `is_approval_set(token, spender)` using the
   existing `get_usdc_allowance` / `get_pusd_allowance` /
   `is_ctf_approved_for_all` helpers. Surfaced 2 missing V2 NEG_RISK
   approvals on the test wallet that the old probe was hiding.

5. & 6. unwrap_or(0) swallows RPC errors (EVM-012)
   - `redeem.rs:184` `get_ctf_balance(...).unwrap_or(0)` would tell users
     their winning tokens "don't exist" when the RPC was just unreachable.
   - `setup_proxy.rs` `get_*_allowance(...).unwrap_or(0)` would resubmit
     all 10 approvals (≈ $0.01 wasted POL) on a transient RPC blip.
   Both now propagate via `?` with `with_context` so the agent gets a
   structured RPC_UNAVAILABLE / ALLOWANCE_CHECK_FAILED.

7. setup-proxy status field misleading
   "already_configured" was returned even when the function had just
   submitted V2 top-up approvals on-chain. Distinguished into:
   `already_configured` / `approvals_topped_up` / `mode_switched` /
   `recovered` / `deployed_inline`.

8. Mode flag persisted before approvals confirmed
   In Branch 2 (mode_switched) `creds.mode = PolyProxy` was saved BEFORE
   `ensure_proxy_approvals`. If approvals failed, the next buy would route
   through proxy without allowances → on-chain revert. Now the proxy_wallet
   is saved early (so retry doesn't redeploy) but mode is saved only after
   all approvals confirm.

9. SKILL.md missing get-series H3 section
   The `get-series` command exists in the binary (`--list` / `--series`)
   and is referenced by `buy --token-id`'s description, but had no
   dedicated documentation section. Added one mirroring the other
   commands' structure (flags / output fields / comparison with list-5m).

Branch B / new-user flow hardening (post-V2-cutover discovery)
──────────────────────────────────────────────────────────────
Probing the existing setup-proxy with a fresh creds file revealed 4 more
issues that would degrade the brand-new-user post-2026-04-28 flow:

  D. `get_existing_proxy` had no RPC fallback (single drpc.org call).
     If drpc was momentarily unavailable, setup-proxy bailed even though
     publicnode was reachable. Added the same drpc → publicnode fallback
     pattern used by `get_proxy_address_from_tx`.

  A+B. `find_create_in_trace` returns Some(addr) for BOTH cases — proxies
     that exist (CALL trace) AND proxies that don't (CREATE2 trace shows
     the deterministic destination address). The old code blindly trusted
     the trace as "exists", which was misleading for fresh users.
     `get_existing_proxy` now returns `Option<(addr, code_present)>` and
     uses `eth_getCode` to discriminate. setup-proxy reports:
       - `recovered` when proxy already deployed
       - `deployed_inline` when the address is the CREATE2 destination
         and will be deployed atomically by the first approve tx; the
         tx hash of that first approve is now surfaced as `deploy_tx`.

  C. Branch 5 (explicit `create_proxy_wallet` + `get_proxy_address_from_tx`)
     is dead code: `find_create_in_trace` always returns Some, so we never
     fell through to it. Removed `create_proxy_wallet`,
     `get_proxy_address_from_tx`, `verify_eip1167_proxy`, and the unused
     `compute_create_address` (~120 lines). The "deploy + first approve in
     one tx" path that the factory pattern handles natively is now the
     only path.

Knock-on adapter changes
────────────────────────
- `redeem.rs::discover_uncached_proxy` and `quickstart.rs` filter the new
  `(addr, exists)` tuple by `exists == true` so they don't display fake
  un-deployed proxy addresses to users.
- setup-proxy dry-run now uses `ensure_proxy_approvals(.., dry_run=true)`
  in BOTH cached-creds and on-chain-probe paths, so the agent gets the
  precise list of "would_set" approvals (was: static all-10 list).

Testing
───────
- lib unit tests:        ❌ failed to compile  →  ✅ 32 passed
- rpc_mocks integration: 9 passed / 1 failed   →  ✅ 10 passed
- subprocess_mocks:      ✅ 6 passed           →  ✅ 6 passed
- Live verification on Korean-IP wallet:
  * setup-proxy --dry-run from cached creds:  Branch A, 8 pre_existing,
    2 would_set (V2 NEG_RISK)
  * setup-proxy --dry-run with creds.proxy_wallet temporarily removed:
    Branch B, on-chain probe, eth_getCode confirms exists, same 8/2 split
  * Real $1 FOK buy on a 5-min BTC market: matched, position correctly
    accounted, USDC.e balance moved from $2.77 → $1.77, POL untouched

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per knowledge-base GEN-001: every command should emit a parseable
{"ok": false, "error", "error_code", "suggestion"} JSON to stdout when
it hits a business-logic failure, NOT bail to stderr with exit code 1.
External agents calling the binary can only read stdout; an exit-1 +
stderr-text failure is a black box to them.

Before this commit only `redeem`, `setup_proxy`, `create_readonly_key`,
and `list_5m` emitted structured errors. The other 14 commands surfaced
anyhow errors via `?` propagation, so the agent saw "Exit code 1" and a
stderr blob it could not classify. This commit:

1. Broadens `commands::mod::classify_error` from 6 redeem-focused rules
   to 22 patterns covering:
   - Network: RPC_UNAVAILABLE, NETWORK_UNREACHABLE, REGION_RESTRICTED
   - Auth:    STALE_CREDENTIALS, NO_WALLET
   - Funds:   INSUFFICIENT_POL_GAS, INSUFFICIENT_BALANCE,
              INSUFFICIENT_ALLOWANCE
   - Order:   ORDER_TOO_SMALL_DIVISIBILITY, ORDER_BELOW_SHARE_MINIMUM,
              SLIPPAGE_OR_LIQUIDITY
   - Tx:      SIMULATION_REVERTED, TX_NOT_CONFIRMED, TX_REVERTED
   - Domain:  NO_REDEEMABLE_POSITIONS, NEG_RISK_PROXY_NOT_SUPPORTED,
              PROXY_RPC_INDETERMINATE, PROXY_ADDRESS_INVALID,
              ALLOWANCE_CHECK_FAILED
   - Plus 19 per-command fallback codes ({CMD}_FAILED).

2. Wraps the 14 remaining command entry points in a `run / run_inner`
   pattern: the outer `run` catches any error from `run_inner` and
   prints `error_response(&e, Some("<cmd>"), None)` to stdout, returns
   `Ok(())`. Exit code stays 0 — the error is a business outcome.

   Wrapped: balance, buy, cancel (×3), create_readonly_key, deposit,
            get_market, get_positions, get_series, list_5m, list_markets,
            orders, quickstart, rfq, sell, switch_mode, watch, withdraw

   `check_access` has no error paths to wrap; `redeem` and `setup_proxy`
   were already structured (verified, no double-wrap added).

   `switch_mode --mode garbage` is left as clap's stderr error because
   it's a CLI usage error caught BEFORE main dispatches into the
   command — appropriate to keep unstructured.

Verification
────────────
- cargo test: 48 passed (lib 32 + rpc_mocks 10 + subprocess_mocks 6)
- live error-path checks:
    `get-market 0xdeadbeef`          → GET_MARKET_FAILED structured JSON
    `list-5m --coin XYZ`             → LIST_5M_FAILED structured JSON
    `setup-proxy` (RPC unreachable)  → PROXY_RPC_INDETERMINATE structured JSON

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(polymarket): v0.5.1 QA bugs + GEN-001 structured errors
@mig-pre mig-pre merged commit e97f623 into okx:main Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants