Skip to content
Merged
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
77 changes: 77 additions & 0 deletions app/__tests__/components/launch-backing-seed-warning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* GH#2514 — a failed backing-domain seeding must not produce a silent
* "Market created!".
*
* The sequential launch path (retry/resume with startStep <= 3, or the
* pre-broadcast fallback from fresh batching) wraps the TopUpBackingBucket
* transaction in a try/catch, warns to the console, and continues. Staying
* non-fatal is deliberate and correct — a transient RPC error must not strand
* an otherwise-live market, and a repeat TopUp against an already-Fresh-at-MAX
* bucket is a harmless no-op.
*
* Staying SILENT is the defect. That rationale was written when the seed was
* dust; it is now `backingSeedPerDomain(lp)` per domain — 100% of LP collateral
* each at the current policy — so swallowing the failure hands the creator a
* success screen for a market missing two allocations worth twice their LP.
*
* These assertions are against the source, not a rendered component: the
* property is "the catch records it and the success screen reads it", which is
* a wiring fact spanning three files. A render test of LaunchSuccess alone
* would pass even if the hook never set the flag.
*/

import { describe, it, expect } from "vitest";
import * as fs from "fs";
import * as path from "path";

const read = (rel: string) =>
fs.readFileSync(path.resolve(__dirname, "../..", rel), "utf8");

const HOOK = read("hooks/useCreateMarket.ts");
const SUCCESS = read("components/create/LaunchSuccess.tsx");
const WIZARD = read("components/create/CreateMarketWizard.tsx");

