Skip to content

feat(transaction-pay-controller): support polymarket deposit-wallet#8754

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

feat(transaction-pay-controller): support polymarket deposit-wallet#8754
matthewwalsh0 wants to merge 22 commits into
mainfrom
feat/polymarket-bridge-withdraw-strategy

Conversation

@matthewwalsh0
Copy link
Copy Markdown
Member

@matthewwalsh0 matthewwalsh0 commented May 11, 2026

Explanation

Polymarket users hold pUSD inside a deposit wallet — a deterministic, EOA-owned batch contract on Polygon, not the EOA itself — so the existing Relay strategy cannot move funds out of it directly (Relay submits source-chain transactions from the EOA).

This PR adds support for the deposit-wallet predictWithdraw flow by folding it into the existing RelayStrategy as a source-leg variant, mirroring how Hyperliquid is integrated today. The Polymarket-relayer protocol itself (EIP-712 typed data signing, HTTP transport, polling, busy-retry, error shapes) is delegated to the consuming client through two new optional callbacks supplied at controller construction.

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.
…into Relay strategy

Five cleanups based on review:

1. Use @ethersproject/{abi,address,bytes,keccak256} standard helpers in
   computeDepositWalletAddress instead of the hand-rolled abiEncode,
   hexZeroPad, hexConcat, and getCreate2Address. The Solady
   ERC-1967 initCodeHash bytecode-prefix math stays as-is because no
   standard library reproduces it.
2. Inline encodeUnwrap/encodeWrap shared selector+args pattern.
   encodeTwoArg/encodeThreeArg helpers removed - the two call sites
   carry the selector literal directly and share the same padAddress
   and padUint256 primitives.
3. relay-quotes.ts builds the standard quote body once, then patches
   the Polymarket overrides (originCurrency, user, refundTo,
   useDepositAddress) via applyPolymarketDepositWalletOverrides. No
   more parallel body-construction branch.
4. relay-submit.ts consolidates the parallel
   executePolymarketDepositWalletQuote function into the single
   executeSingleQuote. Source-leg branches on isHyperliquidSource vs
   isPolymarketDepositWallet vs default, then runs a unified
   waitForRelayCompletion with tolerateFailure=true for Polymarket.
   waitForRelayCompletion's signature is now (quote, messenger, {
   onSourceHash?, tolerateFailure? }) and returns Hex | undefined.
   The sweep call happens only when isPolymarket, between the status
   poll and the isIntentComplete marker.
5. polymarket/withdraw.ts exports are slimmed to submitPolymarketWithdraw
   (source leg) and sweepPolymarketDepositWallet (post-completion).
   setPolymarketSourceHash is inlined into relay-submit as
   setRelaySourceHash since it is a generic Relay-flow concern, not
   Polymarket-specific.
…ation

- Move polymarketRelayerUrl feature flag under payStrategies.relay so it
  sits with the other relay-strategy settings instead of at the
  feature-flag root.
- Collapse the verbose JSDoc on isPolymarketDepositWallet to the
  single-line shape matching the existing two instances.
- Remove the redundant if/else split in executeSingleQuote: the
  nonce-removal step now runs unconditionally before any source-leg
  branch, then a single if-else-if-else dispatches to hyperliquid,
  polymarket, or the default submitTransactions path.
- Fold wallet-batch-typed-data into relayer-api.ts and drop the
  PolymarketRelayerApi class. Replaced with standalone utils getNonce
  and submitDepositWalletBatch that take the messenger directly,
  resolve the URL via getPolymarketRelayerUrl, build the typed data,
  call KeyringController:signTypedMessage, and poll the relayer to a
  terminal state in one call.
- Inline the four-line applyPolymarketDepositWalletOverrides into
  withdraw.ts so the polymarket subdirectory drops the quotes.ts file
  entirely; the override applier sits next to the withdraw helper that
  uses the resulting quote.
- One-line CHANGELOG entry now that the per-step detail belongs to the
  PR description not the changelog.
…pi + orchestration

- relayer-api.ts is now strictly the HTTP transport (getNonce,
  submitRelayerRequest, getTransactionStatus) plus the JSON envelope
  parser and PolymarketRelayerError.
- relayer.ts is the orchestration layer: submitDepositWalletBatch
  builds typed data, calls KeyringController:signTypedMessage, posts
  via the api, polls getTransactionStatus to terminal, and wraps the
  whole thing in wallet-busy retry.
- withdraw.ts drops its own busy-retry helper - retry is now automatic
  for any submitDepositWalletBatch caller.
