Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/__tests__/lib/resolveMarketOracleMode.test.ts
Original file line number Diff line number Diff line change
@@ -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<MarketOracleMode>([
"pyth",
"admin",
"keeper",
])("does not alter %s mode", (requestedMode) => {
expect(
resolveMarketOracleMode({
requestedMode,
network: "mainnet",
hasMainnetCA: true,
}),
).toBe(requestedMode);
});
});
34 changes: 20 additions & 14 deletions app/hooks/useCreateMarket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions app/lib/resolveMarketOracleMode.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading