Skip to content

feat(transaction-pay-controller): add PolymarketBridgeStrategy for deposit-wallet predictWithdraw#8754

Draft
matthewwalsh0 wants to merge 14 commits into
mainfrom
feat/polymarket-bridge-withdraw-strategy
Draft

feat(transaction-pay-controller): add PolymarketBridgeStrategy for deposit-wallet predictWithdraw#8754
matthewwalsh0 wants to merge 14 commits into
mainfrom
feat/polymarket-bridge-withdraw-strategy

Conversation

@matthewwalsh0
Copy link
Copy Markdown
Member

@matthewwalsh0 matthewwalsh0 commented May 11, 2026

Explanation

Polymarket users whose USD balance lives in a deposit wallet (a deterministic per-user batch contract on Polygon) currently have no way to withdraw cross-chain via Pay. The deposit wallet is owned by the user's EOA but is not the EOA itself, so the existing Relay strategy — which submits source-chain transactions from the EOA — cannot move pUSD out of it without a manual transfer first.

This PR adds a PolymarketBridgeStrategy that handles predictWithdraw for deposit-wallet users by orchestrating Polymarket's Bridge and Relayer APIs end-to-end:

  • Computes the deposit-wallet address deterministically from the EOA owner.
  • Calls the Bridge API to mint a one-shot destination-chain deposit address for the requested withdrawal.
  • Builds a deposit-wallet Batch (approve + transfer) that moves pUSD from the deposit wallet to the bridge deposit address.
  • Signs the batch with the EOA owner and submits it through the MetaMask Polymarket relayer proxy.
  • Polls the bridge until funds are delivered, surfacing the on-chain hashes via sourceHash and the strategy return value.

Configuration model

The strategy talks to the relayer through the existing MetaMask Polymarket relayer proxy contract — a single POST to /transaction whose body is { path, method, body|query }. The proxy authenticates the request and forwards it to the underlying Polymarket relayer. As a result, the controller carries no relayer credentials.

The proxy base URL defaults to https://predict.api.cx.metamask.io and can be overridden by the polymarketRelayerUrl field on the confirmations_pay remote feature flag, so the proxy host can be swapped without a controller release.

Strategy routing

A new isPolymarketDepositWallet flag is added to TransactionConfig, mirroring the existing isHyperliquidSource flag. When set via setTransactionConfig, the controller routes the transaction's quotes and execution to PolymarketBridgeStrategy regardless of the host's normal strategy ordering. The flag also propagates into QuoteRequest and exempts the affected source-amount entries from the same-token-same-chain dedupe, since the strategy renormalizes the source from the EOA to the deposit-wallet contract.

Non-obvious decisions

  • Why an envelope instead of direct relayer URLs. Mobile already routes every Polymarket relayer call through the same proxy contract, which injects credentials server-side. Reusing the same envelope shape avoids divergent client contracts and keeps the controller credential-free.
  • Why a feature-flagged URL when mobile hardcodes its proxy URL. The controller may need to point at a different proxy host (preprod, regional, migrated) without a release.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

…rategy

Adds PolymarketBridgeStrategy for predictWithdraw transactions of deposit-wallet
users. Routes withdrawals through Polymarket's Bridge API (quote + one-shot
deposit address) and Relayer API (signed WALLET batch dispatch). Gated behind
payPolymarketBridgeWithdrawEnabled feature flag. Legacy Safe users continue to
use the Relay strategy.

User-facing flow: one EIP-712 signature, no on-chain transaction from the
user's EOA, zero gas paid (Polymarket's relayer covers it), ~25s end-to-end.

Mobile-side adoption ships in feat/polymarket-bridge-withdraw-adopt.
- Fix relayer auth: use request.from for RELAYER_API_KEY_ADDRESS header
- Remove address from RelayerApiKeyCredentials (derived from from)
- Fix nonce query: use EOA address instead of deposit wallet address
- Add bridge status polling (pollUntilBridgeComplete) for target-side tracking
- Set metamaskPay.sourceHash and isIntentComplete in execute flow
- Add deposit-wallet address computation in core (computeDepositWalletAddress)
- Add DEPOSIT_WALLET_IMPLEMENTATION_POLYGON constant
…ath to Relay strategy

Add isPolymarketDepositWallet config flag to route Relay deposit
transactions through the Polymarket gasless relayer. When set, the
Relay strategy submits approve+deposit calls as a deposit-wallet
Batch via the Polymarket relayer instead of TransactionController
or Relay /execute.

