Skip to content

feat(predict): add crypto up/down feed card#30342

Merged
ghgoodreau merged 80 commits into
mainfrom
predict/crypto-updown-feed-card
May 20, 2026
Merged

feat(predict): add crypto up/down feed card#30342
ghgoodreau merged 80 commits into
mainfrom
predict/crypto-updown-feed-card

Conversation

@ghgoodreau

@ghgoodreau ghgoodreau commented May 18, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds the Predict crypto up/down feed card and its supporting plumbing. It introduces the inline live sparkline + dual Up/Down CTA card that renders for crypto up/down markets inside the Predict feed, including current-series resolution so the card always shows the next active window. It also wires live RTDS crypto prices through to the feed card and tightens the underlying live chart behavior so the chart stays continuous across market rollover and uses a fixed 30-second live window.

It also includes targeted fixes to the existing crypto details screen so the header, chart, and CTA prices line up with the new feed-card expectations: live-updating CTA prices via WebSocket, simplified details header, and bottom margin under the CTA buttons.

TODO

  1. This PR is stack item 4/4 and builds on feat(predict): add crypto up/down details UI #30005, which builds on feat(predict): add crypto price data plumbing #30004 (merged) and feat(predict): update liveline chart foundation #30003 (merged).
  2. Keep this PR as draft until the stacked PR (feat(predict): add crypto up/down details UI #30005) is validated and all required checks are passing.
  3. Add screenshots or recordings before marking ready for review if reviewers want visual evidence for the new feed card.

Changelog

CHANGELOG entry: Added a live crypto up/down feed card with inline sparkline and Up/Down actions in Predict

Related issues

Fixes: N/A - stacked Predict crypto up/down implementation split from #29436.

Manual testing steps

Feature: Crypto up/down feed card

  Scenario: user sees and uses a crypto up/down feed card
    Given the Predict feature is enabled and a crypto up/down market is available

    When user opens the Predict feed
    Then user sees a crypto up/down card with the current price, target price, live sparkline, and Up/Down actions

    When the current series window expires
    Then the feed card automatically advances to the next active market without manual refresh

    When user taps the Up or Down action on the feed card
    Then the Predict buy flow opens for the selected outcome on the current market

    When user opens the crypto up/down details screen from the feed card
    Then the details screen shows a continuous live chart that does not reset on market rollover

Screenshots/Recordings

Before

N/A

After

N/A

Pre-merge author checklist

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 to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

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 new crypto Up/Down feed card and refactors crypto price streaming/history fetching (including new RTDS topic and Chainlink candle history), which could affect live price display and chart continuity across rollovers.

Overview
Introduces a new crypto Up/Down feed card (PredictCryptoUpDownMarketCard) with inline animated sparkline, live countdown/progress ring, and Up/Down buy CTAs, and wires it into PredictMarket rendering via isCryptoUpDown.

Refactors crypto series/market resolution and data plumbing by adding useCurrentPredictMarketFromSeries + useCurrentCryptoUpDownMarketData, centralizing series-window utilities (getCurrentSeriesWindowMs, findLiveMarket/findNearestMarket), and updating TimeSlotPicker scrolling behavior.

Improves live chart behavior (useCryptoUpDownChartData + PredictCryptoUpDownChart) with explicit enabled/liveUpdatesEnabled/historicalWindow options, preserved continuity across market rollover, target-based momentum coloring, and updated chart padding; updates the crypto details screen formatting/layout and shifts open-outcome computation to useOpenOutcomes.

Switches Polymarket crypto history fetching from the legacy endpoint to Chainlink candles with variant-based interval/limit mapping and window filtering, and updates crypto price streaming to the crypto_prices_chainlink RTDS topic with symbol/usd formatting; tests updated/added accordingly.

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

ghgoodreau and others added 30 commits May 11, 2026 14:56
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix two stale test assertions in PredictCryptoUpDownMarketCard that referenced the old 4-arg useCryptoUpDownChartData signature (market, chartRef, targetPrice, options) - the hook is now 3-arg (market, targetPrice, options) since 25d3f50, so drop the extra undefined arg and re-index the calls[0][2] options lookup. Address three SonarCloud smells flagged on this PR: remove the unused PredictMarket import in useCurrentCryptoUpDownMarketData, drop the unused isCarousel prop on PredictCryptoUpDownMarketCard, and extract the historyEndDate nested ternary in useCryptoUpDownChartData into a named intermediate (liveHistoryEndDate).
PredictMarket.tsx routes to all variant cards and forwards isCarousel uniformly. Removing the prop from PredictCryptoUpDownMarketCard broke the variant-card contract and produced a TS2322 in PredictMarket.tsx. Restore the prop with a JSDoc explaining it is accepted for contract parity with PredictMarketSingle / PredictMarketMultiple; the crypto card does not yet implement a carousel sizing variant. The earlier sonar smell ('PropType defined but never used') will be addressed properly by wiring the carousel sizing in a follow-up.
@ghgoodreau ghgoodreau marked this pull request as ready for review May 19, 2026 22:28
@ghgoodreau ghgoodreau requested review from a team as code owners May 19, 2026 22:28
@MarioAslau

Copy link
Copy Markdown
Contributor

High-priority issues

H1. Chainlink-candles history endpoint silently returns empty for non-recent / expired markets

const { CHAINLINK_CANDLES_ENDPOINT } = getPolymarketEndpoints();
const { interval, limit } =
  CHAINLINK_CANDLE_CONFIG_BY_VARIANT[variant] ??
  DEFAULT_CHAINLINK_CANDLE_CONFIG;
const searchParams = new URLSearchParams({
  symbol: normalizedSymbol,
  interval,
  limit: String(limit),
});

// ...

return data.candles
  .filter((entry): entry is { time: number; close: number } => ... )
  .filter((entry) =>
    isWithinWindow({
      timestamp: entry.time,
      startSeconds,
      endSeconds,
    }),
  )
  .map((entry) => ({
    timestamp: entry.time,
    value: entry.close,
  }));

The previous price-history endpoint took eventStartTime and endDate as server-side query parameters, so the server returned data for the exact window. The new endpoint only accepts symbol, interval, limit and always returns the latest limit candles, then we client-side filter to [startSeconds, endSeconds]. This is fine while the user looks at a live market, but it breaks for any historical look-up:

  • A user opens PredictCryptoUpDownDetails and taps a past time slot in the TimeSlotPicker for a 5-min BTC market that ended 1 hour ago.
  • The hook requests { symbol: 'BTC', interval: '1m', limit: 15 } → server returns the last 15 minutes from now, e.g. timestamps in [now-15m, now].
  • Client filters by endSeconds = market.endDate (1 hour ago) → every candle fails the <= endSeconds check → returns [].
  • The details chart shows nothing for that time slot.

For longer recurrences this is worse:

  • daily returns 30 × 1h candles = the last 30 hours only. Tap a daily market from yesterday → no data, even though "yesterday" is well within 30h.
  • 4h returns 60 × 5m candles = last 5 hours. Tap a 4h market from 6 hours ago → empty.

The "past time slot" path is exactly what TimeSlotPicker is built for in PredictCryptoUpDownDetails. The PR adds the picker integration but does not add coverage for this regression. The unit tests on the provider ('gets crypto price history from Chainlink candle closes', 'uses supported Chainlink candle intervals…') only assert that the URL is built correctly — they don't assert that requesting a historical window actually returns the candles for that window.

Suggested fix (in priority order):

  1. Have the server-side endpoint accept startTime/endTime (or from/to) and return the candles for that window, the same way the previous endpoint did. This is the only fix that scales beyond the recurrence-sized lookback table.
  2. Failing that, dynamically compute limit from endDate - eventStartTime so the limit covers the requested window (e.g. Math.ceil((endSeconds - startSeconds) / intervalSeconds) + safetyMargin).
  3. At minimum, log a warning when the response is non-empty but all candles fail the window filter — currently the UI silently shows an empty chart.

A direct regression test for this path:

it('returns empty when the requested window is older than limit * interval', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({
      candles: Array.from({ length: 15 }, (_, i) => ({
        time: 10_000 + i * 60,
        close: 100 + i,
      })),
    }),
  });

  const result = await createProvider().getCryptoPriceHistory({
    symbol: 'BTC',
    eventStartTime: '1000',
    endDate: '1300',
    variant: 'fiveminute',
  });

  expect(result).toEqual([]); // currently passes silently — this is the regression
});

