Commit d9f38f2
authored
feat(predict): add Polymarket CLOB v2 support cp-7.73.1 (#29076)
## **Description**
This PR implements Polymarket CLOB v2 support for MetaMask Predict (7.74
target), introducing a clean coexistence architecture that keeps v1
working while v2 is toggled on via `predictClobV2`. During the temporary
CLOB host migration window, a companion rollout flag
(`predictClobV2UseLegacyClobHost`) can force v2 traffic onto the legacy
host for internal RC testing without turning v2 on for everyone by
default.
The implementation is designed with deletion in mind — after the v1/v2
coexistence window, v1 can be cleanly removed with minimal surgery. The
work follows the "data-first protocol definition" architecture from the
internal plan: a private `protocol/` module owns all version-specific
differences and is resolved once at the provider entry point.
Lower-level helpers never read feature flags directly.
**Key design decisions:**
- Primary remote flag: `predictClobV2` (default `false`) enables the v2
protocol
- Temporary rollout flag: `predictClobV2UseLegacyClobHost` forces v2 to
use `https://clob-v2.polymarket.com` during the migration window
- Canonical/default v2 host remains `https://clob.polymarket.com`
- Host selection is resolved once into `protocol.transport.clobBaseUrl`;
lower-level helpers never read raw flags
- v2 API key caching is host-aware to avoid reusing credentials across
host changes
- v2 `getBalance()` always returns `Safe USDC.e + Safe pUSD` — no
branching on upgrade state
- Trade path is **relayer-only**, **Permit2-only** fee collection on
pUSD, always runs preflight before submission
- Canonical v2 allowance requirement set defined once; inspector +
compiler are small pure modules
- Deposit/withdraw use **release-time code choices** for preferred vs
fallback variant — not runtime flags
- Wrap: always wraps **entire current Safe USDC.e balance** (never
`MaxUint256`) when maintenance tx is already being emitted
- Unwrap: always unwraps **exact deficit** needed — no over-unwrapping
---
## **Commits**
This PR is now structured as 12 focused commits for easier review:
### 1. `feat(predict): add CLOB v2 feature flag plumbing`
Wires the `predictClobV2` boolean through the Predict feature-flag
infrastructure:
- Adds `predictClobV2Enabled` to `PredictFeatureFlags` interface
- Adds `selectPredictClobV2EnabledFlag` selector
- Extends `resolvePredictFeatureFlags` to resolve the new flag
- Refactors `resolveVersionGatedBooleanFlag` helper to reduce
duplication across the existing flag resolution logic
- Adds selector and resolver tests
### 2. `feat(predict): add CLOB v2 protocol and preflight foundation`
Introduces the two new private modules that everything else builds on:
**`protocol/`** — data-first protocol definitions:
- `definitions.ts`: v1 and v2 `PolymarketProtocolDefinition` objects;
protocol resolution; builder code env config
(`MM_PREDICT_BUILDER_CODE`); deposit/withdraw execution mode types
- `orderCodec.ts`: v2-aware order build, EIP-712 typed data, and relayer
payload serialization
- `transport.ts`: shared CLOB transport helpers parameterized by
protocol endpoint; collapses v1/v2 endpoint differences
**`preflight/`** — private readiness inspection and Safe plan
construction:
- `v2AllowanceRequirements.ts`: canonical declarative list of all 10 v2
allowance requirements (7 ERC-20, 3 ERC-1155)
- `inspectMissingRequirements.ts`: reads on-chain state and returns the
subset of requirements not yet satisfied
- `compileRequirementTransactions.ts`: compiles missing requirements
into `SafeTransaction[]`
- `core.ts`: shared raw-fact readers, wrap/unwrap builders, and signed
Safe execution helpers
All modules covered by unit tests.
### 3. `feat(predict): add CLOB v2 buy and sell flow`
Implements the full v2 trade path inside `PolymarketProvider`:
- `preflight/trade.ts`: `planTradePreflight` + `buildTradeAllowancesTx`
— inspects missing v2 allowances and Safe USDC.e balance; compiles
optional maintenance tx (allowances-only / wrap-only / allowances+wrap /
none)
- `PolymarketProvider`: `placeOrder` now resolves the protocol once,
runs preflight under v2, builds the optional `allowancesTx`, and uses
the protocol's `orderCodec` for order construction, EIP-712 signing, and
relayer payload
- `utils.ts`: adds `encodeWrapUsdceTransaction` and
`encodeUnwrapTransaction` helpers (+ tests)
- v2 preview keeps `feeRateBps = '0'` until the upstream fee endpoint is
confirmed
### 4. `feat(predict): add CLOB v2 deposit flow`
Adds the v2 deposit maintenance planner and wires it into
`PolymarketProvider.prepareDeposit`:
- `preflight/deposit.ts`: `planDepositMaintenance` +
`compileDepositMaintenanceTransactions` — inspects missing v2 allowances
and **pre-existing** Safe USDC.e balance (does not incorporate the
just-entered deposit amount); emits optional maintenance tx
- `PolymarketProvider.prepareDeposit`: under v2, resolves protocol, runs
the maintenance planner, and attaches the optional maintenance tx to the
deposit plan — the 3-step shape (optional deploy → transfer funding
asset → optional maintenance tx) is preserved
- Currently wired to the `usdce-transfer` fallback mode; flip
`depositMode` in the protocol definition to switch to pUSD-native when
that dependency lands
### 5. `feat(predict): add CLOB v2 withdraw flow`
Adds the v2 withdraw planner and wires both the `prepareWithdraw` /
`signWithdraw` contract:
- `preflight/withdraw.ts`: `planWithdraw` — reads missing v2 allowances
and Safe USDC.e balance; computes the exact USDC.e deficit; compiles the
final MultiSend (allowance repair → exact-deficit unwrap-to-Safe →
USDC.e transfer to EOA)
- `PolymarketProvider.signWithdraw`: under v2, parses the requested
amount from the original template calldata, calls `planWithdraw`, and
returns the user-requested amount (not any larger intermediate amount)
- `safe/utils.ts`: adds `parseTransactionCalldata` helper to extract the
withdraw amount from the stored template
- Currently wired to the `usdce-deficit-unwrap` fallback mode; flip
`withdrawMode` in the protocol definition to switch to pUSD-native
### 6. `feat(predict): add CLOB v2 claim flow`
Adds the v2 claim planner and wires it into
`PolymarketProvider.claimWinnings`:
- `preflight/claim.ts`: `planClaim` — reads EOA USDC.e directly (not
Safe balances or provider `getBalance()`); computes `gasStationDeficit =
max(0, MIN_GAS_STATION_USDCE_BALANCE - eoaUsdceBalance)`; proactively
wraps the **entire current Safe USDC.e balance**; compiles the MultiSend
in the required order: (1) missing allowance/operator repair, (2)
wrap-all current Safe USDC.e, (3) claim subcalls, (4) optional
exact-deficit unwrap to EOA
- `PolymarketProvider.claimWinnings`: under v2, resolves protocol and
delegates to `planClaim`; uses protocol-owned claim targets (pUSD as
collateral) rather than per-position collateral metadata
### 7. `test(predict): add CLOB v2 integration coverage`
Adds integration-level tests and reorganizes the provider test suite:
- `preflight/workflows.test.ts`: end-to-end workflow planner tests
covering all four preflight paths (trade, deposit, claim, withdraw) with
concrete on-chain mock scenarios — verifies MultiSend ordering invariant
(approvals first, then wraps/claims/transfers)
- `PolymarketProvider.test.ts`: reorganized into `v1` / `v2` describe
blocks; covers protocol routing (flag off → v1, flag on → v2), v2
`getBalance()` aggregation, all four trade preflight outcomes (none /
allowances-only / wrap-only / allowances+wrap), deposit/withdraw
preferred vs fallback shape, and claim ordering
### 8. `fix: preserve EIP-712 field order in buildProtocolUnsignedOrder`
Fixes the v2 order-signing codec so the generated typed data preserves
the expected field ordering for EIP-712 signing.
### 9. `codex: address PR review feedback (#29076)`
Addresses review feedback in the allowance inspector path:
- forwards `requirement.tokenAddress` into the ERC-1155 operator
approval read instead of relying on a hardcoded contract address
- adds assertions proving the token address is propagated correctly
through the read path
### 10. `refactor(predict): tighten CLOB v2 preflight workflow seams`
Addresses follow-up review feedback without changing behavior:
- makes lower-level transaction compilers private where possible
- keeps `getClaimRequirements` exported for focused pure testing
- tightens `preflight/workflows.test.ts` to assert through planner entry
points instead of private helpers
- renames claim gas-top-up terminology for clarity
(`MIN_GAS_STATION_USDCE_BALANCE_RAW`, `gasStationDeficit`) while keeping
`eoaUsdceBalance` unchanged
### 11. `feat(predict): add configurable CLOB v2 host override`
Adds temporary host-migration plumbing while keeping canonical host
resolution centralized:
- introduces canonical and legacy CLOB host constants
- resolves the selected host into `protocol.transport.clobBaseUrl`
- threads the resolved host through API key creation, order-book reads,
and preview flow
- isolates API key cache entries by protocol + host + address
### 12. `refactor(predict): gate legacy CLOB v2 host with boolean flag`
Simplifies the rollout shape for internal RC testing:
- replaces the nested raw `predictClobV2.clobBaseUrl` rollout control
with a second version-gated boolean flag:
`predictClobV2UseLegacyClobHost`
- keeps the lower-level host plumbing from commit 11 intact
- allows internal RC builds to enable the legacy host remotely while
internal users locally toggle `predictClobV2`
---
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/PRED-817
## **Manual testing steps**
```gherkin
Feature: Polymarket CLOB v2 trading
Scenario: user trades with CLOB v2 enabled
Given the predictClobV2 remote flag is enabled
And the user has an active Polymarket Safe
When user places a buy or sell order
Then the provider runs preflight and submits any needed allowance/wrap tx first
And the order is submitted via the relayer with X-Clob-Version: 2
Scenario: internal RC uses the legacy v2 CLOB host
Given the predictClobV2UseLegacyClobHost remote flag is enabled
And predictClobV2 is enabled via local override
When user places a buy or sell order
Then v2 CLOB requests use https://clob-v2.polymarket.com
And API keys are cached separately from canonical-host credentials
Scenario: user deposits with CLOB v2 enabled
Given the predictClobV2 remote flag is enabled
When user initiates a deposit
Then the deposit plan includes an optional maintenance tx (allowance repair + wrap pre-existing USDC.e)
And the newly deposited amount is not included in the wrap
Scenario: user withdraws with CLOB v2 enabled
Given the predictClobV2 remote flag is enabled
And the user requests a specific withdrawal amount
When user confirms the withdraw
Then only the exact USDC.e deficit is unwrapped (not all pUSD)
And the reported amount matches the user-requested amount
Scenario: user claims winnings with CLOB v2 enabled
Given the predictClobV2 remote flag is enabled
And the user has winning positions
When user claims
Then all missing v2 allowances are repaired first
And the entire pre-existing Safe USDC.e is wrapped
And claim subcalls execute after
And only the exact EOA gas-top-up deficit is unwrapped to USDC.e
Scenario: CLOB v2 flag is disabled (v1 behavior)
Given the predictClobV2 remote flag is disabled
When user interacts with any predict flow
Then v1 protocol is used unchanged
```
## **Screenshots/Recordings**
https://www.loom.com/share/155120bd46c44723a8b838172b4fd45b
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **High Risk**
> High risk because it changes Polymarket trade/claim/deposit/withdraw
transaction construction and relayer submission paths, including new
Safe preflight logic and EIP-712 signing for a new protocol version.
>
> **Overview**
> Adds **Polymarket CLOB v2** support behind new Predict feature flags
(`predictClobV2Enabled` plus optional legacy host override), with
host-aware API key caching and endpoint selection.
>
> Refactors `PolymarketProvider` to resolve a protocol definition once
and route **preview + order submission** through a new `protocol/`
module (v2 uses new order schema/typed data, zero preview fee rate, and
a relayer request header for v2 routing).
>
> Introduces a `preflight/` layer that inspects on-chain
allowance/approval requirements and builds signed Safe executions for
**trade allowances**, **deposit maintenance**, **claim**, and
**withdraw** (including wrap/unwrap flows and balance aggregation across
USDC.e + pUSD for v2). Adds extensive unit/integration tests and updates
Safe utilities (raw USDC amount decoding, token-address-aware approvals,
Permit2 token selection) plus CI env wiring for
`MM_PREDICT_BUILDER_CODE`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5df04d5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 63ef30c commit d9f38f2
33 files changed
Lines changed: 4125 additions & 409 deletions
File tree
- app/components/UI/Predict
- providers/polymarket
- preflight
- protocol
- safe
- selectors/featureFlags
- types
- utils
Lines changed: 409 additions & 2 deletions
Large diffs are not rendered by default.
Lines changed: 562 additions & 324 deletions
Large diffs are not rendered by default.
Lines changed: 25 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
7 | 10 | | |
8 | 11 | | |
9 | 12 | | |
| |||
81 | 84 | | |
82 | 85 | | |
83 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
84 | 109 | | |
85 | 110 | | |
86 | 111 | | |
| |||
Lines changed: 220 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
Lines changed: 39 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
Lines changed: 32 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
0 commit comments