feat(polymarket): v0.5.1 — CLOB V2/pUSD cutover + full v0.4.11 preservation#366
Merged
Merged
Conversation
…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>
…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>
4 tasks
fix(polymarket): v0.5.1 QA bugs + GEN-001 structured errors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
CLOB V2 order signing and routing
GET /version; branches onOrderVersion::V1vsV2for signing, approval targets, and collateral token throughout buy/sell/redeem.pUSD collateral auto-wrap
New commands:
orders,watch,rfq,create-readonly-keyQA fixes (v0.5.1)
M1 — EOA allowance regression (v0.4.11 Bug [new-plugin] test-rust-cli v1.0.0 — E2E verification #3 re-introduced)
get_usdc_allowanceeth_call withget_balance_allowance(CLOB API). CLOB API returns stale values — every EOA buy triggered a redundant unlimited approval on every order.get_usdc_allowance) and V2 (get_pusd_allowance). CLOB API allowance fetch removed from pre-flight join entirely.rfq series ID not resolved
rfqcalledresolve_market_tokendirectly, bypassing the series routing that lives inbuy::run().series::resolve_to_marketfirst when market_id is a series ID (e.g.btc-5m).create-readonly-key opaque error on V1 CLOB
/auth/readonly-api-keyis V2-only; on V1 the server returns "Unauthorized/Invalid api key" with no context."requires CLOB v2") when server is still V1.sell live output missing fields
selllive response omittedmarket_idandfee_rate_bpsthat were present in--dry-runoutput, causing schema mismatch for agent consumers.Approval log message misleading
"Approving {amount} USDC.e"— implying a per-order approval — when the actual on-chain call approvesu128::MAX(unlimited, one-time)."Approving unlimited {token} for {exchange} (one-time)".orders --limitundocumented;get-marketoutput fields ambiguous--limitflag present in binary--helpbut absent from SKILL.md flags table.get-marketlistedslugandlast_trade_priceas universal fields, but they only appear on the slug lookup path.--limitadded toordersflags table; output fields split by lookup path (condition_id vs slug).Hardening (post-QA review)
GEN-001 structured error output across all commands
?propagation — external agents received "Exit code 1 + stderr blob" with no parseable structure.run/run_innerpattern.classify_errorexpanded from 6 to 22 patterns (network, auth, funds, order sizing, tx lifecycle, domain-specific) with per-command fallback codes.setup-proxy group-probe idempotency failure
ensure_proxy_approvalschecked 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.RPC URL constant bypassed test mocks
get_ctf_balanceandget_pusd_allowanceposted toUrls::POLYGON_RPC(const) instead ofUrls::polygon_rpc()(the helper that respectsPOLYMARKET_TEST_POLYGON_RPCenv-var override). Integration tests silently hit production RPC.setup-proxy mode saved before approvals confirmed
TradingMode::PolyProxywas 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.Files Changed
src/commands/buy.rssrc/commands/sell.rsmarket_id/fee_rate_bpsto live output, GEN-001 wrapsrc/commands/rfq.rsresolve_market_token, GEN-001 wrapsrc/commands/create_readonly_key.rssrc/commands/orders.rs--state,--v1,--limit), GEN-001 wrapsrc/commands/watch.rssrc/commands/mod.rsclassify_errorexpanded to 22 patterns + 19 per-command fallback codessrc/commands/setup_proxy.rssrc/signing.rssign_order_v2_via_onchainos)src/onchainos.rsget_usdc_allowance,get_pusd_allowance,get_pusd_balance, pUSD wrap helpers; RPC URL const → helper fix;get_existing_proxyhardened with RPC fallback +eth_getCode; ~120 lines dead code removedsrc/config.rsSKILL.mdorders --limit;get-marketpath-split output docs;get-seriesH3 section added; stale date fixCHANGELOG.mdSUMMARY.mdLive Verification
Tested on Polygon mainnet, 2026-04-27, POLY_PROXY mode, polymarket-plugin 0.5.1.
Buy #1 — order placed, no approval tx:
Buy #2 — M1 fix verified, no
[polymarket] Approving...line on second run:Both orders cancelled immediately after placement.
rfq series ID (N4 fix):
btc-5mresolved to correct condition_id — series routing confirmed.create-readonly-key V1 pre-flight (N6 fix):
Clear actionable error instead of opaque "Unauthorized" — CLOB is still V1 as of 2026-04-27 (cutover expected ~2026-04-28).
Checklist
polymarket --version→ 0.5.1)cargo test)🤖 Generated with Claude Code