diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..425d458 --- /dev/null +++ b/.github/CODEOWNERS @@ -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 \ No newline at end of file diff --git a/.github/workflows/bindings-check.yml b/.github/workflows/bindings-check.yml index 3136666..2ddd272 100644 --- a/.github/workflows/bindings-check.yml +++ b/.github/workflows/bindings-check.yml @@ -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 diff --git a/apps/web/src/components/PasskeyPanel.tsx b/apps/web/src/components/PasskeyPanel.tsx index 215d0ca..9a3079a 100644 --- a/apps/web/src/components/PasskeyPanel.tsx +++ b/apps/web/src/components/PasskeyPanel.tsx @@ -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"; @@ -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)); } } @@ -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).`, @@ -233,10 +233,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); } } diff --git a/apps/web/src/hooks/useTraceHealth.ts b/apps/web/src/hooks/useTraceHealth.ts index a024ce5..aca1ecf 100644 --- a/apps/web/src/hooks/useTraceHealth.ts +++ b/apps/web/src/hooks/useTraceHealth.ts @@ -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 } @@ -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( diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index 3b44f34..114b0b5 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -3,6 +3,80 @@ import type { EscrowConservationPhase, EscrowConservationReport, } from "./conservation.js"; +import { Errors as RoundBindingsErrors } from "@sub-rosa/round-bindings"; + +export interface RoundContractErrorSpec { + code: number; + name: string; + retryable: boolean; +} + +const RETRYABLE_ERROR_NAMES = new Set([ + "CommitNotClosed", + "RevealNotOpen", + "RevealStillOpen", + "NotCleared", + "NotVoidable", +]); + +function buildRoundContractErrors(): RoundContractErrorSpec[] { + return Object.entries(RoundBindingsErrors) + .map(([codeStr, info]) => ({ + code: Number(codeStr), + name: info.message, + retryable: RETRYABLE_ERROR_NAMES.has(info.message), + })) + .sort((a, b) => a.code - b.code); +} + +export const ROUND_CONTRACT_ERRORS = buildRoundContractErrors(); + +export const ROUND_CONTRACT_ERRORS_BY_NAME: Record = Object.fromEntries( + ROUND_CONTRACT_ERRORS.map((e) => [e.name, e]), +); + +export function getRoundContractError(key: string | number): RoundContractErrorSpec | undefined { + if (typeof key === "number") { + return ROUND_CONTRACT_ERRORS.find((e) => e.code === key); + } + return ROUND_CONTRACT_ERRORS_BY_NAME[key]; +} + +export function isRoundContractErrorRetryable(key: string | number | undefined | null): boolean { + if (key === undefined || key === null) { + return false; + } + const spec = getRoundContractError(key); + return spec?.retryable ?? false; +} + +export function diffContractErrorMapping( + contractErrors: { code: number; name: string }[], +): string[] { + const failures: string[] = []; + const sdkByCode = new Map(ROUND_CONTRACT_ERRORS.map((e) => [e.code, e])); + const sdkByName = ROUND_CONTRACT_ERRORS_BY_NAME; + + for (const { code, name } of contractErrors) { + const sdk = sdkByCode.get(code); + if (!sdk) { + failures.push(`code ${code} (${name}) not mapped in SDK`); + continue; + } + if (sdk.name !== name) { + failures.push(`code ${code}: SDK has ${sdk.name}, contract has ${name}`); + } + } + + for (const sdk of ROUND_CONTRACT_ERRORS) { + const found = contractErrors.some((e) => e.code === sdk.code); + if (!found) { + failures.push(`SDK error ${sdk.name} (#${sdk.code}) missing from contract errors list`); + } + } + + return failures; +} export class SubRosaClientConfigError extends Error { readonly name = "SubRosaClientConfigError"; diff --git a/scripts/check-env-examples.mjs b/scripts/check-env-examples.mjs index e227bc9..69b154e 100644 --- a/scripts/check-env-examples.mjs +++ b/scripts/check-env-examples.mjs @@ -2,6 +2,9 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; +import { createLogger } from '../packages/logging/src/index.cjs'; + +const diagnostics = createLogger('scripts.check-env-examples'); const TEMPLATES = [ ['.env.example', new URL('../.env.example', import.meta.url)], @@ -61,7 +64,7 @@ export function checkTemplates(templates = TEMPLATES) { if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const errors = checkTemplates(); - for (const error of errors) console.error(error); + for (const error of errors) diagnostics.error('invalid-template', error); if (errors.length) process.exitCode = 1; - else console.log('Environment templates are valid.'); + else diagnostics.info('valid-templates', 'Environment templates are valid.'); }