fix(tests): update three api-route suites to their current contracts - #2456
Conversation
Fifth slice of the 65 CI failures #2450 exposed. Fixes 8, adds 10. Test files only, no product change. Each suite asserted a contract the route had deliberately moved away from: - funding-global (4): the route is no longer a pass-through proxy. It is a three-tier chain (Railway proxy -> local indexer Postgres -> graceful empty) that intentionally does NOT surface upstream errors, so the funding UI degrades to "no data" rather than an error state when the backend is down — the live situation whenever Railway is dead (#2443). Tests now assert the degradation, and cover the indexer-db fallback tier, which had none: it serves rows, applies the blocklist, and falls through to empty when the query itself fails. - middleware-rate-limit (3): these set BLOCKED_MARKET_ADDRESSES and expected a 404. middleware.ts imports `@/lib/blocklist-edge`, which is edge-pure and intentionally omits env-var overrides — importing the env-reading `@/lib/blocklist` co-bundled Node-only code into the Edge chunk and Vercel's deploy validator rejected it. The tests now pin that boundary explicitly instead of failing on it: env-injected slabs are blocked at the Node API layer only, and blocking at the Edge requires editing blocklist-edge.ts. - markets-supabase-outage-fallback (1): the devnet static directory was migrated to the single v17 wrapper program, so filtering by an old per-slab-tier program id matched 0 rows, not 3. Now asserts the filter's behaviour rather than a hardcoded count, plus the negative case — without it a no-op filter returning everything would still pass. Also adds __tests__/unit/blocklist-edge-sync.test.ts. blocklist-edge.ts carries a "KEEP IN SYNC with HARDCODED_BLOCKED_SLABS" comment and nothing enforced it. The two lists agree today (35 entries each), but drift would leave a slab blocked at one layer and reachable through the other — and entries there include a wrong oracle_authority (price-manipulation risk, GH#837). Includes a size assertion so emptying one set cannot make the diffs pass vacuously. Mutation-verified: removing one address from the edge list fails it. 95 -> 87 failures, 28 -> 25 files. tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…pper program The 2026-07 fee-split merge re-migrated the static DEVNET_MARKET_DIRECTORY_FALLBACK program from 69VUZ7a2… to the fee-split wrapper DhSkE7uTb8…, so the positive 'filters by program_id' test queried a program no longer in the directory and got 0 rows (expected >0). Updates both program-id constants to the current value; the negative-case test (unknown program → 0 rows) still guards against a no-op filter.
…ator On a staked market the wizard's full create flow (StakeInitPool + BindInsuranceAuthority) rotates marketauth, insurance_authority AND insurance_operator to program PDAs. The panel gated on insurance_operator, so on every real (staked) market it rendered for NOBODY -- no wallet holds a PDA key. Verified on the live staked market 7FBXdrm1…: insurance_operator = PDA 6a3tiSd2…, but asset_admin = the creator's wallet 7JVQvrAf…. asset_admin bootstraps to the creator and the stake flow leaves it alone, so it is the field that reliably tracks the creator through staking. Matches the on-chain re-gate (wrapper hash 4f5df6be…, tag 90 WithdrawCreatorFee now checks asset 0's asset_admin). - lib/v17-creator-fee.ts: read the gate from parseAssetOracleProfileV17(...). assetAdmin (SDK-owned, profile-rel 368), not .insuranceOperator. - useCreatorClaim.ts: isOperator -> isClaimAuthority; error copy "Only this market's admin (the creator) can claim". - CreatorClaimPanel.tsx: badge "You are the operator" -> "You are the creator". - Tests use three distinct keys (asset_admin=creator, insurance_operator=PDA, marketauth=third) mirroring the live market, and assert the panel shows for asset_admin and NOT insurance_operator -- reverting the read to insuranceOperator turns 20 tests red. tsc clean, next build green, 53/53 in the creator-claim suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
…d (code-split heavy cards)
Two issues reported on /analytics/[slab]: data lags behind reality, and initial
load is slow.
DATA LAG — SlabProvider used adaptive polling ("30s when wsActive, 3s when not"),
but wsActive latched to true on the FIRST onAccountChange message and was never
reset. A silently-dropped WebSocket (frequent on Vercel/public RPC — there's even
a reconnect-storm note in useWalletCompat) left the page refreshing only every
30s with nothing detecting it. Replaced with a steady 5s backup poll that the WS
rides on top of: WS still delivers ~instant updates when alive, and a dead socket
now costs at most 5s of staleness instead of 30s. bytesEqual in parseSlab dedups
the no-op re-parses, so the steadier poll adds zero extra re-renders. (Also
benefits /trade/[slab], which shares this provider.)
INITIAL LOAD — the analytics page mounted all 11 cards at once, 4 of which scan
every portfolio in the 26KB slab (AccountsCard, AdlLeaderboard,
LiquidationAnalytics, SystemCapitalCard). Code-split those 4 via next/dynamic
(ssr:false) with skeleton fallbacks so they load in their own chunks after the
light top-of-page cards paint, trimming the initial JS bundle and first-render
work for the dashboard route.
tsc clean, next build green (analytics route compiles with the split chunks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
…ly go
The tag-90 creator claim worked, but was reachable only via the URL-only
/analytics/[slab] route (a small "Analytics →" link on the trade page). Creators
had no way to find or claim their fees from the creator dashboard. This brings it
into My Markets.
- api/markets/[slab]: add creator_fee_claimable_atoms (u64 string) +
creator_fee_authority (asset-0 asset_admin) to the response. Cheap — reuses the
raw bytes the route already fetched; readCreatorFeeClaimable does the decode.
- CreatorMarketDetail + toCreatorMarketDetail: carry the two new fields.
- CreatorMarketRow:
- Collapsed row: a non-interactive "◈ $X to claim" pill when the connected
wallet IS the market's fee authority and a balance is accrued — so creators
SEE fees at a glance without expanding.
- Expand drawer: mount the working CreatorClaimPanel wrapped in a lazy
SlabProvider (only the open row fetches the 26KB slab; one drawer open at a
time). One-click claim, self-hiding for non-authority wallets. No hidden URL.
tsc clean, next build green, 53/53 creator-claim suites pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
Adds a "Fee distribution" card to /analytics/[slab] showing how each trade fee is split and how much has reached each leg, read straight from the SDK-parsed WrapperConfigV17 (no hand-rolled offsets): - SHARE (exact policy): FEE_SPLIT.PROTOCOL_FEE_BPS (fixed 20%) + the market's creator/lp/insurance share bps, rendered as a proportional bar + per-leg %. - COLLECTED: the cumulative on-chain *_accrued counters for protocol / LP / insurance, and creatorFeeClaimableAtoms for the creator. The creator leg uses a single counter (drops on claim), so it's labelled "claimable", not "collected", to stay honest — the other three are cumulative. Verified against the live market 7FBXdrm on-chain: trade fee 0.05%, split 20/16/48/16 (protocol/creator/lp/insurance) summing to exactly 10000 bps; collected LP $0.703 / protocol $0.293 / insurance $0.234 (ratios match the shares to 2dp), creator $0.000 (correctly reflects the claim that already ran). Also verified all analytics cards call the SDK properly — every card reads via SlabProvider's SDK parse; the only manual byte read is CrankHealthCard's asset.slot_last (no SDK parser exists for it, documented in-file), and AdlLeaderboard sources a server-side SDK API. tsc clean, next build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
Adds a desktop sticky footer to the trade page with Capital / Health / Liquidations / Fees tabs that reveal analytics inline on hover, so traders no longer need the separate /analytics route for a quick read. - Reuses the page's already-mounted SlabProvider (no extra RPC) and lazy-mounts each tab's data hooks only while that tab is open (fast; the closed dock costs nothing). - No repetition of the top MarketInfoBar (price/OI/high-low/spread/funding); the dock shows only the capital stack, health internals, liquidation params, and the fee split. - Fees tab omits the creator claimable figure (share % only) — claiming stays on the creator-only /analytics panel. - Health/Liquidations reuse the audited EngineHealthCard/CrankHealthCard/ LiquidationAnalytics; every figure goes through sanitizeOnChainValue. - Desktop-only (self-hides < lg); mobile keeps the utility-row link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the analytics dock. The hover panels were full-width and tall with dead space (Liquidations especially, which on v17 held only liq-fee + buffer + coverage). - Panels are now content-sized and left-anchored (not full-bleed): Capital and Fees ~268px, Liquidations ~320px, Health a single ~300px column. - Liquidations enriched with real risk params (maintenance/initial margin, max leverage, insurance) so it isn't sparse — still sanitized, no garbage. - Health stacks the reused Engine/Crank cards in one narrow column instead of two wide ones. - Tighter type/padding throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…panels Reworks the dock to merge with the site's own UI idioms: - Tabs are now a left-aligned SEGMENTED CONTROL (same pattern as the tokens/usd toggle and nav buttons): bordered well, active tab in bg-[var(--accent)]/15 text-[var(--accent-text)]. - Each panel is a bordered card with an uppercase section header + "full →" link — the same idiom as the /analytics Section wrapper — anchored directly above its tabs. - Thin 36px status-bar baseline; "Full analytics →" on the right. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per feedback: drop the segmented-control background well; render the tabs as plain inline text in the status bar with an accent-text + top-hairline active state (distinct from the header nav's filled-pill style). Tabs now sit on the RIGHT of the bar (where the full-page link was); the "Full analytics ↗" link moves to the left. Panel anchors right-aligned above the tabs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eipt - AnalyticsDock capital note: replace "stake → insurance → LP vault" with a plain sentence (the arrows read as a conversion/flow). - OrderTicket DiffRow: only render the struck-through "before → after" transition when `before` is a REAL prior value. Rows like Entry / Liq price / Fees / Slippage / Margin pass a placeholder "—", so they were rendering a meaningless "—̶ → value" that looked like a conversion; now they show just the value. "Available to trade" (real before) keeps its before → after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two leverage numbers confused users. Add the existing InfoIcon next to both labels (confirm dialog, position panel, portfolio) so hovering explains them, using the ready ORDER_LEVERAGE_TITLE / RISK_LEVERAGE_TITLE copy: - Order Lev. — the slider value used to size the trade. - Risk Lev. — the account's effective exposure (notional ÷ collateral in the slab account); extra collateral lowers it. Replaces the invisible native `title=` with a visible hover marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m utility row Per request — trims the trade page's top utility row to just the market address + admin status. Analytics now lives in the bottom AnalyticsDock, so the "Analytics →" link was redundant. Also drops the now-unused UsdToggleButton helper, the ShareButton import, and the useUsdToggle import (the UsdToggleProvider stays — MarketInfoBar/PositionsDock still read it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified against the v17 engine + wrapper + stake program source. The old
note ("losses hit the stake pool first, then insurance, then the LP vault")
was wrong: the engine has NO stake concept, and the stake pool is not in the
automatic loss path. Real order: position margin → insurance fund → LP/
winning positions (haircut/ADL). Stake (mode-0, tranche off) only pre-funds
insurance via a manual admin FlushToInsurance.
- "Total backing" now = LP vault + insurance only (the capital that actually
backs positions); no longer sums in the stake pool.
- Stake pool shown as a separate line labelled "pre-funds insurance", not a
loss tier.
- Note rewritten to the verified order.
Refs: percolator/src/v16.rs:12587,13694,8102,12336/12343 (waterfall);
percolator-stake/src/processor.rs:1494 (admin-only flush); tranche off by
default. On the deployed pool: trancheEnabled=false, totalRecoveredFromWrapper=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d hubs Perp-DEX-standard IA (Hyperliquid/dYdX/Drift all use one Portfolio hub). Cuts the scattered "your stuff" pages down to two tabbed hubs: - /portfolio hub — tabs: Overview (dashboard), Positions, Wallet, My Markets. - /earn hub — tabs: LP Vault, Stake. Each source page is composed as a tab (only the active tab mounts, so no extra data hooks). Current /portfolio (positions) extracted to components/portfolio/PortfolioPositionsView; current /earn extracted to components/earn/EarnVaultView. Tab bar matches the site (accent underline on active); tabs are hash-deep-linkable (/portfolio#wallet, /earn#stake). Header nav slimmed: Trade group = Markets/Portfolio/Earn; Build group = Create/Developers/Faucet (Dashboard/Wallet/My Markets/Stake are now tabs). Old routes (/dashboard /wallet /my-markets /stake) stay working standalone. Portfolio.test repointed to the extracted PortfolioPositionsView. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ty, Portfolio by wallet Per request: remove the Build dropdown and the Trade dropdown. Top-level nav is now flat links — Trade (terminal), Earn, Create a Market — plus the one remaining Community dropdown (Leaderboard, Developers, Faucet, absorbing the old Build utilities). Portfolio moves to the right cluster next to the wallet/connect button (it IS the account hub). Markets is no longer a top-level item (reached via the trade-page market switcher + /markets). Mobile menu + waitlist-host filtering updated to the new Menu/Community grouping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the "old vaults" bug and redesigns both Earn tabs to the terminal look. Data: useEarnStats no longer seeds its list from the hardcoded PLAYGROUND_SLAB_META (the July-10 SOL/JUP/TRUMP/PENGU/BURNIE born-immortal ghosts). It now sources the market list from the live /api/markets (same as /markets), unioned with user-launched registered markets, deduped, gated on a real on-chain LP Vault Registry read — so the LP Vault tab shows the same live markets as /markets with real TVL. PLAYGROUND_SLAB_META kept only as a symbol/logo fallback. Fixes a double "-PERP-PERP" ticker too. Design: loud "IN DEVELOPMENT" banners → compact honest inline NOTE (APY/APR genuinely 0%, deposits = counterparty backing / insurance funding). Sharp terminal styling on vault cards (clear Deposit/Withdraw CTA), the Stake hero toned down + accent-token button, page-level min-h-screen removed so it sits cleanly in the Earn hub tab (still renders standalone at /stake). tsc 0; 22 earn/stake unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, mirrors LP Vault The Stake page read like a marketing landing page (big "Stake. Earn. / Back the Fund." hero, DEPOSIT NOW / Learn More CTAs, centered layout). Rebuilt it as a dense, functional app page in the site's terminal idiom, a sibling of the LP Vault tab: - compact "// insurance lp" eyebrow + "Insurance Staking" title (no hero); - honest inline NOTE (APR genuinely 0%, flushes reduce staked value); - stats strip (Total Staked / Your Deposits / Active Pools / Avg APR); - 2-col main+sidebar: pool cards (VaultCard idiom) + Deposit/Withdraw panel + Your Positions in the main column; "What Staking Backs" + "How It Works" + Risk Notice sidebar (copy matches the verified loss-order: stake pre-funds insurance via admin flush); - sharp corners (rounded-md → rounded-sm/none), mono numbers, uppercase micro-labels, accent-only CTAs; compact empty states. All on-chain logic (deposit/withdraw, cooldown, positions, decodeStakePoolV1) untouched. Renders in the Earn hub tab and standalone at /stake; no min-h-screen. tsc 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OI was rendered ~600× too large on sub-cent markets. buildMarketVaultInfo did `total_open_interest / 10^decimals`, treating the raw base-asset "Q" quantity (scale 1e6) as USD — on Percolator ($0.00165) that showed ~$589K instead of the real ~$971. Now prefer the indexer's `total_open_interest_usd` (already notional USD), falling back to (raw / 1e6) × last_price. Fixes the OI figure + the OI-capacity utilization on the Earn page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p-dex layout) Thorough LAYOUT redesign, not copy tweaks. Both Earn-hub tabs now mirror the trade terminal (main content + right action rail), the shape Hyperliquid/GMX/ Drift use for vaults: - MAIN: a dense, scannable table (LP: Market · TVL · Utilization · Fee · Your Deposit; Stake: Pool · TVL · Cooldown · Your Stake · APR), selectable rows (accent left-border on the selected row), search + TVL/Volume/Util sort. - RIGHT RAIL (OrderTicket-analogue): clicking a row binds that vault/pool into a sticky Deposit/Withdraw rail with its key figures + your position. First row auto-selects so the rail is always bound. Deposit/withdraw logic reused unchanged (useInsuranceLP / DepositWidget / cooldown / partial withdraw). - Explanatory content (How It Works / Insurance / Risk) demoted from a tall competing sidebar to a slim secondary strip below the table. - The "in development" NOTE is removed on both; the honest 0% APY/APR is now a small muted caption. Sharp corners, mono numbers, uppercase micro-labels. New: VaultRow, VaultDepositRail. useInsuranceLP now sources slab context-first so the rail binds to the selected row (/earn/[slab] route path unaffected). tsc 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The indexer is now history-only (see percolator-indexer #189). Neutralize reads of dropped tables/columns so the API stops 500-ing against the new Supabase project; live OI/price/insurance are already read from chain on the pages. - funding_history: queryFundingHistory/queryFundingGlobal -> no-op [] (table dropped); /api/funding history + global degrade to empty. - insurance_snapshots: stake/pools computeAprs() -> 0 APR without querying (dropped). - Trimmed dropped market_stats columns (OI/insurance/vault/mark/index/funding etc.) from the SELECTs in markets/stats/prices/stake routes; kept last_price/volume_24h/ trade_count_24h + registry cols. trades/candles/leaderboard unchanged (kept tables). tsc clean. Points at the new PercolatorIndexer Supabase project (env set on Vercel). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…dentity
Two problems, both fallout from the indexer reduction.
1. The market list was EMPTY in production — /api/markets returned
{total:0, zombieCount:4} with four live markets in the DB.
isZombieMarket() keys off vault_balance, c_tot and total_accounts. The
reduction stopped mirroring those into market_stats (they're read live from
chain now) and SELECT_FIELDS stopped requesting them, so the keys are ABSENT
on the row. Every branch read absence as death: `vault_balance ?? null`
collapses undefined to null, `total_accounts ?? 0` to 0, and the null-vault
branch then returned true for everything.
Absent is not zero. isZombieMarket now declines to classify when none of the
three liveness signals was supplied, and both call sites (/api/markets and
/api/stats) forward undefined for columns the query never selected instead of
coercing them to null. Genuinely dead markets are still excluded upstream —
the indexer's sweep sets status=closed + indexer_excluded on dust-vault slabs
and both the view and the query filter those. The 56 existing zombie tests
still pass, so real detection is unchanged.
2. Six unrelated devnet markets all displayed as "SOL/USDC Perpetual".
v17 admin-oracle markets carry no on-chain pointer to a base asset — no Pyth
indexFeedId, no DEX pool, no mainnet CA — so the indexer's hyperp branch
always fell through to its hardcoded baseSymbol="SOL" default. That default
was the only branch that ever ran for them.
Market identity now lives in the database, as the one place it can:
- markets.metadata_source ('auto' | 'manual') marks who wrote symbol/name/
logo_url. The indexer only ever INSERTs missing slabs, so a human's edit is
never clobbered by a later discovery cycle.
- PATCH /api/markets/[slab] sets symbol/name/logo_url and flips the row to
'manual'. Authed with the same HMAC-SHA256 scheme as the keeper register
route: secret never crosses the wire, signed timestamp bounds replay, body
read as raw text and verified before parsing. Fails closed when the secret
is unset. Values are length-checked and stripped of control characters and
markup; logo_url goes through the same allowlist the launch flow uses, and
an explicit null clears it.
- markets_with_stats had to be dropped and recreated, not CREATE OR REPLACEd:
it selects m.*, which is expanded to a fixed column list at CREATE time, and
re-expanding it inserts metadata_source mid-list, which REPLACE rejects.
Done in a transaction with grants restored.
The indexer no longer guesses. It resolves the base asset via the DEX pool
when one exists — taking the base asset's logo, not the collateral's — and
otherwise writes a neutral UNKNOWN / "Market <prefix>" placeholder rather
than inventing SOL. A one-off migration resets the already-guessed rows,
under a predicate narrow enough that it cannot rename a market that really
is SOL (auto + no pool + no CA + no logo).
Verified against the live database: the guessed rows reset to placeholders, the
rebuilt view exposes metadata_source with grants intact, and it still returns
the four active markets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…it SHA The SDK was pinned to github:dcccrypto/percolator-sdk#497ec33. That commit is now published as @percolatorct/sdk@4.3.0 — the registry tarball's shasum matches the artifact built from that exact commit — so this is the same code by a more reliable route, not a version bump. Git dependencies are fetched and built on every install, which is why PLAYGROUND.md carried a standing "if pnpm install fails fetching the SDK, it's almost always a transient GitHub hiccup — just re-run" caveat and a matching troubleshooting row. Installing from the registry removes that failure mode, so both are gone rather than reworded. Docs updated only where they described the old mechanism (setup section, troubleshooting row, CLAUDE.md golden rule); nothing else touched. Verified: resolves from .pnpm/@percolatorct+sdk@4.3.0 rather than a git checkout, tsc clean, production build succeeds. The test suite is unchanged at 29 failing files / 92 failing tests — identical to the counts before this change, all in waitlist / Privy / chart-drawing / stake-hook areas that predate it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…and dead proxy
/api/markets had TWO alternative paths — an on-chain one that computed live
price and OI, and a Supabase one that did not — and the Supabase path wins
whenever the DB is configured. So configuring Supabase silently downgraded the
endpoint. Every symptom traced back to that: activeTotal: 0 with four live
markets, totalOpenInterest: 0 while /api/open-interest reported 588,928,150,764
from the same slab, and liveness heuristics reading absent columns as evidence
of death.
The fix is to stop branching. Registry data (identity, logo, 24h volume) comes
from Postgres because only Postgres has it; live state (price, OI, insurance,
vault, c_tot) comes from the chain because only the chain has it. One merged
row, one loader, shared by both routes.
1. lib/live-market-state.ts — batched live read
One getMultipleAccountsInfo per 100 slabs, using addresses the registry
already supplied. This REPLACES the getProgramAccounts LP-capital scan that
ran in the list request path: that call scans the entire program, so its cost
tracks protocol size rather than market count, and it is the first thing an
RPC provider throttles. Discovery of new markets belongs to the indexer,
which already does it.
vault_balance now carries the engine vault from the market-group header
rather than LP-portfolio capital. In this route it is only a liveness and
phantom-OI guard, never a rendered column, and the engine vault is the value
those guards were written against. The trade-page detail view keeps its own
single-market LP lookup, which is a different figure.
2. lib/market-registry.ts — one loader, shared
/api/stats used to HTTP-fetch /api/markets to stay consistent with it (they
had drifted badly enough to render "161 markets / $237K OI" beside pages
showing 6). Correct in result, but it costs a second serverless invocation
and its cold start on every stats request, and duplicates all the RPC work.
Sharing the loader gets the same guarantee structurally, in process.
3. Third instance of absent-is-not-zero, and the one that hid OI
isPhantomOpenInterest(accountsCount, vault) treats accountsCount === 0 as
phantom. total_accounts is no longer mirrored, and callers coerced the absent
value to 0 — asserting "this market definitely has no accounts". That made
EVERY market's OI phantom and zeroed it. Unknown now abstains; a KNOWN zero
still suppresses, and the dust-vault condition (which carries the guard now
that vault is read live) is untouched. Tests pin both directions.
4. Dead percolator-api proxies removed
/api/prices and /api/prices/[slab] called ${NEXT_PUBLIC_API_URL}/prices on a
host that answers "Application not found" — a guaranteed-failing round trip
on an endpoint useLivePrice polls every 10s per market. The Pyth and
GeckoTerminal fallbacks were already doing the work and are now the primary
path. The indexer that replaced that service is ingest-only: /health and the
Helius webhook, nothing else.
5. stale-while-revalidate 30s -> 60s
A true split by volatility would need separate identity/live endpoints; not
worth doubling client requests at this market count. Widening the revalidate
window is the lever that matters — inside it the CDN answers from cache and
refreshes in the background, so only the first request after expiry waits on
origin.
Verified against the live database via a local server on the production env,
diffed field-by-field against the deployed response: no key or market removed,
insurance_fund/insurance_balance added, activeTotal 0 -> 4, marketsWithPrice
0 -> 4, prices and OI populated. Percolator's merged OI is 588928150764 —
an exact match with the dedicated /api/open-interest endpoint reading the same
slab, which independently confirms the parse. Full suite unchanged at the
pre-existing 29 files / 92 tests failing, plus 7 new passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
Hides all six rows of the `markets` table from the API and UI: Percolator,
Jimothy, TROLL, the dead SOL slab, the empty BLAHwD5w slab, and ANSEM.
Blocklisted rather than deleted from the DB because neither DB lever holds.
StatsCollector.syncMarkets() re-inserts any on-chain market missing from the
table every COLLECT_INTERVAL_MS (60s) via insertMarketRow({status:"active"}),
so a DELETE reappears within a minute as an UNKNOWN placeholder, minus the
metadata it destroyed. indexer_excluded is self-reverting too: the auto-close
sweep flips it back to false for anything still in the discovery map with
vault > $1 or numUsedAccounts > 0 (StatsCollector.ts:901) — which covers the
one visible bad market, BPgSUbDs (vault 60,496,656,666). This blocklist is the
only lever independent of on-chain state.
Each entry records its verified condition. The two that were actually broken:
- Jimothy — LP vault bankrupt (capital $0.00, pnl -$2,479). Sole negative-PnL
account, so negative_pnl_account_count=1 pins bankruptcy_hlock_active=1,
try_clear_bankruptcy_hlock_if_healthy() can never fire, h_lock_lane returns
HMax, and every favorable action reverts Custom(21).
- SOL/USDC — asset0.slot_last 1,143,246 slots behind (~5.3 days), oracle_epoch=0,
no matcher-enabled LP portfolio so the recovery cranker has never cranked it.
Percolator and TROLL were healthy at time of writing and are retired with the
lineup rather than for a fault; Percolator's LP was draining ($240.35 ->
$215.91 in ~12 min), on the same trajectory as Jimothy.
Verified: tsc --noEmit clean; markets-slab-blocklist-symbol 10/10. The suite's
29 failing files / 92 failing tests are pre-existing and identical with and
without this change (confirmed by stashing it and re-running).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…ocklist instead 394f856 blocklisted the entire devnet-2.0 lineup. DEVNET_MARKET_DIRECTORY_FALLBACK is now a SINGLE entry and that entry is itself blocklisted, so the outage fallback legitimately returns zero rows on devnet — which broke this test's `markets.length > 0` premise. Dropping the emptiness assertion rather than propping it up: it was never the point, and it would re-break the moment the directory is repopulated. The filter's real contract — everything returned matches the requested program — is kept. To make sure removing it did not leave a vacuous test, added a guard that pins the behaviour that now actually matters: requesting the blocklisted entry's OWN program must still return nothing, proving the blocklist filter runs ahead of the program filter rather than the emptiness being incidental. Mutation-verified: disabling the fallback's BLOCKED_SLAB_ADDRESSES filter fails the new guard; restoring it passes. Note (not fixed here, it is a product call): during a Supabase outage the devnet fallback now serves zero markets. Flagged separately.
|
Updated for
I dropped the emptiness assertion rather than propping it up (it was never the point, and it would re-break the moment the directory is repopulated), kept the filter's real contract, and — to be sure I hadn't just made the test vacuous — added a guard for the behaviour that now actually matters: requesting the blocklisted entry's own program must still return nothing, proving the blocklist filter runs ahead of the program filter.
Full stack: 2820 passed, 0 failed. Separately, this PR's |
Fifth slice of the 65 failures #2450 exposed. Fixes 8, adds 10. Test files only — no product change.
Each suite asserted a contract its route had deliberately moved away from.
funding-global (4)
The route stopped being a pass-through proxy. It's now a three-tier chain — Railway proxy → local indexer Postgres → graceful empty — that intentionally does not surface upstream errors, so the funding UI degrades to "no data" instead of an error state when the backend is down. That's the live situation whenever Railway is dead (#2443).
I checked the history before touching it: the fallback landed with the playground trading terminal, for exactly that case. The product is right; the tests encoded the old contract.
They now assert the degradation — and cover the indexer-db fallback tier, which had no tests at all: it serves rows, applies the blocklist on that path too, and falls through to empty when the query itself fails. Also pins
Cache-Control: no-storeon the degraded response, since caching an empty result would keep the UI blank for the whole TTL after recovery.middleware-rate-limit (3)
These set
BLOCKED_MARKET_ADDRESSESand expected a 404.middleware.tsimports@/lib/blocklist-edge, which is edge-pure and intentionally omits env-var overrides — importing the env-reading@/lib/blocklistco-bundled Node-only code into the Edge chunk and Vercel's deploy validator rejected it (referencing unsupported modules).Rather than failing on that boundary, the tests now pin it:
That's a non-obvious operational fact that cost me a few minutes to reconstruct; it's now written down where the next person will hit it.
markets-supabase-outage-fallback (1)
The devnet static directory was migrated to the single v17 wrapper program, so filtering by an old per-slab-tier
program_idmatches 0 rows, not 3. Now asserts the filter's behaviour rather than a hardcoded count — plus the negative case, without which a no-op filter that returned everything would still pass.New: blocklist drift guard
blocklist-edge.tscarries aKEEP IN SYNC with HARDCODED_BLOCKED_SLABScomment and nothing enforced it. The two lists agree today (35 entries each, zero drift) — but drift would leave a slab blocked at one layer and reachable through the other, and entries there include a wrongoracle_authority(price-manipulation risk, GH#837).Includes a size assertion so emptying one set can't make the difference checks pass vacuously. Mutation-verified: removing one address from the edge list fails it.
Evidence
npx tsc --noEmitcleanStacks cleanly with #2451–#2455 — disjoint files, all based on
playground.Separate finding — not fixed here, deliberately
validateBlocklist()is never called in production. Its only callers are its own tests. Its docstring says to call it frominstrumentation.ts, andinstrumentation.tsis a no-opregister().It exists to warn when the legacy
BLOCKED_MARKET_ADDRESSESis set without theNEXT_PUBLIC_mirror — the GH#1539 drift. Right now that misconfiguration is silent.I did not bundle the fix here:
instrumentation.tshas a documented history of breaking Vercel deploys via Edge bundling (the comment in the file is about exactly that), so wiring it up needs a Node-runtime-guarded dynamic import and its own preview deploy to verify. Mixing a deploy-risky product change into a test-only PR would make both harder to review and revert. Happy to open it as a follow-up.🤖 Generated with Claude Code