This isn't a hypothetical: it's exercised every time a user taps a non-live time slot in the new details screen the PR plugs the card into.


Medium-priority issues

M1. OutcomeButtons opens the buy sheet on resolved/closed markets at rollover boundaries

const { getPrice } = useLiveMarketPrices(tokenIds, {
  enabled: marketStatus === PredictMarketStatus.OPEN,
});
const upPrice = getLivePrice(upToken, getPrice);
const downPrice = getLivePrice(downToken, getPrice);

return (
  <Box flexDirection={BoxFlexDirection.Row} twClassName="...">
    <ButtonBase
      testID={PredictCryptoUpDownMarketCardSelectorsIDs.UP_BUTTON}
      onPress={() => onBuyPress(upToken)}
      twClassName="h-10 flex-1 rounded-lg bg-success-muted"
      disabled={!upToken}
    >
      ...
    </ButtonBase>
    <ButtonBase
      testID={PredictCryptoUpDownMarketCardSelectorsIDs.DOWN_BUTTON}
      onPress={() => onBuyPress(downToken)}
      twClassName="h-10 flex-1 rounded-lg bg-error-muted"
      disabled={!downToken}
    >
      ...
    </ButtonBase>
  </Box>
);

disabled is only !upToken/!downToken. The live-prices subscription correctly gates on marketStatus === OPEN, but the buttons themselves don't. Combined with resolvePredictSeriesMarket falling back to findNearestMarket when findLiveMarket returns undefined:

