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
11 changes: 11 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Execution-sensitive paths require maintainer review to prevent unauthorized changes
# that could affect build integrity, runtime behavior, or asset delivery.

.github/workflows/** @karagozemin
.vscode/** @karagozemin
package.json @karagozemin
pnpm-lock.yaml @karagozemin
Cargo.toml @karagozemin
Cargo.lock @karagozemin
scripts/** @karagozemin
**/public/fonts/** @karagozemin
6 changes: 5 additions & 1 deletion .github/workflows/bindings-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ jobs:
- name: Install stellar CLI
# 22.8.2 is the latest stable stellar-cli that still supports soroban-sdk 22.
# CLI 23.0.0 and newer require soroban-sdk 23.
run: cargo install stellar-cli --version 22.8.2 --locked
# Compile with Rust 1.85.0 (stellar-cli 22.8.2 MSRV) because its locked
# ethnum 1.5.0 dependency fails with E0512 on newer stable rustc.
run: |
rustup toolchain install 1.85.0 --profile minimal
RUSTUP_TOOLCHAIN=1.85.0 cargo install stellar-cli --version 22.8.2 --locked

- name: Print Stellar CLI version
run: stellar --version
Expand Down
12 changes: 11 additions & 1 deletion apps/web/src/components/AuditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,17 @@ export function AuditorView({ trace }: { trace: DemoTrace }) {
const round = trace.meta.revealRound;
const nonce = generateNonce();
const value = 42_000_000n;
const sealed = await sealBid({ value, nonce, round, client });
const sealed = await sealBid({
value,
nonce,
round,
revealRound: round,
contractId: trace.meta.contractId,
bidderId:
trace.bidders[0]?.address ??
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
client,
});
const opened = await openBid(new Uint8Array(sealed.ciphertext), client);
if (opened.value !== value) throw new Error("opened value mismatch");
setBidDemo({ value: (Number(value) / 1e7).toFixed(2), round });
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/components/ObserverView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ test("live bid state without a revealed value stays sealed even post-R", () => {
trace={DEMO_TRACE}
live={{
round: {
asset_config: {
asset_type: "native",
code: "XLM",
contract_id: "",
decimals: 7,
issuer: "",
},
auditor_pubkey: Buffer.alloc(0),
bidders: DEMO_TRACE.bidders.map((b) => b.address),
clearing_rule: { tag: "HighestBid", values: undefined },
Expand All @@ -148,6 +155,10 @@ test("live bid state without a revealed value stays sealed even post-R", () => {
},
]),
),
phase: "Reveal",
revealCursor: { revealed: 0, total: DEMO_TRACE.bidders.length },
escrow: { committed: 0n, revealed: 0n },
sequence: 1,
polledAt: fake.clock.nowMs(),
}}
/>
Expand Down
17 changes: 9 additions & 8 deletions apps/web/src/components/PasskeyPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { publicErrorMessage } from "@sub-rosa/logging/errors";
import { normalizeError, publicErrorMessage } from "@sub-rosa/logging/errors";
// Copyright (c) 2026 Sub Rosa contributors
import { useMemo, useState } from "react";
import { SubRosaClient, validatePasskeySession } from "@sub-rosa/sdk";
Expand Down Expand Up @@ -131,8 +131,8 @@ export function PasskeyPanel({
);
} catch (e) {
setStatus("error");
const err = e instanceof Error ? e : new Error(String(e));
setPanelError(err);
const normalized = normalizeError(e);
setPanelError(new Error(normalized.message));
setMessage(publicErrorMessage(e));
}
}
Expand Down Expand Up @@ -173,8 +173,8 @@ export function PasskeyPanel({
setMessage(`Smart wallet deployed on testnet: ${created.contractId}`);
} catch (e) {
setStatus("error");
const err = e instanceof Error ? e : new Error(String(e));
setPanelError(err);
const normalized = normalizeError(e);
setPanelError(new Error(normalized.message));
const detail = publicErrorMessage(e);
setMessage(
`Deploy failed: ${detail}. Try again after refresh; if it persists, sponsor funding on testnet may be missing (Create passkey alone is enough for the demo).`,
Expand Down Expand Up @@ -222,6 +222,7 @@ export function PasskeyPanel({
commitment: new Uint8Array(32),
ciphertext: new Uint8Array([0x61, 0x67, 0x65]),
auditorBlob: new Uint8Array(1),
sealRound: 1,
},
escrow: 100_000n,
bidder: session.account,
Expand All @@ -233,10 +234,10 @@ export function PasskeyPanel({
setMessage("Passkey commit submitted successfully.");
} catch (e: unknown) {
setStatus("error");
const err = e instanceof Error ? e : new Error(String(e));
setPanelError(err);
const normalized = normalizeError(e);
setPanelError(new Error(normalized.message));
// Ensure fixture secret seed is kept strictly out of the error message
const scrubbed = err.message.replace(/\bS[A-Z2-7]{55}\b/g, "[REDACTED]");
const scrubbed = normalized.message.replace(/\bS[A-Z2-7]{55}\b/g, "[REDACTED]");
setMessage(scrubbed);
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/VerifiedSettlement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function receipt(overrides: Partial<RoundReceipt> = {}): RoundReceipt {
bidders: [W, L],
bids: { [W]: bid("750000000", true), [L]: bid("500000000", true, "900000000") },
winner: W, winningValue: "750000000", status: "Settled",
events: [],
...overrides,
};
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/demo/trace-health-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function checkBidders(
issues: string[],
): { labels: string[]; addresses: string[] } {
const bidders = requireNonEmptyArray(value, "bidders", issues);
if (!bidders) return [];
if (!bidders) return { labels: [], addresses: [] };

const labels: string[] = [];
const addresses: string[] = [];
Expand Down
19 changes: 16 additions & 3 deletions apps/web/src/hooks/useLiveRound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { publicErrorMessage } from "@sub-rosa/logging/errors";
import { useEffect, useRef, useState } from "react";
import type { Round, BidState } from "@sub-rosa/sdk";
import { useTime } from "../lib/time";
import { classifyRoundPhase, type RoundPhase } from "../lib/round-phase";
import {
classifyRoundPhase,
roundStatusFromTag,
type RoundPhase,
} from "../lib/round-phase";

import type { TimerHandle } from "@sub-rosa/time";

Expand Down Expand Up @@ -69,13 +73,22 @@ export function buildRoundSnapshot({
(sum, state) => sum + (state.revealed_value ?? 0n),
0n,
);
const rawStatus = (round as { status?: string | { tag?: string } }).status;
const statusTag =
typeof rawStatus === "string" ? rawStatus : rawStatus?.tag;
return {
round,
bidders,
bidStates,
phase: classifyRoundPhase({ status: round.status, drandPublished }),
phase: classifyRoundPhase({
status: roundStatusFromTag(statusTag),
drandPublished,
}),
revealCursor: { revealed, total: bidders.length },
escrow: { committed: round.escrow ?? 0n, revealed: revealedTotal },
escrow: {
committed: (round as { escrow?: bigint }).escrow ?? 0n,
revealed: revealedTotal,
},
sequence,
polledAt,
};
Expand Down
15 changes: 14 additions & 1 deletion apps/web/src/hooks/useRoundSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
LIVE_REVEAL_IN_SECONDS,
LIVE_REVEAL_WINDOW_AFTER_REVEAL_SECONDS,
NETWORK,
PUBLIC_ENV,
detectChainNetworkPassphrase,
displayError,
formatDemoAmount,
Expand Down Expand Up @@ -273,6 +274,13 @@ export function useRoundSession(active: UseCase) {
commit_deadline: BigInt(commitDeadline),
reveal_deadline: BigInt(revealDeadline),
auditor_pubkey: Buffer.from(auditor.publicKey),
asset_config: {
asset_type: "native",
code: "XLM",
contract_id: "",
decimals: 7,
issuer: "",
},
});
const sent = await tx.signAndSend();
const nextRoundId = sent.result.unwrap() as bigint;
Expand Down Expand Up @@ -371,10 +379,14 @@ export function useRoundSession(active: UseCase) {
const roundAuditorPublicKey = new Uint8Array(round.auditor_pubkey);
const value = toDemoEscrowAmount(entryValue);
const drand = quicknet();
const revealRound = Number(round.reveal_round);
const sealed = await sealBid({
value,
nonce: generateNonce(),
round: Number(round.reveal_round),
round: revealRound,
revealRound,
contractId: CONTRACT_ID ?? "",
bidderId: address,
client: drand,
identity: new TextEncoder().encode(`${active.nav}:${address}`),
auditorPublicKey: auditorPublicKey ?? roundAuditorPublicKey,
Expand All @@ -386,6 +398,7 @@ export function useRoundSession(active: UseCase) {
ciphertext: Buffer.from(sealed.ciphertext),
escrow: value,
auditor_blob: Buffer.from(sealed.auditorBlob),
seal_round: BigInt(sealed.sealRound),
});
await tx.signAndSend();
updateSession(id, {
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/hooks/useTraceHealth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from "react";
import { DEMO_TRACE } from "../demo/trace";
import { assertDemoTrace } from "../demo/trace-health-check";
import { verifyMilestones } from "../demo/demo-trace.checksum";
import { normalizeError } from "@sub-rosa/logging/errors";

export type TraceHealthState =
| { ok: true }
Expand All @@ -24,8 +25,8 @@ export function useTraceHealth(): TraceHealthState {
try {
assertDemoTrace(DEMO_TRACE);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { ok: false, errorCode: `TRACE_INVALID: ${message}` };
const normalized = normalizeError(e);
return { ok: false, errorCode: `TRACE_INVALID: ${normalized.message}` };
}

const phases = (DEMO_TRACE as { lifecycle: Array<{ phase: string }> }).lifecycle.map(
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import { configuredNetworkPassphrase, publicEnv } from "./config";

const env = publicEnv();

export const PUBLIC_ENV = env;
export const LOGO_SRC = "/sub-rosa-logo.png";
export const RPC_URL = env.VITE_RPC_URL ?? "https://soroban-testnet.stellar.org";
export const NETWORK = configuredNetworkPassphrase(env);
export const CONTRACT_ID = env.VITE_CONTRACT_ID;
export const SDK_CLIENT_IDENTITY = CONTRACT_ID
? { contractId: CONTRACT_ID, networkPassphrase: NETWORK }
: null;
export const ESCROW_TOKEN_LABEL = env.VITE_ESCROW_TOKEN_LABEL ?? "token";
export const DEFAULT_ROUND_ID = env.VITE_ROUND_ID ? BigInt(env.VITE_ROUND_ID) : null;

Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/lib/demoActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,34 @@ import { systemClock } from "@sub-rosa/time";
import { DEMO_ACTIONS, type DemoAction, type DemoActionGate } from "./config";

export type { AttackStep, CapDemoResult };
export type { DemoAction, DemoActionGate };

export class DemoActionBlockedError extends Error {
readonly name = "DemoActionBlockedError";
readonly action: DemoAction;

constructor(action: DemoAction) {
super(`Demo ${action} is disabled: public config does not match the SDK client.`);
this.action = action;
}
}

/** Which demo actions may submit; all-or-nothing on the config gate. */
export function demoActionAvailability(
gate: DemoActionGate,
): Record<DemoAction, boolean> {
return Object.fromEntries(
DEMO_ACTIONS.map((action) => [action, gate.enabled]),
) as Record<DemoAction, boolean>;
}

