Skip to content

chore(predict): Block prediction orders on post-active markets#30403

Merged
caieu merged 13 commits into
mainfrom
predict/PRED-828-block-bet-placement-on-markets-in-proposed-resolution-or-dispute-state
May 21, 2026
Merged

chore(predict): Block prediction orders on post-active markets#30403
caieu merged 13 commits into
mainfrom
predict/PRED-828-block-bet-placement-on-markets-in-proposed-resolution-or-dispute-state

Conversation

@caieu

@caieu caieu commented May 19, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds a confirm-time Predict market state guard so orders cannot be submitted after a Polymarket market has entered a post-active state, even if the user reached the market through a stale feed card, deeplink, search result, or cached details screen.

The change:

  • Propagates Polymarket active, acceptingOrders, resolved, and UMA resolution status fields into Predict market/outcome models.
  • Adds a live market details check inside PredictController.placeOrder before forwarding the order to the provider.
  • Blocks pending resolution, dispute, finalized/resolved, closed, inactive, and non-accepting markets with specific Predict error codes.
  • Shows the parsed market-state error in the buy sheet banner instead of a generic order-failed message, with generic copy as a fallback.
  • Resets the confirm/loading state when an immediate guard error is returned, preventing the buy sheet CTA from getting stuck disabled.

Scope note: this is the client/controller guard only. No server-side forwarding guard is included.

Changelog

CHANGELOG entry: Fixed a bug that could allow prediction orders on markets that are no longer accepting bets.

Related issues

Fixes: PRED-828

Related: PRED-747

Manual testing steps

Feature: Predict market state guard

  Scenario: user tries to place a prediction on a market pending resolution
    Given a Predict market has entered proposed resolution or dispute
    And the user opens the buy sheet from the market details screen
    When the user taps Confirm
    Then the order is not submitted
    And the buy sheet shows "This market is pending resolution."

  Scenario: user tries to place a prediction on a market that is no longer accepting bets
    Given a Predict market is closed, resolved, inactive, or no longer accepting orders
    And the user opens the buy sheet from the market details screen
    When the user taps Confirm
    Then the order is not submitted
    And the buy sheet shows "This market is no longer accepting bets."

  Scenario: live market state cannot be confirmed
    Given the confirm-time market details check fails
    And the user has entered a valid prediction amount
    When the user taps Confirm
    Then the order is not submitted
    And the buy sheet shows "We couldn't confirm this market is accepting bets. Try again."

  Scenario: active market still accepts predictions
    Given a Predict market is open, active, unresolved, and accepting orders
    And the user has enough Predict balance
    When the user taps Confirm
    Then the order submission flow proceeds normally

Automated testing

  • yarn jest app/components/UI/Predict/controllers/PredictController.test.ts app/components/UI/Predict/providers/polymarket/utils.test.ts app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyActions.test.ts app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyError.test.ts
    • Result: 4 suites passed, 491 tests passed.
  • git diff --check
    • Result: passed.

Screenshots/Recordings

Before

N/A

After

N/A

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android. Not applicable: no Android-specific behavior changed.
  • I've tested with a power user scenario. Not applicable: no account/token scaling behavior changed.
  • I've instrumented key operations with Sentry traces for production performance metrics. Not applicable: this uses existing Predict order submission tracing.

For performance guidelines and tooling, see the Performance Guide.

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.

Note

Medium Risk
Adds a confirm-time live market lookup that can block order placement and alters pay-with-any-token state/cleanup behavior; risk is mainly around false negatives or new failure paths that could prevent valid orders.

Overview
Adds a confirm-time market bettability guard before PredictController.placeOrder submits to the provider, rejecting orders when the live market/outcome is inactive, closed/resolved, in dispute/pending resolution, or not accepting orders (and failing closed when the check can’t be performed).

Propagates Polymarket active/acceptingOrders fields into Predict market/outcome models, introduces new Predict error codes + localized messages for these blocked states, and updates buy-sheet hooks to avoid stuck confirming state and to display the specific parsed guard error (with logging and generic fallback).

Reviewed by Cursor Bugbot for commit 3cbeec2. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2 metamaskbotv2 Bot added the team-predict Predict team label May 19, 2026
@caieu caieu marked this pull request as ready for review May 19, 2026 20:52
@caieu caieu requested a review from a team as a code owner May 19, 2026 20:52