export const resolvePredictSeriesMarket = (
  sourceMarket: PredictMarketWithSeries,
  seriesMarkets?: PredictMarket[],
): PredictMarketWithSeries => {
  if (!seriesMarkets?.length) {
    return sourceMarket;
  }

  return attachSeriesToMarket(
    findLiveMarket(seriesMarkets) ??
      findNearestMarket(seriesMarkets) ??
      sourceMarket,
    sourceMarket.series,
  );
};

findNearestMarket happily returns a market whose endDate is in the past (its check is Math.abs(... - now), not "> now"). At exactly the rollover boundary, before the next usePredictSeries window refetch finishes, selectedMarket is the just-expired market. The user taps UphandleBuyPress runs executeGuardedAction(... openBuySheet({ market: selectedMarket, outcome, outcomeToken, ... })). openBuySheet opens the betslip for a closed outcome; downstream rejection emits an order-failure toast.

Two recommended changes:

  1. Gate the buttons:
    disabled={!upToken || marketStatus !== PredictMarketStatus.OPEN}
    and likewise for Down. This also fixes the visual cue (right now the buttons look identical regardless of market status).
  2. Add a guard in handleBuyPress that early-returns if selectedMarket.status !== PredictMarketStatus.OPEN. Defense in depth — the parent feed could pass a non-live market via market prop.

A unit test covering this path is missing — the existing "renders missing outcome data without opening a buy sheet" test (lines 449–466) only covers the outcomes: [] edge case, not status.

M2. Per-card 1-minute history refetch fires for every feed card

const chartHistoryEndMs = useSharedBucketedNowMs(
  CHART_HISTORY_WINDOW_BUCKET_MS,
);
const chartHistoryWindow = useMemo(
  () => ({
    startDate: new Date(
      chartHistoryEndMs - chartRequestDurationMs,
    ).toISOString(),
  }),
  [chartHistoryEndMs, chartRequestDurationMs],
);

CHART_HISTORY_WINDOW_BUCKET_MS = 60 * 1000. Every 60 seconds the startDate in chartHistoryWindow ticks forward, the react-query key in useCryptoUpDownChartData changes, and the card re-fetches its history. With:

staleTime: shouldStreamLive ? 1000 : Infinity,
refetchOnMount: shouldStreamLive || !liveUpdatesEnabled ? 'always' : false,

staleTime: Infinity doesn't help because each minute creates a new query key. On a feed showing N crypto up/down cards, that's N requests/minute sustained, plus an extra refetch on every mount. For a typical 5-card feed view on cellular this is borderline.

Two options:

  1. Keep chartHistoryWindow stable across the recurrence's display duration (e.g. bucket at chartDisplayDurationMs / 12 instead of fixed 60s, so a daily card refetches every 2 hours and a 5m card every ~25s).
  2. Anchor startDate to a stable epoch (getCurrentSeriesWindowMs) and let staleTime: Infinity actually cache. The chart already merges historical+live points; older history sliding forward by 60s is invisible to the user for a 24h-wide daily chart.

M3. useCurrentPredictMarketFromSeries polls at 1Hz when the window changes at durationMs granularity

useEffect(() => {
  if (!enabled || !resolvedSeriesId) {
    return undefined;
  }

  const interval = setInterval(() => {
    const nextWindowMs = getCurrentSeriesWindowMs(durationMs);
    setWindowMs((currentWindowMs) =>
      currentWindowMs === nextWindowMs ? currentWindowMs : nextWindowMs,
    );
  }, 1000);

  return () => clearInterval(interval);
}, [durationMs, enabled, resolvedSeriesId]);