/** Call before any demo commit, reveal or settle submission. */
export function assertDemoActionAllowed(
action: DemoAction,
gate: DemoActionGate,
): void {
if (!gate.enabled) throw new DemoActionBlockedError(action);
}

// Demo actions must never submit a commit/reveal/settle into the wrong
// network. Re-export the guard so demo flows share one implementation.
Expand Down Expand Up @@ -185,6 +213,9 @@ export async function runSealAttackDemo(): Promise<{
value,
nonce,
round: revealRound,
revealRound,
contractId: "CDEMO",
bidderId: "GDEMO",
client,
});
sealOn.push({
Expand Down
27 changes: 25 additions & 2 deletions apps/web/src/lib/round-phase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Copyright (c) 2026 Sub Rosa contributors
import {
ROUND_PHASE_LABELS,
type RoundPhase,
type RoundStatus as SdkRoundStatus,
roundPhaseLabel,
} from "@sub-rosa/sdk";
import type { RoundStatus as DashboardRoundStatus } from "../dashboard/types";
import type { RoundStatus as SdkRoundStatus } from "@sub-rosa/sdk";

// Re-export the shared phase vocabulary so the UI cannot invent a fourth phase
// name that drifts from `packages/sdk/src/round-status.ts`.
Expand All @@ -13,7 +18,7 @@ export interface ClassifyRoundPhaseInput {
}

/** Status tags the shared phase helper accepts; anything else is sealed. */
const KNOWN_ROUND_STATUSES: readonly RoundStatus[] = [
const KNOWN_ROUND_STATUSES: readonly DashboardRoundStatus[] = [
"Open",
"Revealing",
"Cleared",
Expand All @@ -32,6 +37,24 @@ export function classifyRoundPhase({
return "Open";
}

/** True once the shared phase helper says bid values are public. */
export function isRevealPhase(phase: RoundPhase): boolean {
return phase !== "Open";
}

/**
* Normalize an on-chain or recorded status tag into a `RoundStatus` the shared
* phase helper accepts. Unknown tags classify conservatively as "Open" so a
* sealed value can never be formatted from an unrecognized status.
*/
export function roundStatusFromTag(
tag: string | null | undefined,
): DashboardRoundStatus {
return (KNOWN_ROUND_STATUSES as readonly string[]).includes(tag ?? "")
? (tag as DashboardRoundStatus)
: "Open";
}

// ---------------------------------------------------------------------------
// Lifecycle step derivation — driven by the SDK RoundStatus only.
// ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/pages/DemoPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ function EvidencePanelTestDouble({ errorCode }: { errorCode?: string }) {

// When healthy, render a stand-in that includes trace bid amounts so we
// can assert they ARE visible when the trace is valid.
const bidAmounts = (DEMO_TRACE as { bidders: Array<{ bidUsdc: number | null }> }).bidders
const bidAmounts = (
DEMO_TRACE as { bidders: ReadonlyArray<{ bidUsdc: number | null }> }
).bidders
.map((b) => b.bidUsdc)
.filter((v): v is number => v !== null);

Expand Down
Loading