@MarioAslau MarioAslau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @caieu awesome work! I couldn't find any high severity issues

These are the medium ones:

Medium severity

M1 — Confirm-time API round-trip with no caching adds latency to every order

try {
  await validateMarketBettable({
    provider: this.provider,
    preview: params.preview,
  });
} catch (error) {

validateMarketBettable calls provider.getMarketDetails({ marketId: preview.marketId }) on every placeOrder invocation. getMarketDetails hits the Gamma API (getMarketDetailsFromGammaApi) — a fresh fetch with no in-memory cache, no dedupe, no retry, no timeout override.

For a user that just sat on the buy preview for 10–60 seconds and then taps Confirm, the data was fetched seconds ago by the same screen. We're adding a serial network round-trip in the critical path of the order — typically 100–500 ms on Wi-Fi, 1–3 s on degraded mobile connectivity — before the order is even forwarded. On a transient failure this becomes a hard block via MARKET_BETTABLE_CHECK_FAILED.

Suggested improvements:

  1. Add a short TTL cache keyed by marketId (5–10 s is enough to cover preview→confirm taps and double-taps without weakening the guard against stale feed-card entries).
  2. Single retry on network error before failing closed — a single 5xx/timeout shouldn't permanently block a valid order.
  3. Optional: time-box the call (e.g. Promise.race with a 2 s timeout) so users on slow connections see the error promptly instead of the spinner hanging.

Failing closed on the validation API is the right security default; the issue is the latency tax on the happy path.

M2 — Outcome-not-found leaks across two distinct failure modes

export const getNonBettableMarketErrorCode = (
  market: PredictMarket,
  preview: OrderPreview,
): string | undefined => {
  const outcome = market.outcomes.find(({ id }) => id === preview.outcomeId);

  if (!outcome) {
    return PREDICT_ERROR_CODES.MARKET_NOT_ACCEPTING_BETS;
  }

There are two reasons the outcome can be missing from market.outcomes:

  • The outcome really doesn't exist (probably impossible in practice if the preview was valid).
  • The outcome was filtered out at parse time. In parsePolymarketEvents, line 1138:
    .filter((market: PolymarketApiMarket) => market?.active !== false);
    
    Polymarket markets with active === false are dropped before being mapped to PredictOutcome[].

Because the filter runs before validateMarketBettable sees the data, the outcome.active === false branch on line 54 of marketState.ts is effectively dead code, and a market whose only outcome is active === false + resolutionStatus === 'proposed_resolution' will surface as MARKET_NOT_ACCEPTING_BETS ("This market is no longer accepting bets.") even though the more accurate copy is "This market is pending resolution.".

Suggested fixes (pick one):

  • A: Bypass the active !== false filter when parsePolymarketEvents is called from getMarketDetails (single-market context), so the bettability util can read the full outcome state. The filter still makes sense in the markets-list rendering path.
  • B: In validateMarketBettable, when the outcome is missing, fetch the raw PolymarketApiMarket (or look it up via umaResolutionStatus from a sibling source) so we can distinguish the two cases.
  • C (lowest-risk): Treat outcome-not-found as MARKET_BETTABLE_CHECK_FAILED instead of MARKET_NOT_ACCEPTING_BETS, since the data state is ambiguous and the user-facing copy "We couldn't confirm this market is accepting bets. Try again." is more honest.

M3 — selectedPaymentToken is wiped on a transient network error

this.update((state) => {
  state.lastError = errorMessage;
  state.lastUpdateTimestamp = Date.now();
  if (isBuyWithAnyToken && state.activeBuyOrders[activeOrderAddress]) {
    state.activeBuyOrders[activeOrderAddress].state =
      ActiveOrderState.PREVIEW;
    state.activeBuyOrders[activeOrderAddress].error = errorMessage;
  }
  if (isBuyWithAnyToken) {
    state.selectedPaymentToken = null;
  }
});

For the pay-with-any-token flow, the user picked a payment token (USDC on Linea, ETH on Arbitrum, etc.). When the guard returns MARKET_BETTABLE_CHECK_FAILED — a retryable network error, not a final state — we still null out selectedPaymentToken. The user has to:

  1. Read the "We couldn't confirm…" banner
  2. Reopen the token picker
  3. Re-select the same token
  4. Tap Confirm again

The existing post-createOrder-failure path does the same reset, which makes sense there (the deposit batch is no longer reusable). For a pre-flight check failure where no deposit has been initiated, this is over-reactive.

Recommended: keep selectedPaymentToken intact on MARKET_BETTABLE_CHECK_FAILED. On MARKET_PENDING_RESOLUTION / MARKET_NOT_ACCEPTING_BETS (terminal states for this market), keeping the token is also fine — the user typically switches markets, not tokens.

M4 — No analytics signal when the guard fires

The guard throws before trackPredictOrderEvent({ status: PredictTradeStatus.SWAP_INITIATED, ... }) runs (around line 1196). We don't currently emit any analytics event when an order is blocked by the bettability check.

Consequences:

  • We lose product visibility into how often feed cards / deeplinks / cached details screens are surfacing stale (post-active) markets to the order screen.
  • We can't measure whether the upstream fixes (e.g. PR #30342's isMarketUnavailable branch, or feed-card filtering) reduce the rate of guard hits over time.
  • QA can't easily verify "the guard fires when expected" in a production-like environment.

Recommended: emit a predict_order_blocked analytics event in the catch block, with properties { reason: errorMessage, marketId, outcomeId, side, entryPoint }. One call site, one event — but it unlocks dashboards and regression detection.

@github-actions github-actions Bot added size-L and removed size-M labels May 20, 2026
MarioAslau
MarioAslau previously approved these changes May 20, 2026

@MarioAslau MarioAslau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@caieu caieu force-pushed the predict/PRED-828-block-bet-placement-on-markets-in-proposed-resolution-or-dispute-state branch from d04961b to 4975b55 Compare May 20, 2026 19:30

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4975b55. Configure here.

Comment thread app/components/UI/Predict/controllers/PredictController.ts

@MarioAslau MarioAslau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePredictions, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePredict
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
All changes are scoped to the Predict/Polymarket feature area. Key behavioral changes:

  1. PredictController.ts: A new validateMarketBettable check is inserted before order submission. This is a critical path change - orders for markets that are pending resolution, closed, or inactive will now be blocked with specific error codes before reaching the provider. This directly affects the order placement flow tested by SmokePredictions.

  2. marketState.ts (new): New validation utility that fetches live market details and checks outcome/market status. This adds a network call to the order flow.

  3. usePredictBuyActions.ts: Refactored error handling distinguishes between pre-provider failures (which reset initiation state) and provider-side failures (which preserve initiation state). This affects the UX flow when orders fail.

  4. usePredictBuyError.ts: Error banner now shows specific error messages (e.g., "This market is pending resolution") instead of generic text.

  5. errors.ts + en.json: New error codes and localization strings.

SmokePredictions is the primary tag - these changes directly affect the position lifecycle (opening positions, error handling) which is the core of SmokePredictions tests.

SmokeWalletPlatform is required per SmokePredictions tag description: "Predictions is also a section inside the Trending tab (SmokeWalletPlatform); changes to Predictions views affect Trending. When selecting SmokePredictions, also select SmokeWalletPlatform."

SmokeConfirmations is required per SmokePredictions tag description: "opening/closing positions are on-chain transactions. When selecting SmokePredictions, also select SmokeConfirmations."

The changes are medium risk - they add a new validation step in the critical order placement path that could potentially block valid orders if the market state check fails unexpectedly, but the logic is well-tested with unit tests and the fallback behavior is conservative (fail closed).

Performance Test Selection:
The changes add a new async network call (getMarketDetails) to the order placement flow in PredictController. This adds latency to the order confirmation path. The @PerformancePredict tag covers prediction market flows including deposit flows and balance display, which would be affected by this additional network round-trip before order submission. This is worth measuring to ensure the added validation step doesn't significantly degrade the user experience.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@caieu caieu enabled auto-merge May 21, 2026 13:17
@caieu caieu added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 2aae66f May 21, 2026
277 of 281 checks passed
@caieu caieu deleted the predict/PRED-828-block-bet-placement-on-markets-in-proposed-resolution-or-dispute-state branch May 21, 2026 14:08
@github-actions github-actions Bot locked and limited conversation to collaborators May 21, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.79.0 Issue or pull request that will be included in release 7.79.0 label May 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.79.0 Issue or pull request that will be included in release 7.79.0 size-L team-predict Predict team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants