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
16 changes: 8 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 @@ -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);
}
}
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
74 changes: 74 additions & 0 deletions packages/sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RoundContractErrorSpec> = 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";
Expand Down
7 changes: 5 additions & 2 deletions scripts/check-env-examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand Down Expand Up @@ -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.');
}
Loading