- Add isPolymarketDepositWallet to TransactionConfig, TransactionData, QuoteRequest
- Propagate flag through quotes.ts and source-amounts.ts
- Add getPolymarketBridgeOptions getter for cross-strategy credential access
- Create polymarket-bridge/index.ts barrel for primitive reuse
- Create relay/submit-polymarket-relayer.ts orchestration function
- Add third branch in executeSingleQuote with mutual-exclusivity guard
- Suppress originGasOverhead when isPolymarketDepositWallet is set
…osit-wallet Relay path

The Polymarket gasless relayer pays source-chain gas, so the user
owes nothing. Extend the existing Hyperliquid zero-fee guard in
calculateSourceNetworkCost to also cover isPolymarketDepositWallet.
…ission path to Relay strategy"

This reverts commit 2afa11a.
… PolymarketBridgeStrategy

The strategy now talks to the Polymarket relayer through a URL that is
read from the remote feature flag at request time. Authentication is
handled out-of-band by the configured endpoint, so the controller no
longer accepts or stores any relayer credentials.

- Add getPolymarketRelayerUrl feature-flag accessor with prod default
- Drop polymarketBridgeOptions controller constructor option
- Drop PolymarketBridgeStrategyOptions, *Input, RelayerCredentials, and
  related auth types
- Drop HMAC / API-key header construction from PolymarketRelayerApi;
  it now takes a base URL and nothing else
- Pin PolymarketBridgeApi to the prod URL (preprod URL no longer used)
- Drop preprod URL constants
… flag + envelope-based relayer transport

Convert PolymarketRelayerApi to the MetaMask Polymarket relayer-proxy
envelope contract: a single POST to /transaction with a
{ path, method, body|query } body. The proxy authenticates and forwards
to the underlying Polymarket relayer, so the controller carries no
credentials.

Add isPolymarketDepositWallet on TransactionConfig (mirrors the
isHyperliquidSource pattern). When set, the controller routes the
transaction to PolymarketBridgeStrategy and the post-quote source-amount
calculation no longer dedupes same-token-same-chain (the strategy
renormalizes the source to the on-chain deposit wallet).

- Default proxy URL constant POLYMARKET_RELAYER_PROXY_URL_PROD
- Drop unused raw-relayer URL constants
- Propagate flag through quotes.ts and source-amounts.ts
@matthewwalsh0 matthewwalsh0 changed the title feat(transaction-pay-controller): add Polymarket deposit-wallet withdraw + relayer-backed Relay path feat(transaction-pay-controller): add PolymarketBridgeStrategy for deposit-wallet predictWithdraw May 11, 2026
…rce and target amounts

Calculate provider fees from the bridge quote's estFeeBreakdown
(gasUsd + appFeeUsd + swapImpactUsd) and convert to fiat via the
source-token fiat rate.

Populate sourceAmount and targetAmount with fiat and USD values from
token rates so the confirmation surfaces meaningful amounts instead of
zeros.
…plete at execute start

The 7702 batch wrapper transaction created by addTransactionBatch is
never broadcast on-chain — PolymarketBridgeStrategy intercepts the
publish hook and submits an off-chain envelope to the relayer, which in
turn broadcasts a separate transaction signed by the relayer.

Without isIntentComplete set, PendingTransactionTracker.#checkTransaction
runs against the wrapper, finds no hash, and fails the transaction with
NoTxHashError — even though the user's pUSD has been bridged successfully.

Set isIntentComplete at the start of execute() so the wrapper is treated
as confirmed by the tracker. Drop the post-submit pollUntilBridgeComplete
because target-side bridge completion is independent of source-side
success and does not gate the user's wrapper transaction status.
…t bridge withdraw

Behind a hardcoded USE_RELAY_BRIDGE flag, the Polymarket bridge strategy now
fetches a Relay quote (pUSD on Polygon -> target chain/token) at quote time
and executes in two steps:

1. Transfer pUSD from the deposit wallet to the user EOA via the existing
   Polymarket relayer proxy (single ERC-20 transfer batch).
2. Submit the stored Relay quote from the user EOA via submitRelayQuotes,
   which polls Relay status and returns the destination tx hash.

Synthetic QuoteRequest sets isPostQuote: true so submitRelayQuotes skips its
own source-balance validation (the EOA is funded by Step 1 and the chain
RPC may lag); txParams.to is stripped on the relayed transaction so the
post-quote prepend stays disabled.

Flag toggles to false to fall back to the original Polymarket bridge flow.
…y deposit address with USDC.e sweep