- calldata.ts uses @ethersproject/abi Interface with inline function
  signatures instead of hand-padded hex; decoding the transfer
  recipient also goes through Interface.decodeFunctionData so the
  selector mismatch check comes for free.
- TransactionPayController.#getStrategiesWithFallback no longer
  short-circuits to Relay for isPolymarketDepositWallet; the regular
  strategy chain selects Relay naturally now that the flag is just a
  source-leg flavour inside the Relay strategy.
…d throw post-sweep when Relay did not succeed

waitForRelayCompletion now returns the terminal status plus the optional
target hash instead of just a hash (the default behaviour for non-tolerant
callers is unchanged - they still throw on any failure status).

For the Polymarket path (tolerateFailure: true):
- refund is treated as mid-flight (Relay-issued refund tx not yet
  confirmed) - polling continues until refunded so the deposit wallet's
  USDC.e balance is actually present by the time the sweep runs.
- refunded, failure, and timeout all return a terminal status without
  throwing; the caller still runs the USDC.e sweep so any USDC.e left at
  the deposit wallet is wrapped back into pUSD.
- After the sweep, the caller throws unless the status is success, so the
  transaction reports as failed in activity even when the sweep recovered
  the funds.
…e relayer cap

Polymarket's relayer rejects deadlines above 300s. Signing, posting, and
the relayer's own validation step can easily eat 60s, so 240s was leaving
too small a window and producing intermittent "deadline too soon"
rejections. Use the full 300s allowed by the relayer (matches mobile).
…lient callbacks

The Polymarket relayer protocol (EIP-712 typed-data, HTTP transport,
polling, busy retry, error shapes) is Polymarket-domain and does not
belong in the controller. Move it out into a pair of client-supplied
callbacks the controller invokes via messenger actions, mirroring the
existing getDelegationTransaction pattern.

New on TransactionPayControllerOptions:
- polymarket?: PolymarketCallbacks
  - getDepositWalletAddress({ eoa })
  - submitDepositWalletBatch({ eoa, depositWallet, calls })

The polymarket option is optional. Clients that do not use the
Polymarket deposit-wallet flow can omit it; the controller throws a
clear error if a strategy tries to invoke the callbacks when they were
not supplied.

The withdraw orchestration (Relay quote overrides, call building,
USDC.e sweep, Relay polling with refund -> refunded wait) stays in
core because it is Relay-flow logic. Only the Polymarket-protocol
transport is moved out.

Removed from core:
- strategy/relay/polymarket/deposit-wallet.ts
- strategy/relay/polymarket/relayer.ts
- strategy/relay/polymarket/relayer-api.ts
- strategy/relay/polymarket/types.ts (relayer envelope types)
- POLYMARKET_BATCH_DEADLINE_SECONDS, POLYMARKET_WALLET_DOMAIN_*,
  POLYMARKET_RELAYER_TERMINAL_STATES, POLYMARKET_RELAYER_PROXY_URL_PROD,
  DEPOSIT_WALLET_FACTORY_ADDRESS_POLYGON,
  DEPOSIT_WALLET_IMPLEMENTATION_POLYGON constants
- getPolymarketRelayerUrl feature-flag helper + payStrategies.relay.polymarketRelayerUrl
- @ethersproject/{address,bytes,keccak256} dependencies
…market action types

Add the PolymarketCallbacks public type and the two new polymarket
messenger action types
(TransactionPayControllerPolymarketGetDepositWalletAddressAction and
TransactionPayControllerPolymarketSubmitDepositWalletBatchAction) to
the package's public exports so clients can supply the callbacks at
construction and declare the actions in their messenger types.
Wrap the two polymarket messenger calls in private helpers that log
the EOA + derived deposit-wallet address on every getDepositWalletAddress
call, and the EOA + depositWallet + call count + returned sourceHash on
every submitDepositWalletBatch call. Makes it possible to trace the
full predictWithdraw flow from the polymarket-withdraw logger alone
without having to dig into client-side logs.
@matthewwalsh0 matthewwalsh0 changed the title feat(transaction-pay-controller): add PolymarketBridgeStrategy for deposit-wallet predictWithdraw feat(transaction-pay-controller): support Polymarket deposit-wallet predictWithdraw via Relay strategy May 12, 2026
@matthewwalsh0 matthewwalsh0 changed the title feat(transaction-pay-controller): support Polymarket deposit-wallet predictWithdraw via Relay strategy feat(transaction-pay-controller): support polymarket deposit-wallet May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant