feat(transaction-pay-controller): support polymarket deposit-wallet#8754
Draft
matthewwalsh0 wants to merge 22 commits into
Draft
feat(transaction-pay-controller): support polymarket deposit-wallet#8754matthewwalsh0 wants to merge 22 commits into
matthewwalsh0 wants to merge 22 commits into
Conversation
…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.
10 tasks
…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
…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.
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.
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
predictWithdrawflow by folding it into the existingRelayStrategyas 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