The deposit wallet now unwraps pUSD directly into USDC.e at Relay's
one-shot deposit address in a single relayer-broadcast batch (approve +
unwrap). After Relay settles, the deposit wallet's USDC.e balance is read
live from RPC and any remainder is wrapped back into pUSD via the
CollateralOnramp, preserving the deposit-wallet pUSD invariant on partial
fills, refunds, or solver failures.

Implementation notes:
- New flags USE_RELAY_DEPOSIT_ADDRESS + FORCE_SKIP_RELAY_POLL gate the new
  flow and the in-flight test shortcut.
- Relay status poll now treats 'refund' as in-flight (the refund tx has
  not yet confirmed); only 'refunded'/'failure'/'success' are terminal.
- submitWithBusyRetry retries the wrap submission when the Polymarket
  relayer reports the deposit wallet still has an active action; the
  retry matches against the message text, not a typed error class, so it
  catches both proxy-wrapped and direct relayer errors.
- relayer-api now reads the JSON body on non-OK HTTP responses and
  surfaces the relayer's actual 'error'/'message' field, so callers can
  branch on the real reason instead of a generic status-code wrapper.
…uotes + submit modules

Reorganises strategy/polymarket-bridge/ to match the layout of the other
strategies (across, relay) and removes the dead code from the prior
experimental phases:

- PolymarketBridgeStrategy class renamed to PolymarketStrategy, file
  renamed to match. Class shrinks from 945 to 43 lines and delegates to
  the two new modules.
- New polymarket-quotes.ts owns Relay quote fetch + TransactionPayQuote
  normalisation.
- New polymarket-submit.ts owns the deposit-wallet batched approve +
  unwrap to Relay's deposit address, Relay status polling, the USDC.e
  sweep (approve + wrap back to pUSD), the deposit-wallet batch transport
  with EIP-712 signing + relayer submission, and the 'wallet busy' retry.
- New polymarket-calldata.ts owns the ABI encoders for approve, unwrap,
  wrap, and the ERC-20 transfer-recipient extractor.
- Removed bridge-api.ts (no longer needed - Polymarket's own bridge API
  is no longer called).
- Removed withdraw.ts (its surface moved into polymarket-submit.ts as
  submitDepositWalletBatch with retry collapsed into the same function).
- Removed dead flags USE_RELAY_BRIDGE, USE_RELAY_DEPOSIT_ADDRESS,
  FORCE_SKIP_RELAY_POLL and the legacy execute branches they gated.
- PolymarketBridgeQuote slimmed to { relayQuote } - the wrapper exists
  only so the strategy can carry a typed quote through the controller.
…withdraw into Relay strategy

The Polymarket withdraw flow is now a flavour of the Relay strategy,
matching the HyperLiquid precedent. The standalone PolymarketStrategy
class and its TransactionPayStrategy enum entry are removed in favour of
an isPolymarketDepositWallet branch in the existing Relay quote and
submit pipelines.

Why: 90% of the previous strategy duplicated work the Relay strategy
already does (quote fetch, response normalisation, status polling,
destination tx hash extraction). The only Polymarket-specific parts are
the deposit-wallet transport (EIP-712 batch via the Polymarket relayer
proxy), the pUSD <-> USDC.e conversion (unwrap pre-deposit, wrap-back
sweep on completion), and the useDepositAddress=true Relay request shape.

Structure:
- strategy/relay/polymarket/ holds all Polymarket-specific code:
  - withdraw.ts orchestrates the approve+unwrap source-leg batch, the
    USDC.e sweep helper run after Relay completion, and the deposit
    wallet batch transport with wallet-busy retry.
  - quotes.ts builds the USDC.e + useDepositAddress=true Relay quote
    body.
  - calldata.ts, constants.ts, deposit-wallet.ts, relayer-api.ts,
    types.ts, wallet-batch-typed-data.ts host the supporting primitives.
- relay-quotes.ts branches on isPolymarketDepositWallet when building
  the Relay quote body and skips the contract-call embedding step
  (Polymarket sends a bare token transfer to the deposit address).
- relay-submit.ts branches on isPolymarketDepositWallet to call
  submitPolymarketDepositWalletWithdraw, then runs waitForRelayCompletion
  (tolerating refund-failure to allow sweep recovery), then
  sweepPolymarketDepositWalletUsdce.
- TransactionPayController short-circuit for isPolymarketDepositWallet
  now returns Relay instead of the removed PolymarketBridge.

Removed:
- strategy/polymarket-bridge/ directory entirely (8 files).
- TransactionPayStrategy.PolymarketBridge enum entry.
- PolymarketStrategy class and registration.
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.

1 participant