From 50c9d3aeb1a0338bfa7bb711cef4241a5dc507ce Mon Sep 17 00:00:00 2001 From: Bayyan16 Date: Fri, 17 Jul 2026 11:18:55 +0700 Subject: [PATCH] fix(create-market): preserve Hyperp oracle mode on mainnet --- .../lib/resolveMarketOracleMode.test.ts | 51 +++++++++++++++++++ app/hooks/useCreateMarket.ts | 34 ++++++++----- app/lib/resolveMarketOracleMode.ts | 39 ++++++++++++++ 3 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 app/__tests__/lib/resolveMarketOracleMode.test.ts create mode 100644 app/lib/resolveMarketOracleMode.ts diff --git a/app/__tests__/lib/resolveMarketOracleMode.test.ts b/app/__tests__/lib/resolveMarketOracleMode.test.ts new file mode 100644 index 00000000..4dcfa4c5 --- /dev/null +++ b/app/__tests__/lib/resolveMarketOracleMode.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + resolveMarketOracleMode, + type MarketOracleMode, +} from "../../lib/resolveMarketOracleMode"; + +describe("resolveMarketOracleMode", () => { + it("preserves Hyperp mode on mainnet when mainnetCA is present", () => { + expect( + resolveMarketOracleMode({ + requestedMode: "hyperp", + network: "mainnet", + hasMainnetCA: true, + }), + ).toBe("hyperp"); + }); + + it("falls back from Hyperp to Admin for a devnet mirror", () => { + expect( + resolveMarketOracleMode({ + requestedMode: "hyperp", + network: "devnet", + hasMainnetCA: true, + }), + ).toBe("admin"); + }); + + it("preserves Hyperp mode on devnet when mainnetCA is absent", () => { + expect( + resolveMarketOracleMode({ + requestedMode: "hyperp", + network: "devnet", + hasMainnetCA: false, + }), + ).toBe("hyperp"); + }); + + it.each([ + "pyth", + "admin", + "keeper", + ])("does not alter %s mode", (requestedMode) => { + expect( + resolveMarketOracleMode({ + requestedMode, + network: "mainnet", + hasMainnetCA: true, + }), + ).toBe(requestedMode); + }); +}); diff --git a/app/hooks/useCreateMarket.ts b/app/hooks/useCreateMarket.ts index ead4dda6..0d90dadb 100644 --- a/app/hooks/useCreateMarket.ts +++ b/app/hooks/useCreateMarket.ts @@ -83,6 +83,7 @@ import { checkSignatureLanded, } from "@/lib/tx"; import { getConfig, getNetwork } from "@/lib/config"; +import { resolveMarketOracleMode } from "@/lib/resolveMarketOracleMode"; import { normalizeDexType } from "@/lib/dex-type"; import { parseMarketCreationError } from "@/lib/parseMarketError"; import { @@ -1373,23 +1374,28 @@ export function useCreateMarket() { // - "pyth": index_feed_id = pyth hex, uses KeeperCrank with Pyth PDA // - "hyperp": index_feed_id = zeros, uses UpdateHyperpMark (reads DEX pool directly) // - "admin": index_feed_id = zeros, uses PushOraclePrice + KeeperCrank - // PERC-470 devnet guard: Hyperp mode reads live DEX pool accounts on-chain. - // On devnet, mirror tokens have no PumpSwap pool — mainnet pool addresses are invalid. - // Force admin oracle mode for all devnet mirror markets (params.mainnetCA is set). - const isDevnetMirror = !!params.mainnetCA; - const resolvedOracleMode = params.oracleMode ?? (params.oracleFeed === ALL_ZEROS_FEED ? "admin" : "pyth"); - // "keeper" mode: AUTH_MARK oracle with oracle_authority delegated to our keeper service. - // On devnet mirrors, hyperp falls back to admin; keeper stays keeper (it's designed for devnet). - const oracleMode: "pyth" | "hyperp" | "admin" | "keeper" = (resolvedOracleMode === "hyperp" && isDevnetMirror) ? "admin" : resolvedOracleMode as "pyth" | "hyperp" | "admin" | "keeper"; + // PERC-devnet: detect the runtime network instead of inferring it from + // params.mainnetCA. mainnetCA identifies mirrored-token metadata and is + // populated for mainnet market creation as well. + const network = getNetwork(); + const isDevnetEnv = network === "devnet"; + + const resolvedOracleMode = + params.oracleMode ?? + (params.oracleFeed === ALL_ZEROS_FEED ? "admin" : "pyth"); + + // Hyperp reads live DEX pool accounts on-chain. Mainnet pool accounts are + // unavailable for devnet mirrors, so only that exact combination falls + // back to Admin. Mainnet Hyperp must remain Hyperp. + const oracleMode = resolveMarketOracleMode({ + requestedMode: resolvedOracleMode, + network, + hasMainnetCA: !!params.mainnetCA, + }); + const isAdminOracle = oracleMode === "admin"; const isHyperpOracle = oracleMode === "hyperp"; const isKeeperOracle = oracleMode === "keeper"; - // PERC-devnet: isDevnetEnv must be runtime-detected, not build-time. - // Users toggle devnet via localStorage — NEXT_PUBLIC_DEFAULT_NETWORK is always "mainnet" on Vercel prod. - // Use getNetwork() which reads localStorage("percolator-network") first, then env var, then defaults - // to "mainnet" (fail-closed). DO NOT use params.mainnetCA as a devnet proxy — it signals - // "this is a devnet mirror market" not "the user is connected to devnet" (issue #835). - const isDevnetEnv = getNetwork() === "devnet"; // PERC-470: Resolve DEX pool vault addresses for hyperp mode // If vaults weren't provided, fetch the pool account on-chain diff --git a/app/lib/resolveMarketOracleMode.ts b/app/lib/resolveMarketOracleMode.ts new file mode 100644 index 00000000..fd1bb77d --- /dev/null +++ b/app/lib/resolveMarketOracleMode.ts @@ -0,0 +1,39 @@ +export type MarketOracleMode = + | "pyth" + | "hyperp" + | "admin" + | "keeper"; + +export type MarketNetwork = "mainnet" | "devnet"; + +export interface ResolveMarketOracleModeInput { + requestedMode: MarketOracleMode; + network: MarketNetwork; + hasMainnetCA: boolean; +} + +/** + * Resolve the effective oracle mode for market creation. + * + * Hyperp may fall back to Admin only for devnet mirror markets, + * because mainnet DEX pool accounts are unavailable on devnet. + * + * mainnetCA alone is token metadata and must not be treated as + * proof that the current runtime network is devnet. + */ +export function resolveMarketOracleMode( + input: ResolveMarketOracleModeInput, +): MarketOracleMode { + const isDevnetMirror = + input.network === "devnet" && + input.hasMainnetCA; + + if ( + input.requestedMode === "hyperp" && + isDevnetMirror + ) { + return "admin"; + } + + return input.requestedMode; +}