From 53de00d5d3086d8700b8f5baffd1f184c9b267a7 Mon Sep 17 00:00:00 2001 From: Santi Date: Thu, 27 Aug 2026 13:46:41 -0300 Subject: [PATCH 1/5] fix(demo): handle rejected wallet connection and display error message --- .../src/components/WalletButton.tsx | 13 ++++++++----- demos/freelance-escrow/src/hooks/useWallet.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/demos/freelance-escrow/src/components/WalletButton.tsx b/demos/freelance-escrow/src/components/WalletButton.tsx index be6f50f..8ef0077 100644 --- a/demos/freelance-escrow/src/components/WalletButton.tsx +++ b/demos/freelance-escrow/src/components/WalletButton.tsx @@ -2,7 +2,7 @@ import { useWallet } from "../hooks/useWallet"; import { shortenAddress } from "../lib/wallet"; export function WalletButton() { - const { wallet, connecting, connect } = useWallet(); + const { wallet, connecting, error, connect } = useWallet(); if (wallet.status === "unavailable") { return ( @@ -27,8 +27,11 @@ export function WalletButton() { } return ( - +
+ + {error &&

{error}

} +
); -} +} \ No newline at end of file diff --git a/demos/freelance-escrow/src/hooks/useWallet.ts b/demos/freelance-escrow/src/hooks/useWallet.ts index 72df683..d2f506f 100644 --- a/demos/freelance-escrow/src/hooks/useWallet.ts +++ b/demos/freelance-escrow/src/hooks/useWallet.ts @@ -4,6 +4,7 @@ import { type WalletState, connectWallet, detectWallet } from "../lib/wallet"; export function useWallet() { const [wallet, setWallet] = useState({ status: "disconnected" }); const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(null); useEffect(() => { detectWallet().then(setWallet); @@ -11,12 +12,16 @@ export function useWallet() { const connect = useCallback(async () => { setConnecting(true); + setError(null); try { - setWallet(await connectWallet()); + const state = await connectWallet(); + setWallet(state); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to connect wallet."); } finally { setConnecting(false); } }, []); - return { wallet, connecting, connect }; -} + return { wallet, connecting, error, connect }; +} \ No newline at end of file From 9cb7409934f99d70936a00ecbbcb110b8ed23823 Mon Sep 17 00:00:00 2001 From: Santi Date: Thu, 27 Aug 2026 16:29:50 -0300 Subject: [PATCH 2/5] fix(demo): throw on rejected wallet and add button/error styles --- demos/freelance-escrow/src/App.css | 13 +++++++++++++ demos/freelance-escrow/src/lib/wallet.ts | 14 +++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/demos/freelance-escrow/src/App.css b/demos/freelance-escrow/src/App.css index 708e920..c58ede1 100644 --- a/demos/freelance-escrow/src/App.css +++ b/demos/freelance-escrow/src/App.css @@ -428,3 +428,16 @@ background: var(--bg); color: var(--text); } + +.wallet-button-container { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; +} + +.wallet-error { + margin: 0; + font-size: 0.85rem; + color: #ef4444; +} diff --git a/demos/freelance-escrow/src/lib/wallet.ts b/demos/freelance-escrow/src/lib/wallet.ts index c1197a3..0f54b11 100644 --- a/demos/freelance-escrow/src/lib/wallet.ts +++ b/demos/freelance-escrow/src/lib/wallet.ts @@ -25,15 +25,15 @@ export async function detectWallet(): Promise { return resolveConnectedState(); } - export async function connectWallet(): Promise { const access = await requestAccess(); if (access.error) { - return { status: "disconnected" }; + throw new Error( + typeof access.error === "string" ? access.error : "Failed to connect wallet" + ); } return resolveConnectedState(); } - async function resolveConnectedState(): Promise { const [addressResult, networkResult] = await Promise.all([ getAddress(), @@ -41,7 +41,11 @@ async function resolveConnectedState(): Promise { ]); if (addressResult.error || !addressResult.address) { - return { status: "disconnected" }; + throw new Error( + typeof addressResult.error === "string" + ? addressResult.error + : "Failed to retrieve wallet address" + ); } return { @@ -53,4 +57,4 @@ async function resolveConnectedState(): Promise { export function shortenAddress(address: string): string { return `${address.slice(0, 4)}...${address.slice(-4)}`; -} +} \ No newline at end of file From 202fd4257af70478a3e517320ae6f71f2059ea8e Mon Sep 17 00:00:00 2001 From: Santi Date: Thu, 27 Aug 2026 21:23:24 -0300 Subject: [PATCH 3/5] fix(wallet): catch detectWallet errors gracefully and fallback to disconnected --- demos/freelance-escrow/pnpm-workspace.yaml | 2 ++ demos/freelance-escrow/src/hooks/useWallet.ts | 6 ++++- demos/freelance-escrow/src/lib/wallet.ts | 8 +++++- ...ment 29d cap in DEPLOYMENT_V2\357\200\242" | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 demos/freelance-escrow/pnpm-workspace.yaml create mode 100644 "tant and document 29d cap in DEPLOYMENT_V2\357\200\242" diff --git a/demos/freelance-escrow/pnpm-workspace.yaml b/demos/freelance-escrow/pnpm-workspace.yaml new file mode 100644 index 0000000..00f6fc4 --- /dev/null +++ b/demos/freelance-escrow/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: set this to true or false diff --git a/demos/freelance-escrow/src/hooks/useWallet.ts b/demos/freelance-escrow/src/hooks/useWallet.ts index d2f506f..dd75557 100644 --- a/demos/freelance-escrow/src/hooks/useWallet.ts +++ b/demos/freelance-escrow/src/hooks/useWallet.ts @@ -7,7 +7,11 @@ export function useWallet() { const [error, setError] = useState(null); useEffect(() => { - detectWallet().then(setWallet); + detectWallet() + .then(setWallet) + .catch(() => { + setWallet({ status: "disconnected" }); + }); }, []); const connect = useCallback(async () => { diff --git a/demos/freelance-escrow/src/lib/wallet.ts b/demos/freelance-escrow/src/lib/wallet.ts index 0f54b11..f44da13 100644 --- a/demos/freelance-escrow/src/lib/wallet.ts +++ b/demos/freelance-escrow/src/lib/wallet.ts @@ -23,8 +23,13 @@ export async function detectWallet(): Promise { return { status: "disconnected" }; } - return resolveConnectedState(); + try { + return await resolveConnectedState(); + } catch { + return { status: "disconnected" }; + } } + export async function connectWallet(): Promise { const access = await requestAccess(); if (access.error) { @@ -34,6 +39,7 @@ export async function connectWallet(): Promise { } return resolveConnectedState(); } + async function resolveConnectedState(): Promise { const [addressResult, networkResult] = await Promise.all([ getAddress(), diff --git "a/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" "b/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" new file mode 100644 index 0000000..ba37df7 --- /dev/null +++ "b/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" @@ -0,0 +1,26 @@ +diff --git a/contracts/tholos-v2/src/lib.rs b/contracts/tholos-v2/src/lib.rs +index 49804f9..2328100 100644 +--- a/contracts/tholos-v2/src/lib.rs ++++ b/contracts/tholos-v2/src/lib.rs +@@ -572,7 +572,7 @@ const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; +  + const MAX_REGISTRATION_DURATION_SECS: u64 = 7 * 24 * 60 * 60; + const MAX_REVEAL_DURATION_SECS: u64 = 7 * 24 * 60 * 60; +-pub const MAX_ANTI_SNIPE_HARD_MAX_SECS: u64 = 29 * 24 * 60 * 60; // 29 days ++const MAX_ANTI_SNIPE_HARD_MAX_SECS: u64 = 29 * 24 * 60 * 60; // 29 days + /// Same 7-day cap as v1's `challenge_window_secs`, for the same reason: it + /// must leave real margin within the 30-day persistent-storage TTL bump + /// (`INSTANCE_BUMP_AMOUNT`) for `finalize` to actually get called before the +diff --git a/docs/src/DEPLOYMENT_V2.md b/docs/src/DEPLOYMENT_V2.md +index b607fd1..8a0f462 100644 +--- a/docs/src/DEPLOYMENT_V2.md ++++ b/docs/src/DEPLOYMENT_V2.md +@@ -32,7 +32,7 @@ a dispute arrives. Every timeline runs from the moment `dispute` is called. + | --- | --- | + | `registration_duration_secs` | How long a dispute stays in registration, during which the asserter, disputer, and any third party can lock capital and commit votes. Must be at least 1 second; the contract enforces a practical upper bound to keep lifetimes reasonable. Deposit this commitment time into your business model: typical Internet disputes might use 1 day; urgent or time-sensitive ones might use 1 hour. If your dispute is about a sports result, a stock price, or anything with a known announcement, set this shorter than the time until the external event resolves, so resolution bonds are visible in time. | + | `anti_snipe_extension_secs` | How much longer the registration deadline moves if a position is funded within this many seconds of the ordinary cutoff. Prevents a late attacker from dominating an already-open dispute in the final second. Set it to 0 if you don't need anti-sniping (a trusted environment with no arms-race incentive), or to a reasonable backstab window (e.g., 5 minutes) if you expect contested disputes. The contract enforces an upper bound relative to `anti_snipe_hard_max_secs` (see below). | +-| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 3 * anti_snipe_extension_secs`). A very large hard max (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`) defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | ++| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this) and at most `MAX_ANTI_SNIPE_HARD_MAX_SECS` (29 days), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`). A very large hard max defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | + | `reveal_duration_secs` | How long a dispute stays in the reveal phase after registration closes, during which all third-party commitments from registration become binding votes by revealing their salted choice. Must be at least 1 second; the contract enforces a practical upper bound. Typical disputes might use 6 hours to 1 day here: long enough for off-chain coordinators to run their own resolution process, short enough to finalize quickly. After the reveal deadline, any position that did not reveal is counted as abstaining (forfeited in settlement). | +  + ### Arithmetic bounds From 9a691044976be653d12b131d64e16b567bdfc12a Mon Sep 17 00:00:00 2001 From: Santi Date: Thu, 27 Aug 2026 22:30:50 -0300 Subject: [PATCH 4/5] chore: remove accidental stray file and unneeded pnpm-workspace.yaml --- demos/freelance-escrow/pnpm-workspace.yaml | 2 -- ...ment 29d cap in DEPLOYMENT_V2\357\200\242" | 26 ------------------- 2 files changed, 28 deletions(-) delete mode 100644 demos/freelance-escrow/pnpm-workspace.yaml delete mode 100644 "tant and document 29d cap in DEPLOYMENT_V2\357\200\242" diff --git a/demos/freelance-escrow/pnpm-workspace.yaml b/demos/freelance-escrow/pnpm-workspace.yaml deleted file mode 100644 index 00f6fc4..0000000 --- a/demos/freelance-escrow/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -allowBuilds: - esbuild: set this to true or false diff --git "a/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" "b/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" deleted file mode 100644 index ba37df7..0000000 --- "a/tant and document 29d cap in DEPLOYMENT_V2\357\200\242" +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/contracts/tholos-v2/src/lib.rs b/contracts/tholos-v2/src/lib.rs -index 49804f9..2328100 100644 ---- a/contracts/tholos-v2/src/lib.rs -+++ b/contracts/tholos-v2/src/lib.rs -@@ -572,7 +572,7 @@ const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; -  - const MAX_REGISTRATION_DURATION_SECS: u64 = 7 * 24 * 60 * 60; - const MAX_REVEAL_DURATION_SECS: u64 = 7 * 24 * 60 * 60; --pub const MAX_ANTI_SNIPE_HARD_MAX_SECS: u64 = 29 * 24 * 60 * 60; // 29 days -+const MAX_ANTI_SNIPE_HARD_MAX_SECS: u64 = 29 * 24 * 60 * 60; // 29 days - /// Same 7-day cap as v1's `challenge_window_secs`, for the same reason: it - /// must leave real margin within the 30-day persistent-storage TTL bump - /// (`INSTANCE_BUMP_AMOUNT`) for `finalize` to actually get called before the -diff --git a/docs/src/DEPLOYMENT_V2.md b/docs/src/DEPLOYMENT_V2.md -index b607fd1..8a0f462 100644 ---- a/docs/src/DEPLOYMENT_V2.md -+++ b/docs/src/DEPLOYMENT_V2.md -@@ -32,7 +32,7 @@ a dispute arrives. Every timeline runs from the moment `dispute` is called. - | --- | --- | - | `registration_duration_secs` | How long a dispute stays in registration, during which the asserter, disputer, and any third party can lock capital and commit votes. Must be at least 1 second; the contract enforces a practical upper bound to keep lifetimes reasonable. Deposit this commitment time into your business model: typical Internet disputes might use 1 day; urgent or time-sensitive ones might use 1 hour. If your dispute is about a sports result, a stock price, or anything with a known announcement, set this shorter than the time until the external event resolves, so resolution bonds are visible in time. | - | `anti_snipe_extension_secs` | How much longer the registration deadline moves if a position is funded within this many seconds of the ordinary cutoff. Prevents a late attacker from dominating an already-open dispute in the final second. Set it to 0 if you don't need anti-sniping (a trusted environment with no arms-race incentive), or to a reasonable backstab window (e.g., 5 minutes) if you expect contested disputes. The contract enforces an upper bound relative to `anti_snipe_hard_max_secs` (see below). | --| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 3 * anti_snipe_extension_secs`). A very large hard max (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`) defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | -+| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this) and at most `MAX_ANTI_SNIPE_HARD_MAX_SECS` (29 days), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`). A very large hard max defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | - | `reveal_duration_secs` | How long a dispute stays in the reveal phase after registration closes, during which all third-party commitments from registration become binding votes by revealing their salted choice. Must be at least 1 second; the contract enforces a practical upper bound. Typical disputes might use 6 hours to 1 day here: long enough for off-chain coordinators to run their own resolution process, short enough to finalize quickly. After the reveal deadline, any position that did not reveal is counted as abstaining (forfeited in settlement). | -  - ### Arithmetic bounds From 58728f607035f2c0d1da18f63ce7ee877745014a Mon Sep 17 00:00:00 2001 From: collinsezedike Date: Fri, 28 Aug 2026 10:23:16 +0100 Subject: [PATCH 5/5] chore: add trailing newlines --- demos/freelance-escrow/src/components/WalletButton.tsx | 2 +- demos/freelance-escrow/src/hooks/useWallet.ts | 2 +- demos/freelance-escrow/src/lib/wallet.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demos/freelance-escrow/src/components/WalletButton.tsx b/demos/freelance-escrow/src/components/WalletButton.tsx index 8ef0077..8b7a78c 100644 --- a/demos/freelance-escrow/src/components/WalletButton.tsx +++ b/demos/freelance-escrow/src/components/WalletButton.tsx @@ -34,4 +34,4 @@ export function WalletButton() { {error &&

{error}

} ); -} \ No newline at end of file +} diff --git a/demos/freelance-escrow/src/hooks/useWallet.ts b/demos/freelance-escrow/src/hooks/useWallet.ts index dd75557..b7df887 100644 --- a/demos/freelance-escrow/src/hooks/useWallet.ts +++ b/demos/freelance-escrow/src/hooks/useWallet.ts @@ -28,4 +28,4 @@ export function useWallet() { }, []); return { wallet, connecting, error, connect }; -} \ No newline at end of file +} diff --git a/demos/freelance-escrow/src/lib/wallet.ts b/demos/freelance-escrow/src/lib/wallet.ts index f44da13..f9ec266 100644 --- a/demos/freelance-escrow/src/lib/wallet.ts +++ b/demos/freelance-escrow/src/lib/wallet.ts @@ -63,4 +63,4 @@ async function resolveConnectedState(): Promise { export function shortenAddress(address: string): string { return `${address.slice(0, 4)}...${address.slice(-4)}`; -} \ No newline at end of file +}