For a 5m market, windowMs only changes once every 5 minutes, but this interval fires 300 times per change — and each tick runs getCurrentSeriesWindowMs and a setState call (gated to no-op). For a daily market, that's 86,400 ticks per useful change.

PredictCryptoUpDownDetails solves this correctly: schedule a setTimeout for the next boundary and re-schedule on fire. Apply the same pattern here:

useEffect(() => {
  if (!enabled || !resolvedSeriesId) return undefined;
  let timeout: ReturnType<typeof setTimeout>;
  const schedule = () => {
    const nowMs = Date.now();
    const delay = durationMs - (nowMs % durationMs);
    timeout = setTimeout(() => {
      setWindowMs(getCurrentSeriesWindowMs(durationMs));
      schedule();
    }, delay);
  };
  schedule();
  return () => clearTimeout(timeout);
}, [durationMs, enabled, resolvedSeriesId]);

This eliminates 99% of the wakeups for the hook, which is also called by PredictMarketDetails on every render (via the new deep-link resolution path).

M4. getCurrentWindowMs is duplicated in PredictCryptoUpDownDetails.tsx

const getCurrentWindowMs = (durationMs: number) => {
  const currentTimeMs = Date.now();
  if (!Number.isFinite(currentTimeMs) || durationMs <= 0) {
    return 0;
  }

  return Math.floor(currentTimeMs / durationMs) * durationMs;
};

vs.

export const getCurrentSeriesWindowMs = (
  durationMs: number,
  nowMs = Date.now(),
): number => {
  if (!Number.isFinite(nowMs) || durationMs <= 0) {
    return 0;
  }

  return Math.floor(nowMs / durationMs) * durationMs;
};

The PR extracted getCurrentSeriesWindowMs to the shared utils/series.ts (and adopted it in the card) but left this local copy in PredictCryptoUpDownDetails.tsx. Two near-identical implementations of the same primitive will drift. Replace the local function with the shared one (and import findLiveMarket from utils/series directly while you're there — it's already a re-export at TimeSlotPicker.utils.ts).

M5. getVariant silently maps unknown recurrences to 'hourly'

export function getVariant(recurrence: string): string {
  return RECURRENCE_TO_VARIANT[recurrence] ?? 'hourly';
}

Combined with the new endpoint:

const CHAINLINK_CANDLE_CONFIG_BY_VARIANT: Record<
  string,
  { interval: ChainlinkCandleInterval; limit: ChainlinkCandleLimit }
> = {
  fiveminute: { interval: '1m', limit: 15 },
  fifteen: { interval: '1m', limit: 30 },
  hourly: { interval: '1m', limit: 60 },
  fourhour: { interval: '5m', limit: 60 },
  daily: { interval: '1h', limit: 30 },
};

A future crypto recurrence (e.g., '30m', which is already a valid key in series.ts/RECURRENCE_MAP) will return variant = 'hourly' from getVariant, then { interval: '1m', limit: 60 } = 1h of data on a 30-min event — half too much, half stale. For a weekly recurrence (also in RECURRENCE_MAP) it's catastrophic: 1h of data on a 7-day event window.

Two mitigations:

  1. Make getVariant return undefined for unknown recurrences and have callers handle "not chartable" instead of pretending the data exists.
  2. Add a DevLogger.log('Unknown variant, falling back to hourly', { recurrence }) so the fallback is visible during QA.

The same drift risk exists between series.ts/RECURRENCE_MAP and cryptoUpDown.ts/RECURRENCE_TO_DURATION_SECS — they overlap on 5m, 15m, 1h, 4h, daily but diverge on 30m, weekly, hourly. Either consolidate to a single map or make cryptoUpDown.ts re-export from series.ts.

M6. Module-level state in PredictCryptoUpDownMarketCard has no test-reset hook

let sparklineGradientIdCounter = 0;
const CLOCK_UPDATE_OFFSET_MS = 50;
const clockListeners = new Set<() => void>();
let clockNowMs = Date.now();
let clockTimeout: ReturnType<typeof setTimeout> | undefined;

Three pieces of module-level state:

  • sparklineGradientIdCounter — monotonically increasing; never decremented. Should be React 18's useId() so each <Sparkline> instance has a stable, render-deterministic id (helps SSR/concurrent rendering too, and avoids test bleed).
  • clockListeners + clockNowMs + clockTimeout — shared module-level clock. If a test (or its teardown) leaves a listener registered, the next test in the same file sees the leftover and may observe phantom updates. Same hazard PR fix(predict): prevent buy/sell sheet from being cropped in HomepageDiscoveryTabs #30219 had with _providerSheetModeCount.

