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
46 changes: 46 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "npm"
groups:
# Group patch and minor updates together
"npm-and-minor":
update-types: ["patch", "minor"]
package-management-puppet: true

# Enable version updates for Cargo
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "cargo"
groups:
# Group patch and minor updates together
"cargo-and-minor":
update-types: ["patch", "minor"]
package-management-puppet: true

# Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
groups:
# Group patch and minor updates together
"github-actions-and-minor":
update-types: ["patch", "minor"]
package-management-puppet: true
14 changes: 9 additions & 5 deletions apps/web/src/components/PasskeyPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { publicErrorMessage } from "@sub-rosa/logging/errors";
import {
normalizeError,
publicErrorMessage,
type NormalizedError,
} 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 @@ -60,7 +64,7 @@ export function PasskeyPanel({
contractId?: string;
} | null>(null);
const [session, setSession] = useState<PasskeySession | null>(initialSession);
const [panelError, setPanelError] = useState<Error | null>(null);
const [panelError, setPanelError] = useState<NormalizedError | null>(null);

const walletWasmHash = resolvePasskeyWalletWasmHash();
const rpId = import.meta.env.VITE_PASSKEY_RP_ID ?? window.location.hostname;
Expand Down Expand Up @@ -131,7 +135,7 @@ export function PasskeyPanel({
);
} catch (e) {
setStatus("error");
const err = e instanceof Error ? e : new Error(String(e));
const err = normalizeError(e);
setPanelError(err);
setMessage(publicErrorMessage(e));
}
Expand Down Expand Up @@ -173,7 +177,7 @@ 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));
const err = normalizeError(e);
setPanelError(err);
const detail = publicErrorMessage(e);
setMessage(
Expand Down Expand Up @@ -233,7 +237,7 @@ export function PasskeyPanel({
setMessage("Passkey commit submitted successfully.");
} catch (e: unknown) {
setStatus("error");
const err = e instanceof Error ? e : new Error(String(e));
const err = normalizeError(e);
setPanelError(err);
// 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]");
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/hooks/useTraceHealth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) 2026 Sub Rosa contributors
import { useMemo } from "react";
import { normalizeError } from "@sub-rosa/logging/errors";
import { DEMO_TRACE } from "../demo/trace";
import { assertDemoTrace } from "../demo/trace-health-check";
import { verifyMilestones } from "../demo/demo-trace.checksum";
Expand All @@ -24,7 +25,7 @@ export function useTraceHealth(): TraceHealthState {
try {
assertDemoTrace(DEMO_TRACE);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
const { message } = normalizeError(e);
return { ok: false, errorCode: `TRACE_INVALID: ${message}` };
}

Expand Down
18 changes: 14 additions & 4 deletions contracts/round/ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ stay in sync with `contracts/round/src/types.rs`.
| 1–4 | Initialization & state lookup |
| 10–22 | Lifecycle & timing |
| 30–39 | Cryptography & validation |
| 40–49 | Escrow accounting |
| 40–42 | Seal-round window |
| 43–49 | Escrow accounting |

## Initialization & state lookup (1–4)

Expand Down Expand Up @@ -57,7 +58,7 @@ stay in sync with `contracts/round/src/types.rs`.
| 21 | `NotVoidable` | `void` | Round is past the `Open` status, or `now <= reveal_deadline + VOID_GRACE` (3600 s). | The round cannot be voided from its current state, or the grace window has not elapsed yet. | Either complete the normal lifecycle, or wait until `reveal_deadline + 1 hour` and try `void` again. |
| 22 | `WrongStatus` | `commit` | `round.status != Status::Open`. | A bid can only be submitted to a round in the Open status. | Start a new round; a Revealing/Cleared/Settled/Voided round no longer accepts commits. |

## Cryptography & validation (30–40)
## Cryptography & validation (30–39)

| Code | Variant | Raised by | Trigger | User-facing message | Suggested next action |
| ---: | --- | --- | --- | --- | --- |
Expand All @@ -71,11 +72,20 @@ stay in sync with `contracts/round/src/types.rs`.
| 37 | `NoValidBids` | `settle` | `round.winner` is `None` on a round whose status is `Cleared`. | Round has no winner to settle against. | Investigate: under current behavior the contract transitions to `Voided` (with all escrow refunded) when no valid bid is revealed, so this code should not appear in normal flow. If it does, the round is in an inconsistent state and warrants a manual review. |
| 38 | `RoundFull` | `commit` | `round.bidders.len() >= MAX_BIDDERS` (500). | The round has reached its bidder cap. | Start a new round to accept further bidders. |
| 39 | `InvalidLimit` | `get_bidders_page` | `limit == 0` or `limit > 100`. | Page size must be between 1 and 100 (inclusive). | Pass a `limit` in `[1, 100]`; use `next_cursor` from the previous page to walk larger rounds. |

## Seal-round window (40–42)

A committed seal must name the Drand round the auction committed to open —
exactly `Round::reveal_round`, never ±1 or 0 (issue #376). The window is
enforced before any escrow is locked, so a rejected seal moves no funds.

| Code | Variant | Raised by | Trigger | User-facing message | Suggested next action |
| ---: | --- | --- | --- | --- | --- |
| 40 | `SealRoundTooEarly` | `commit` | `seal_round < round.reveal_round`: the seal names a Drand round before the one the auction committed to open. | The sealed bid is locked to the wrong Drand round. | Reseal the bid to the round published in the `created` event (`reveal_round`) and commit again; no escrow was moved. |
| 41 | `SealRoundTooLate` | `commit` | `seal_round > round.reveal_round`: the seal names a Drand round after the one the auction committed to open. | The sealed bid is locked to the wrong Drand round. | Reseal the bid to the round published in the `created` event (`reveal_round`) and commit again; no escrow was moved. |
| 42 | `InvalidSealRoundZero` | `create_round`, `commit` | `reveal_round == 0` at round creation, or `seal_round == 0` at commit. A zero or overflowing round can never be published by the Drand chain. | The Drand round number is malformed. | Use a positive round the quicknet chain can actually publish (`genesis + period × R` must fit in `u64`); check before locking escrow. |

## Escrow accounting (40–49)
## Escrow accounting (43–49)

Escrow conservation is one predicate, enforced by `reveal`, `clear`, `void`,
and `settle`: committed escrow equals the settled payout plus refunds plus the
Expand All @@ -84,7 +94,7 @@ the round's bidder index.

| Code | Variant | Raised by | Trigger | User-facing message | Suggested next action |
| ---: | --- | --- | --- | --- | --- |
| 40 | `EscrowNotConserved` | `reveal`, `clear`, `settle`, `void` | The round's escrow ledger does not satisfy `committed == payout + refunds + locked`; or the escrow it reports locked is not matched by indexed, unsettled bids; or a planned payout plus refunds would not drain exactly the locked balance. Concretely: a bidder index that drifted from the escrowed set (dropped, duplicated, or phantom bidder), a payout above the winner's escrow, a bid already marked settled, or a locked balance that survives a terminal round. | The round's escrow cannot be reconciled with the bids it was taken against, so no funds were moved. | Do not retry — the round's state is inconsistent. Export a receipt (`exportReceipt` / `getRound` + `getBidState` per bidder) and compare the bidder index against the escrowed set. If the index is intact, re-simulate from a fresh ledger view before escalating. |
| 43 | `EscrowNotConserved` | `reveal`, `clear`, `settle`, `void` | The round's escrow ledger does not satisfy `committed == payout + refunds + locked`; or the escrow it reports locked is not matched by indexed, unsettled bids; or a planned payout plus refunds would not drain exactly the locked balance. Concretely: a bidder index that drifted from the escrowed set (dropped, duplicated, or phantom bidder), a payout above the winner's escrow, a bid already marked settled, or a locked balance that survives a terminal round. | The round's escrow cannot be reconciled with the bids it was taken against, so no funds were moved. | Do not retry — the round's state is inconsistent. Export a receipt (`exportReceipt` / `getRound` + `getBidState` per bidder) and compare the bidder index against the escrowed set. If the index is intact, re-simulate from a fresh ledger view before escalating. |

Because the check runs before and after the transfers inside a single
invocation, a rejection reverts every transfer the call had already made: a
Expand Down
5 changes: 4 additions & 1 deletion contracts/round/src/error_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const ERROR_PATH_REGISTRY: &[(Error, &'static str)] = &[
(Error::NoValidBids, "error_path_no_valid_bids"),
(Error::RoundFull, "error_path_round_full"),
(Error::InvalidLimit, "error_path_invalid_limit"),
(Error::SealRoundTooEarly, "error_path_seal_round_too_early"),
(Error::SealRoundTooLate, "error_path_seal_round_too_late"),
(Error::InvalidSealRoundZero, "error_path_invalid_seal_round_zero"),
(Error::EscrowNotConserved, "error_path_escrow_not_conserved"),
];

Expand Down Expand Up @@ -88,7 +91,7 @@ fn settle_happy_path(f: &Fixture, t_reveal: u64, commit_deadline: u64, reveal_de
fn error_paths_registry_covers_every_variant() {
assert_eq!(
ERROR_PATH_REGISTRY.len(),
28,
31,
"update ERROR_PATH_REGISTRY when adding/removing Error variants"
);
for (variant, name) in ERROR_PATH_REGISTRY {
Expand Down
20 changes: 14 additions & 6 deletions contracts/round/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2272,7 +2272,7 @@ pub(super) const DOCUMENTED_ERROR_CODES: &[(Error, u32)] = &[
(Error::RoundVoided, 20),
(Error::NotVoidable, 21),
(Error::WrongStatus, 22),
// ── 30–40: cryptography & validation ──
// ── 30–39: cryptography & validation ──
(Error::InvalidDrandSignature, 30),
(Error::HashMismatch, 31),
(Error::AlreadyRevealed, 32),
Expand All @@ -2283,8 +2283,12 @@ pub(super) const DOCUMENTED_ERROR_CODES: &[(Error, u32)] = &[
(Error::NoValidBids, 37),
(Error::RoundFull, 38),
(Error::InvalidLimit, 39),
// ── 40–49: escrow accounting ──
(Error::EscrowNotConserved, 40),
// ── 40–42: seal-round window ──
(Error::SealRoundTooEarly, 40),
(Error::SealRoundTooLate, 41),
(Error::InvalidSealRoundZero, 42),
// ── 43–49: escrow accounting ──
(Error::EscrowNotConserved, 43),
];

/// Convert an `Error` to its on-chain discriminant using the [`repr(u32)`]
Expand Down Expand Up @@ -2323,6 +2327,9 @@ pub(super) fn variant_name(e: Error) -> &'static str {
Error::NoValidBids => "NoValidBids",
Error::RoundFull => "RoundFull",
Error::InvalidLimit => "InvalidLimit",
Error::SealRoundTooEarly => "SealRoundTooEarly",
Error::SealRoundTooLate => "SealRoundTooLate",
Error::InvalidSealRoundZero => "InvalidSealRoundZero",
Error::EscrowNotConserved => "EscrowNotConserved",
}
}
Expand All @@ -2344,7 +2351,7 @@ fn error_discriminants_match_document() {

#[test]
fn error_codes_have_no_duplicate_discriminants() {
// O(n²) is fine: n = 28. Done without `std::collections` because the
// O(n²) is fine: n = 31. Done without `std::collections` because the
// contract's `#![no_std]` applies to this module.
for (i, (variant_a, code_a)) in DOCUMENTED_ERROR_CODES.iter().enumerate() {
let name_a = variant_name(*variant_a);
Expand All @@ -2371,7 +2378,7 @@ fn error_table_enumerates_every_variant() {
// DOCUMENTED_ERROR_CODES.
assert_eq!(
DOCUMENTED_ERROR_CODES.len(),
28,
31,
"DOCUMENTED_ERROR_CODES appears missing entries. The exhaustive \
`variant_name` match already enforces parity at compile time — \
update it together with this list and contracts/round/ERRORS.md."
Expand All @@ -2384,7 +2391,8 @@ fn error_codes_use_reserved_ranges() {
// 1–4 → initialization/lookup
// 10–22 → lifecycle/timing
// 30–39 → crypto/validation
// 40–49 → escrow accounting
// 40–42 → seal-round window
// 43–49 → escrow accounting
// New categories should pick a fresh, contiguous range — not collide with
// logging conventions — and update ERRORS.md at the same time.
for (variant, code) in DOCUMENTED_ERROR_CODES {
Expand Down
7 changes: 6 additions & 1 deletion contracts/round/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ pub enum Error {
NoValidBids = 37,
RoundFull = 38,
InvalidLimit = 39,
// Seal-round window (issue #376): a seal must name the round the auction
// committed to open — exactly `Round::reveal_round`, never ±1 or 0.
SealRoundTooEarly = 40,
SealRoundTooLate = 41,
InvalidSealRoundZero = 42,
// Escrow accounting
EscrowNotConserved = 40,
EscrowNotConserved = 43,
}

/// Round lifecycle. Mirrors the state machine in PRD §6.
Expand Down
5 changes: 4 additions & 1 deletion packages/round-bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ export const Errors = {
37: {message:"NoValidBids"},
38: {message:"RoundFull"},
39: {message:"InvalidLimit"},
40: {message:"EscrowNotConserved"}
40: {message:"SealRoundTooEarly"},
41: {message:"SealRoundTooLate"},
42: {message:"InvalidSealRoundZero"},
43: {message:"EscrowNotConserved"}
}


Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) 2026 Sub Rosa contributors
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";
import { Keypair, rpc, StrKey } from "@stellar/stellar-sdk";

import { SubRosaClient } from "./client.js";
import {
SubRosaClientConfigError,
SubRosaNetworkMismatchError,
SubRosaPaginationError,
SubRosaSessionMismatchError,
SubRosaSubmitError,
} from "./errors.js";
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
SubRosaMissingReturnValueError,
SubRosaNetworkMismatchError,
SubRosaAssetValidationError,
SubRosaPaginationError,
SubRosaSubmitError,
SubRosaTimeoutError,
SubRosaTransactionError,
Expand Down
Loading
Loading