describe("GH#2514: backing-seed failure is recorded, not swallowed", () => {
it("the backing-bucket catch sets backingSeedFailed", () => {
// Isolate the catch block so a match elsewhere in the file cannot satisfy
// this — the flag has to be set at the failure site itself.
const start = HOOK.indexOf("} catch (backingBucketErr) {");
expect(start).toBeGreaterThan(-1);
const block = HOOK.slice(start, start + 1400);
expect(block).toMatch(/backingSeedFailed:\s*true/);
});

it("keeps the step non-fatal — it must not throw or set the fatal error", () => {
// The fix is "report it", not "fail the launch". Turning this fatal would
// reintroduce exactly the stranding the original comment guards against.
const start = HOOK.indexOf("} catch (backingBucketErr) {");
const block = HOOK.slice(start, start + 1400);
expect(block).not.toMatch(/throw\s/);
expect(block).not.toMatch(/error:\s*[`'"]/);
});

it("declares backingSeedFailed on the state and initialises it false", () => {
expect(HOOK).toMatch(/backingSeedFailed:\s*boolean;/);
// Every initial-state object must carry it, or the success screen reads
// undefined and silently renders nothing.
const inits = HOOK.match(/insuranceMintFailed:\s*false,/g) ?? [];
const flags = HOOK.match(/backingSeedFailed:\s*false,/g) ?? [];
expect(inits.length).toBeGreaterThan(0);
expect(flags.length).toBe(inits.length);
});

it("the wizard passes it to LaunchSuccess and the screen renders on it", () => {
expect(WIZARD).toMatch(/backingSeedFailed=\{createState\.backingSeedFailed\}/);
expect(SUCCESS).toMatch(/backingSeedFailed\?:\s*boolean;/);
expect(SUCCESS).toMatch(/\{backingSeedFailed\s*&&\s*\(/);
});

it("the warning says the market is live AND that backing is missing", () => {
// Both halves matter: dropping the first turns a soft warning into an
// apparent failure, dropping the second is the bug this closes.
const start = SUCCESS.indexOf("{backingSeedFailed && (");
const banner = SUCCESS.slice(start, start + 900);
expect(banner).toMatch(/live and tradeable/i);
expect(banner).toMatch(/not seeded/i);
});
});
1 change: 1 addition & 0 deletions app/components/create/CreateMarketWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ export const CreateMarketWizard: FC<{ initialMint?: string }> = ({ initialMint }
devnetAirdropSymbol={createState.devnetAirdropSymbol}
devnetMintError={createState.devnetMintError}
insuranceMintFailed={createState.insuranceMintFailed}
backingSeedFailed={createState.backingSeedFailed}
keeperDelegated={createState.keeperDelegated}
keeperMessage={createState.keeperMessage}
keeperRegistering={createState.keeperRegistering}
Expand Down
16 changes: 16 additions & 0 deletions app/components/create/LaunchSuccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface LaunchSuccessProps {
* Shows a soft warning on the success screen; does not block trading.
*/
insuranceMintFailed?: boolean;
/** GH#2514: backing-domain seeding failed (non-fatal, but must not be silent). */
backingSeedFailed?: boolean;
/** Keeper oracle: true when oracle_authority was delegated to the keeper service */
keeperDelegated?: boolean;
/** Keeper registration message */
Expand Down Expand Up @@ -63,6 +65,7 @@ export const LaunchSuccess: FC<LaunchSuccessProps> = ({
devnetAirdropSymbol,
devnetMintError,
insuranceMintFailed,
backingSeedFailed,
keeperDelegated,
keeperMessage,
keeperRegistering,
Expand Down Expand Up @@ -211,6 +214,19 @@ export const LaunchSuccess: FC<LaunchSuccessProps> = ({
</div>
</div>

{/* GH#2514: backing-domain seeding failed. Non-fatal by design — a transient
RPC error must not strand a live market — but it must not be silent
either: at the current policy each domain's seed is 100% of LP
collateral, so an unreported failure leaves the creator believing a
market is seeded when it is short twice their LP. */}
{backingSeedFailed && (
<div className="border border-[var(--warning)]/20 bg-[var(--warning)]/[0.04] px-4 py-2 mb-4 text-left w-full max-w-sm mx-auto">
<p className="text-[11px] text-[var(--text-secondary)]">
Market is <strong className="text-[var(--text)]">live and tradeable</strong>, but counterparty backing was <strong className="text-[var(--text)]">not seeded</strong> — the deposit for both domains did not land. Retry it from market settings before the market takes size.
</p>
</div>
)}

{/* GH#1761: Insurance LP Mint soft warning — shown when step 5 failed non-fatally */}
{insuranceMintFailed && (
<div className="border border-[var(--warning)]/20 bg-[var(--warning)]/[0.04] px-4 py-2 mb-4 text-left w-full max-w-sm mx-auto">
Expand Down
28 changes: 28 additions & 0 deletions app/hooks/useCreateMarket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,24 @@ export interface CreateMarketState {
* step). Kept for backwards-compatible UI wiring; never set to true by create().
*/
insuranceMintFailed: boolean;
/**
* GH#2514: set when the sequential path's backing-bucket seeding failed.
*
* That step is deliberately non-fatal (see the Step 3 comment in create()) so
* a transient RPC error cannot strand an otherwise-live market. But it was
* ALSO silent: the launch went on to report an unqualified "Market created!"
* while both backing domains were left unseeded.
*
* That was defensible when the seed was dust. It is not now the seed is
* `backingSeedPerDomain(lp)` per domain — 100% of LP collateral each at the
* current policy, so a silent failure leaves the creator's market short two
* allocations totalling twice their LP.
*
* Non-fatal is kept; silent is not. The launch still succeeds, and the
* success screen says what did not happen so the creator can retry or
* backfill rather than believing the market is fully seeded.
*/
backingSeedFailed: boolean;
/** Keeper oracle mode: true when oracle_authority was delegated to keeper */
keeperDelegated: boolean;
/** Keeper registration result message */
Expand Down Expand Up @@ -1532,6 +1550,7 @@ export function useCreateMarket() {
devnetAirdropSymbol: null,
devnetMintError: null,
insuranceMintFailed: false,
backingSeedFailed: false,
keeperDelegated: false,
keeperMessage: null,
keeperRegistering: false,
Expand Down Expand Up @@ -2903,6 +2922,14 @@ export function useCreateMarket() {
"deadlock until this is retried or backfilled:",
backingBucketErr,
);
// GH#2514: staying non-fatal is right — a transient RPC error must
// not strand a live market, and a repeat TopUp against an
// already-Fresh-at-MAX bucket is a harmless no-op. Staying SILENT
// is not. The rationale above was written when this seed was dust;
// it is now backingSeedPerDomain(lp) per domain, so swallowing the
// failure hands the creator a "Market created!" for a market
// missing two allocations worth twice their LP collateral.
setState((s) => ({ ...s, backingSeedFailed: true }));
}
}

Expand Down Expand Up @@ -3399,6 +3426,7 @@ export function useCreateMarket() {
devnetAirdropSymbol: null,
devnetMintError: null,
insuranceMintFailed: false,
backingSeedFailed: false,
keeperDelegated: false,
keeperMessage: null,
keeperRegistering: false,
Expand Down
Loading