Recommended:

  1. Replace sparklineGradientIdCounter with useId.
  2. Export a __resetCardClockForTest() or rename the module-state into a singleton class so tests can new ClockClient() per case.
  3. The existing test calls jest.clearAllMocks() in beforeEach but does not reset the clock state. The current tests pass because each test reaches steady state quickly, but this is a slow-to-find footgun.

M7. PredictMarketDetails deep-link path with series but no series.id/seriesId shows a perpetual skeleton

const shouldResolveMarketFromSeries =
  !marketId && Boolean(series?.id ?? seriesId);
const {
  market: currentSeriesMarket,
  marketId: currentSeriesMarketId,
  isLoading: isCurrentSeriesMarketLoading,
  isFetching: isCurrentSeriesMarketFetching,
  refetch: refetchCurrentSeriesMarket,
} = useCurrentPredictMarketFromSeries({
  series,
  seriesId,
  seriesRecurrence,
  enabled: shouldResolveMarketFromSeries,
});
const resolvedMarketId = marketId ?? currentSeriesMarketId;

If a deep link arrives with series: { id: '', slug: '', title: '', recurrence: '5m' } (a malformed payload, e.g. from a stale share link), then series.id is falsy, seriesId is undefined, shouldResolveMarketFromSeries=false, resolvedMarketId=undefined, useCurrentPredictMarketFromSeries is disabled, usePredictMarket({ id: '', enabled: false }) is also disabled, and isResolvedMarketLoading = isMarketLoading || isResolvingMarketFromSeries. Both flags are false, so isResolvedMarketLoading=false, the skeleton hides, market=null, and the user sees an empty white screen with no error path or "not found" message.

This is an edge case but trivially reachable via stale share URLs. Add a minimal "market unavailable" branch when !resolvedMarketId && !isResolvedMarketLoading.

- Chainlink candles: size limit dynamically against the requested
  window when the recurrence's base coverage isn't enough; warn via
  DevLogger when every candle is filtered out so silent empty charts
  surface in QA.
- OutcomeButtons: gate disabled state on marketStatus === OPEN and
  harden handleBuyPress so rollover-boundary closed markets cannot
  open the buy sheet.
- useCryptoUpDownChartData history bucket: scale by display duration
  (displayMs / 12, floor at 60s) so daily/4h cards stop refetching
  history once a minute.
- useCurrentPredictMarketFromSeries: replace 1Hz setInterval with a
  self-rescheduling setTimeout that fires at the next durationMs
  boundary.
- getVariant: log a DevLogger warning when falling back to 'hourly'
  for an unknown recurrence and map 'hourly' through explicitly.
- PredictCryptoUpDownDetails: drop the duplicate getCurrentWindowMs
  in favour of utils/series#getCurrentSeriesWindowMs and import
  findLiveMarket from the SSOT.
- PredictCryptoUpDownMarketCard: replace the module-level sparkline
  gradient counter with useId and move the shared clock behind a
  testable singleton with __resetCardClockForTest.
- PredictMarketDetails: render a market-unavailable empty state when
  a deep link arrives without a resolvable marketId or series id,
  instead of a perpetual skeleton followed by a blank screen.
…cker

- PredictCryptoUpDownChart: force badge momentum from value vs target
  so the price badge colors by win/lose state instead of recent-tick
  momentum. Falls back to liveline auto-detection when no target.
- TimeSlotPicker: delay scroll-to-selected so onLayout settles after
  rollover refetches, subtract a content-padding offset so the
  selected pill isn't flush left, and drop the lastScrolledId guard
  so the picker re-scrolls when the selection returns to a
  previously-visited pill.
- PredictCryptoUpDownDetails: split formatted currency into whole
  and fraction so the 'Price to beat' and current-price headers
  render the dollars at DisplayMd and the cents inline at HeadingMd.
  Tighten chart top padding.
Drop LivelineChart top padding from 48 to 8 so the chart sits right
under the price headers instead of leaving a large empty band above
the first gridline.
Comment thread app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts Outdated
Comment thread app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts Outdated
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.

Thanks for addressing the issues! LGTM !

The previous guard required both startSeconds AND endSeconds to be
defined before the dynamic limit kicked in. The feed card's trailing
history window passes only startDate (no endDate), so it always
short-circuited to baseLimit and silently under-fetched candles when
the requested lookback exceeded the recurrence's base coverage —
most visibly on daily charts where baseLimit=30 covers 30 hours but
the card requests 7 days of history.

Drop endSeconds from computeChainlinkCandleLimit entirely. It was
never used in the computation body, and the existing
lookbackSeconds <= baseLimitCoverageSeconds branch already handles
future-startSeconds via a negative lookback. Removing it also
addresses the misleading-contract finding from review (endSeconds in
the signature but unused in the body).

Add a regression test that exercises the open-ended trailing-window
path so a future refactor doesn't re-introduce the short-circuit.
Comment thread app/components/UI/Predict/hooks/useCurrentCryptoUpDownMarketData.ts
Comment thread app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts Outdated
Direct probing of https://polymarket.com/api/chainlink-candles
revealed a hard server-side allowlist on the limit parameter:
`{"error":"limit must be one of 15, 30, or 60"}`. The dynamic
expansion introduced for the H1 review finding was computing limits
like 125 / 173 / 200 for deep historical windows; every one of
those requests returned 400 and the sparkline failed silently.

The reviewer's preferred fix for H1 was a server change (accept
start/to in the query) — without that, mobile can only request
{15, 30, 60} candles at one of four allowed intervals. There is no
client-side fix for lookups older than baseLimit * interval, so the
dynamic logic just broke working flows.

Restore baseLimit usage, narrow ChainlinkCandleLimit back to
15 | 30 | 60, and document the contract inline so the next
contributor doesn't repeat the mistake. Keep the DevLogger warning
when client-side filtering drops every candle — that signal is
still useful for QA on deep-history lookups.

Drops the two dynamic-expansion regression tests; keeps the
'logs a development warning when every candle falls outside the
requested window' test.
@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 40 changed files are confined to the Predictions (Polymarket) feature area and its test infrastructure:

  1. tests/component-view/mocks.ts (CRITICAL): Additive change adding mock methods (getMarketSeries, getCryptoPriceHistory, getCryptoTargetPrice, subscribeToCryptoPrices) to the Engine mock for Predict. This supports new Crypto Up/Down functionality in component tests and won't break existing tests.

  2. Predict UI components (39 files): Significant feature additions:

    • New Crypto Up/Down market type with chart visualization using Chainlink candles API (replacing old price history endpoint)
    • New series-based market resolution (useCurrentPredictMarketFromSeries) allowing navigation to market details without a marketId by resolving from a series
    • WebSocketManager updated to use crypto_prices_chainlink topic with per-symbol subscriptions/unsubscriptions
    • PredictMarketDetails enhanced with series resolution, market unavailable state, and improved loading states
    • New utility functions for time window calculations, market resolution, countdown formatting
    • New PredictCryptoUpDownMarketCard component

Tag Selection Rationale:

  • SmokePredictions: Directly affected — the Predictions market details view, market card rendering, and position flows are all modified. The PolymarketProvider's price history API changed (Chainlink candles), WebSocket subscription logic changed, and market navigation flow changed.
  • SmokeWalletPlatform: Required per SmokePredictions tag description — Predictions is a section inside the Trending tab, and changes to Predictions views affect Trending.
  • SmokeConfirmations: Required per SmokePredictions tag description — opening/closing positions are on-chain transactions that go through the confirmations flow.

No changes to shared navigation, Engine controllers, account management, swap/bridge, or other cross-cutting components. The changes are well-contained within the Predict feature.

Performance Test Selection:
The Predictions feature received significant changes including a new Chainlink candles API for price history, new WebSocket subscription logic for crypto prices, new chart rendering (PredictCryptoUpDownChart), and series-based market resolution with time-based window calculations. These changes could impact the rendering performance of the Predict market details view and the balance/data loading performance. The @PerformancePredict tag covers prediction market list loading, market details, deposit flows, and balance display — all of which are touched by these changes.

View GitHub Actions results

@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 4bca5e7. Configure here.

@sonarqubecloud

Copy link
Copy Markdown

@ghgoodreau ghgoodreau added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 9e1e37c May 20, 2026
139 of 144 checks passed
@ghgoodreau ghgoodreau deleted the predict/crypto-updown-feed-card branch May 20, 2026 17:27
@github-actions github-actions Bot locked and limited conversation to collaborators May 20, 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 20, 2026
@SteP-n-s SteP-n-s added team-predict Predict team and removed team-swaps-and-bridge Swaps and Bridge team labels May 26, 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-XL team-predict Predict team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants