From 25763f2fd75c1a898561ba90ff88470a692e1a8a Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Thu, 16 Apr 2026 00:32:55 -0500 Subject: [PATCH 001/117] =?UTF-8?q?docs:=20LS-QMSR=20verification=20?= =?UTF-8?q?=E2=80=94=20disprove=20properness,=20confirm=20bounded=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Independently verify the three LS-QMSR TODO items: - Path independence: confirmed (degree-1 homogeneity of C) - Bounded loss α(N-1)/(4N)·S: confirmed as upper bound, tight for α ≤ 2 - Strict properness: disproven — displayed prices shrink 50% toward uniform (p = π/2 + 1/(2N)), independent of α Add the adaptive-properness-path-independence trilemma section explaining why this is a fundamental constraint: any state-dependent b makes the price field non-conservative (∇C ≠ p), forcing a choice between adaptive liquidity and price accuracy. LMSR's exponential nonlinearity makes the trilemma operationally soft (small bias ∝ 1/α); QMSR's linearity makes it hard (constant 50% bias). Revise recommendations: standard QMSR replaces LS-QMSR as primary choice for both binary and N-outcome markets. Add adaptive liquidity alternatives (LOB, operator-managed b, time-based b growth). Add cubic polynomial scoring rule as research direction for achieving LS-like adaptation with smaller bias. Also fix QMSR depth profile inconsistency (constant price impact ≠ uniform directional depth) and strengthen first-mover incentive discussion in binary market caveats. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../amm-scoring-rule-tradeoffs.md | 205 +++++++++++++----- 1 file changed, 148 insertions(+), 57 deletions(-) diff --git a/docs/contracts/multi-outcome/amm-scoring-rule-tradeoffs.md b/docs/contracts/multi-outcome/amm-scoring-rule-tradeoffs.md index 7405d905..0a23f8dc 100644 --- a/docs/contracts/multi-outcome/amm-scoring-rule-tradeoffs.md +++ b/docs/contracts/multi-outcome/amm-scoring-rule-tradeoffs.md @@ -13,7 +13,11 @@ This document compares four automated market maker scoring rules for use in dead The comparison covers conceptual properties (properness, depth profile, bounded loss) and practical constraints (Simplicity covenant feasibility, witness sizes, interaction with the limit order book) for two distinct market structures: single-event binary YES/NO markets, and N-outcome markets with a YES and NO token per outcome. -> **TODO: Verify LS-QMSR derivations.** The LS-QMSR construction described here (substituting `b = α·S` into the QMSR cost function and price formula) was derived during design discussion and the math has been worked through informally, but it has not been independently verified. Before adopting LS-QMSR for production, the construction should be checked against a formal proof of strict properness, the bounded-loss derivation should be verified at multiple N, and the path independence argument (currently relying on the same homogeneity trick as LS-LMSR) should be confirmed rigorously. +> **LS-QMSR verification status (updated).** The LS-QMSR construction has been independently analyzed with the following results: +> +> - **Path independence**: **Confirmed.** Follows directly from degree-1 homogeneity of `C(q) = Σ q_k · p_k(q)`. +> - **Bounded loss**: **Confirmed** as an upper bound. The formula `α(N-1)/(4N) · S` is tight for α ≤ 2 and conservative for larger α. Verified numerically at multiple (N, α) pairs. +> - **Strict properness**: **Disproven.** The LS construction causes the displayed price to diverge from the trader's true belief: at the trader's optimum, `p_k = π_k/2 + 1/(2N)` — a fixed 50% shrinkage toward uniform, independent of α. This is a fundamental consequence of the adaptive-properness-path-independence trilemma (see dedicated section below) and materially affects LS-QMSR's suitability as a price oracle. ## Background @@ -171,15 +175,16 @@ Prices are degree-0 homogeneous, so by the same construction as LS-LMSR: C(q) = Σ q_k · p_k(q) = (Σ q_k²)/(α·S) + S(α-1)/(α·N) ``` -**Properties** (subject to TODO verification): +**Properties** (verification status updated): - Prices sum to 1, homogeneous of degree 0. -- Path independent (via degree-1 homogeneity of C). +- Path independent (via degree-1 homogeneity of C). **Verified.** - **No domain restriction for α ≥ 1**: the condition `q_k ≥ S(1-α)/N` is automatically satisfied when α ≥ 1. -- Bounded loss: `α(N-1)/(4N) · S` — proportional to volume, half the standard QMSR loss for the same effective b. +- Bounded loss: `α(N-1)/(4N) · S` — proportional to volume. **Verified** as an upper bound; tight for α ≤ 2. See note below. - Adaptive depth: state-dependent. Outcomes with high volume share have low price impact (thick); outcomes with low volume share have high price impact (thin). -- Marginal cost ≠ instantaneous price (same caveat as LS-LMSR). +- Marginal cost ≠ instantaneous price (same caveat as LS-LMSR). **This has material consequences for price accuracy — see the trilemma section below.** +- **Displayed prices do not reflect true belief**: at the trader's optimum, `p_k = π_k/2 + 1/(2N)` — a fixed 50% shrinkage toward uniform, independent of α. See the trilemma section for derivation and implications. -> **TODO**: verify the bounded loss derivation `α(N-1)/(4N) · S` independently. The derivation goes through worst-case analysis of `q_k - C(q)` over all valid q vectors, finding the optimum at `r* = [α(N-1) + 2]/(2N)` (in the symmetric configuration). Result should be checked at N = 2, 3, 5, 10 numerically. +> **Bounded loss verification**: the bound `α(N-1)/(4N) · S` was confirmed via constrained optimization of `max_{r,k} [r_k - (Σr_j²)/α - (α-1)/(αN)]` subject to `Σr_k = 1, r_k ≥ 0`. The unconstrained optimum `r_1* = [1 + (N-1)α/2]/N` is feasible when α ≤ 2; for larger α, the boundary constraint binds and the true worst case is strictly less than the formula. Verified numerically at (N=2, α=1): loss/S = 1/8 ✓; (N=3, α=1): loss/S = 1/6 ✓; (N=5, α=2): loss/S = 2/5 ✓. --- @@ -187,23 +192,23 @@ C(q) = Σ q_k · p_k(q) = (Σ q_k²)/(α·S) + S(α-1)/(α·N) **Strict properness**: a market scoring rule is strictly proper if a risk-neutral trader's unique expected-profit-maximizing strategy is to move prices exactly to their true belief. -### LMSR / LS-LMSR +### LMSR -**Unconditionally strictly proper**. Expected profit for moving prices from π to r given true belief q: +**Unconditionally strictly proper** (fixed b). Expected profit for moving prices from π to r given true belief q: ``` E[profit] = b · [KL(q ‖ π) - KL(q ‖ r)] ``` -(For LMSR with fixed b. For LS-LMSR, a similar KL-based identity holds but with `b = α·S` varying along the trade path; the key conclusion — unique optimum at r = q — is preserved.) - KL divergence is non-negative and equals zero iff arguments are identical, so the expected payoff is uniquely maximized at r = q. No constraints ever bind because LMSR prices approach 0 and 1 asymptotically but never reach them. The KL identity has a strong interpretive consequence: **the LMSR market price is a sufficient statistic for the market's aggregate belief**. The cost function literally is a KL geometry over the probability simplex, and traders trading honestly is equivalent to performing Bayesian updates on the market's posterior. -### QMSR / LS-QMSR +**LS-LMSR**: subject to the trilemma (`∇C ≠ p`), so the displayed price at the trader's optimum is not exactly the true belief. However, the bias is proportional to `1/α` and small for practical α values. See the trilemma section for details. -**Strictly proper in the unconstrained case**. Expected profit for binary QMSR (moving from π to r): +### QMSR + +**Strictly proper in the unconstrained case** (fixed b). Expected profit for binary QMSR (moving from π to r): ``` E[profit] = 2b(r-π)(q-π) - b(r-π)² @@ -211,13 +216,17 @@ E[profit] = 2b(r-π)(q-π) - b(r-π)² Setting ∂/∂r = 0 yields r = q. Unique optimum at the true belief. -**Properness can be compromised when boundary constraints bind**. If the unconstrained optimum r = q lies outside the valid domain, the trader is forced to stop at the boundary. The reported price is then the closest representable belief to q, not q itself. +**Properness can be compromised when boundary constraints bind**. If the unconstrained optimum r = q lies outside the valid domain, the trader is forced to stop at the boundary. The reported price is then the closest representable belief to q, not q itself. The cross-coupling term means a trader pushing one outcome's price up automatically suppresses other prices through `-S/(bN)`, which reduces (but does not eliminate) how often the unconstrained optimum lies outside the valid domain. + +### LS-QMSR + +**Not strictly proper in the price-display sense.** Although no domain constraints bind for α ≥ 1, the LS construction introduces a separate properness failure: the trader's optimal strategy moves displayed prices to `p_k = π_k/2 + 1/(2N)`, not to the true belief π_k. This is a fixed 50% shrinkage toward uniform, independent of α. -For LS-QMSR with α ≥ 1, no domain constraints bind, so properness is unconditional. For standard QMSR, the cross-coupling term means a trader pushing one outcome's price up automatically suppresses other prices through `-S/(bN)`, which reduces (but does not eliminate) how often the unconstrained optimum lies outside the valid domain. +The underlying scoring rule is still proper in the decision-theoretic sense — the marginal cost `∂C/∂q_k` equals the true belief at the optimum. But the displayed price (which the system reports as "the market says X%") is systematically biased. See the trilemma section for the full derivation and comparison with LS-LMSR. ### Sharpness of the optimum -Both families are strictly proper, but they differ in how sharply the optimum is identified — i.e., the curvature of the expected-profit landscape at r = q. +Both fixed-b families (LMSR and QMSR) are strictly proper, but they differ in how sharply the optimum is identified — i.e., the curvature of the expected-profit landscape at r = q. For LMSR: curvature ≈ `1 / [q(1-q)]` per unit b. For QMSR (binary): curvature = `2b` (constant). @@ -240,11 +249,76 @@ Whether stronger first-mover incentives are unambiguously good is debatable — --- +## The Adaptive-Properness-Path-Independence Trilemma + +Any market scoring rule based on a cost function `C(q)` faces a three-way tradeoff among: + +1. **Adaptive liquidity** — the depth parameter `b` adjusts automatically with market volume (`b = α·S`) +2. **Price accuracy** — the displayed price equals the trader's true belief at the optimum (`∇C = p`) +3. **Path independence** — the cost of a trade depends only on start and end states (`C(q') - C(q)`) + +Standard scoring rules (LMSR, QMSR with fixed `b`) achieve (2) and (3): the cost function gradient IS the price vector, and C is well-defined. LS variants (LS-LMSR, LS-QMSR) achieve (1) and (3): the cost function is degree-1 homogeneous (path independent), and b adapts to volume. **Achieving all three is provably impossible** for QMSR-type prices: when `b` depends on the market state via any function `b(q)` with `∂b/∂q_k ≠ 0`, the price field has non-zero curl (`∂p_k/∂q_j ≠ ∂p_j/∂q_k` for `q_k ≠ q_j`), so no cost function `C` with `∇C = p` exists. The same argument applies to LMSR-type prices under any state-dependent `b`. + +### Why ∇C ≠ p in LS variants + +When `b = α·S` depends on the market state, differentiating the cost function `C(q) = Σ q_k · p_k(q)` produces extra terms: + +``` +∂C/∂q_k = p_k + Σ_j q_j · ∂p_j/∂q_k +``` + +The second term is non-zero because changing `q_k` changes `S`, which changes `b`, which changes all prices. For fixed-`b` scoring rules, `∂b/∂q_k = 0` and the extra terms vanish, giving `∇C = p`. + +A risk-neutral trader maximizes `E[profit] = Σ π_k q'_k - C(q')`, yielding the first-order condition `∂C/∂q'_k = π_k`. This means the trader moves the state to where the **marginal cost** (not the displayed price) equals their true belief. The information is encoded in `∇C`, not in `p`. + +### LS-QMSR: constant 50% shrinkage + +For LS-QMSR, the trader's first-order condition can be solved in closed form. At the optimum: + +``` +optimal share: r_k = α·π_k/2 + (2-α)/(2N) +displayed price: p_k = π_k/2 + 1/(2N) +``` + +The displayed price is a **50% shrinkage toward uniform**, independent of α. A trader with true belief π = 0.9 in a binary market moves the displayed price to 0.7, not 0.9. + +**Derivation sketch**: the FOC `∂C/∂q_k = π_k` gives `2r_k/α - (Σr_j²)/α + (α-1)/(αN) = π_k`. Summing over k yields the constraint `Σr_j² = 1/N` at the optimum. Substituting back and computing `p_k = r_k/α - 1/(αN) + 1/N` produces `p_k = π_k/2 + 1/(2N)`. + +The root cause is QMSR's **linear** price formula. The optimal `r_k` scales linearly with α (as `α·π_k/2`), causing α to cancel perfectly in `p_k = r_k/α + ...`, leaving a constant bias. This cancellation is specific to the linear structure. + +Two additional structural consequences: + +- **Prices stay biased regardless of volume.** Because prices are degree-0 homogeneous (they depend only on the ratios `r_k = q_k/S`, not on the scale `S`), additional trading at the optimal ratios adds volume without changing prices. A second trader with the same belief can profitably scale up at the same ratios — the profit function is degree-1 homogeneous, so expected profit scales linearly with trade size — but prices remain at `π/2 + 1/(2N)` throughout. The bias is a property of the ratio equilibrium, not a transient that washes out with volume. +- **Unbounded individual profit.** The degree-1 homogeneity of the profit function means a trader with non-uniform beliefs can achieve unbounded expected profit by scaling up their position. In practice this is bounded by capital, but it contrasts with standard MSRs where strict convexity of `C` ensures a unique finite optimum. The market maker's loss ratio remains bounded (the `α(N-1)/(4N)` bound holds regardless), but the absolute loss scales with volume. + +### LS-LMSR: small, α-dependent shrinkage + +For LS-LMSR, the same structural issue exists (`∇C ≠ p`), but the exponential (softmax) price formula means the optimal `r_k` scales **sub-linearly** with α. The α does not cancel from the price formula, and the bias shrinks as α grows. For practical α values, the displayed price is very close to the true belief. + +This is the fundamental advantage of nonlinear price formulas under the LS construction: the more curvature the price function has, the smaller and more α-dependent the bias becomes. + +The trilemma is mathematically strict — `∇C ≠ p` for any state-dependent `b` — but LS-LMSR makes it operationally soft. For practical α values, the price bias is negligible, giving LS-LMSR most of the benefits of all three properties simultaneously. The trilemma bites hard for LS-QMSR (constant 50% bias) but barely for LS-LMSR. This distinction is what makes the choice of base scoring rule matter so much: the exponential nonlinearity of LMSR nearly overcomes the trilemma, while the linearity of QMSR does not. It also suggests a research direction: a scoring rule with **intermediate** nonlinearity (e.g., quadratic prices from a cubic scoring rule) should produce intermediate bias under the LS construction — potentially small enough to be practical, while remaining polynomial and Simplicity-compatible. See open question 2. + +### Implications + +The trilemma has direct consequences for scoring rule selection: + +| Scoring rule | Adaptive b | ∇C = p | Path independent | Price bias at optimum | +|---|---|---|---|---| +| LMSR (fixed b) | No | Yes | Yes | None | +| QMSR (fixed b) | No | Yes | Yes | None (unconstrained domain) | +| LS-LMSR | Yes | No | Yes | Small, ∝ 1/α | +| LS-QMSR | Yes | No | Yes | 50% shrinkage toward uniform | + +For a system where the AMM's displayed price serves as the source of truth (as in deadcat), price accuracy is a first-order concern. The LS-QMSR's constant 50% bias means the price oracle systematically underreports the market's true belief — a market that "really believes" 90% displays 70%. + +--- + ## Subsidy Efficiency / Bounded Loss The operator's worst-case loss bounds how much subsidy capital must be locked at pool creation. For LS variants, the loss scales with volume; for fixed-b variants, it's a fixed amount. -| N | LMSR loss | QMSR loss | LS-LMSR loss/S | LS-QMSR loss/S (TODO: verify) | +| N | LMSR loss | QMSR loss | LS-LMSR loss/S | LS-QMSR loss/S (verified) | |---|---|---|---|---| | 2 | 0.693b | 0.250b | 0.693α | 0.125α | | 3 | 1.099b | 0.333b | 1.099α | 0.167α | @@ -255,7 +329,7 @@ The operator's worst-case loss bounds how much subsidy capital must be locked at **Comparison ratios** (efficiency advantage of QMSR-family over LMSR-family at equal subsidy): -| N | QMSR vs LMSR | LS-QMSR vs LS-LMSR (TODO: verify) | +| N | QMSR vs LMSR | LS-QMSR vs LS-LMSR (verified) | |---|---|---| | 2 | 2.77× | 5.54× | | 3 | 3.30× | 6.59× | @@ -281,11 +355,11 @@ For binary, depth at price p is `b/(1-p)` for moving the price up; `b/p` for mov ### QMSR -Constant: `∂p/∂q = (N-1)/(bN)`. Same depth at every price level. +Constant price impact: `∂p/∂q = (N-1)/(bN)`, independent of state. A unit of trading moves the price by the same amount regardless of the current price level. -For binary, depth is `2bp` (moving up) or `2b(1-p)` (moving down). +However, the cost per unit of probability shift (depth as defined above) is not constant: for binary, depth is `2bp` (moving up) or `2b(1-p)` (moving down). Directional depth varies linearly with p; the bidirectional average `b·[p + (1-p)] = b` is constant. -**Depth profile shape**: uniform across all prices. +**Depth profile shape**: constant price impact; directional depth varies linearly with p (cheap to push prices toward 0 or 1, expensive to push away). ### LS-QMSR @@ -449,7 +523,17 @@ The router solves a joint optimization: given a requested trade size, it selects - **LMSR + LOB**: strong first-mover incentives (exponential cost), thick extremes (resists tail moves), transcendental routing. Best when accurate tail-probability pricing is critical and implementation overhead is acceptable. - **QMSR + LOB**: moderate first-mover incentives (linear cost), uniform depth, linear routing. Better subsidy efficiency. Best when the active trading range dominates and operator collateral matters. - **LS-LMSR + LOB**: adaptive overall scale on top of LMSR's shape. The LOB provides some organic adaptive liquidity (makers enter when volume justifies it), so LS-LMSR's adaptive-b advantage is partially redundant. Still provides stronger first-mover incentives than QMSR-family options. -- **LS-QMSR + LOB**: adaptive per-outcome depth, no domain constraint (α ≥ 1), best subsidy efficiency. The adaptive depth is orthogonal to the LOB's organic scaling — it provides per-outcome depth tuning that the LOB alone doesn't replicate (since makers concentrate on popular outcomes). +- **LS-QMSR + LOB**: adaptive per-outcome depth, no domain constraint (α ≥ 1), best subsidy efficiency. The adaptive depth is orthogonal to the LOB's organic scaling — it provides per-outcome depth tuning that the LOB alone doesn't replicate (since makers concentrate on popular outcomes). **However**, the 50% price-display bias (see trilemma section) significantly undermines the AMM's role as source of truth for current and historical prices. + +### Adaptive liquidity without the LS construction + +The LS construction's appeal is automatic depth scaling — thin markets are thin, thick markets are thick, without operator intervention. The trilemma shows this comes at the cost of price accuracy. Several alternative mechanisms achieve adaptive liquidity while preserving `∇C = p` (strict properness): + +- **LOB-provided organic depth**: the LOB already provides adaptive liquidity — makers enter when volume and spreads justify it. This scales naturally with market interest without touching the scoring rule. The AMM provides always-available baseline liquidity; the LOB thickens it when demand warrants. +- **Operator-managed b adjustments**: the pool admin mechanism (already part of the LMSR pool design) allows the operator to increase b as the market matures. Exogenous adaptation with no properness cost. The operator observes volume growth and adjusts; the LOB provides interim depth during any lag. +- **Time-based b growth**: `b(t) = b_0 + f(t)` where b increases with market age. Since b depends on time rather than market state, `∇C = p` is fully preserved — strict properness, path independence, polynomial arithmetic. Depth grows automatically without operator intervention. The tradeoff: depth increases even for markets nobody trades, locking up more subsidy capital. Manageable with a modest growth rate and a cap. + +These mechanisms are less elegant than the LS construction but avoid the trilemma entirely. For deadcat's architecture, the combination of standard QMSR + active LOB + operator b-adjustment appears to achieve the practical goal of adaptive liquidity without sacrificing price accuracy. --- @@ -484,7 +568,7 @@ A single binary market has one YES token, one NO token, and resolves to one of t - **Implementable**: 2D Merkle table (because b changes per trade) — feasible for binary but larger/more complex than standard LMSR's 1D table - **Subsidy**: loss/volume = 0.693α (proportional to volume rather than fixed) - **Depth profile**: same logarithmic shape as LMSR; overall scale adapts to volume -- **Properness**: unconditional +- **Properness**: subject to small α-dependent bias (see trilemma section); near-exact for practical α values - **Domain**: no constraint - **First-mover incentive**: strong - **Per-swap witness**: larger than LMSR (table dimension is doubled) @@ -492,14 +576,14 @@ A single binary market has one YES token, one NO token, and resolves to one of t ### LS-QMSR -- **Implementable**: inline polynomial arithmetic, native to Simplicity (TODO: verify) -- **Subsidy**: loss/volume = α/8 (binary) (TODO: verify) +- **Implementable**: inline polynomial arithmetic, native to Simplicity +- **Subsidy**: loss/volume = α/8 (binary) (verified upper bound) - **Depth profile**: state-dependent; adaptive thickness -- **Properness**: unconditional for α ≥ 1 (no boundary binds) +- **Properness**: **biased** — displayed price = `π/2 + 1/4` (binary), a fixed 50% shrinkage toward uniform (see trilemma section) - **Domain**: no constraint for α ≥ 1 - **First-mover incentive**: moderate (similar to QMSR) - **Per-swap witness**: ~2-3 kB (similar to QMSR) -- **Notes**: requires LS-QMSR verification before adoption +- **Notes**: the 50% price-display bias is a significant concern for the AMM's role as source of truth; see trilemma section and adaptive liquidity alternatives --- @@ -540,9 +624,9 @@ Shares LMSR's scaling problems plus the per-trade table invalidation issue (one ### LS-QMSR -Shares QMSR's scaling properties (any N feasible) plus the adaptive-depth advantages. Subsidy bound `α(N-1)/(4N) · S` (TODO: verify) — half the standard QMSR loss for the same effective b. +Shares QMSR's scaling properties (any N feasible) plus the adaptive-depth advantages. Subsidy bound `α(N-1)/(4N) · S` (verified upper bound, tight for α ≤ 2). -For α ≥ 1, the domain constraint disappears entirely, eliminating the boundary concern that exists for standard QMSR at higher N. +For α ≥ 1, the domain constraint disappears entirely, eliminating the boundary concern that exists for standard QMSR at higher N. **However**, the 50% price-display bias (see trilemma section) applies at every N, with displayed prices shrinking to `p_k = π_k/2 + 1/(2N)`. This bias becomes more severe at higher N because the uniform component `1/(2N)` is smaller, meaning the displayed price compresses the full belief range `[0, 1]` into `[1/(2N), 1/2 + 1/(2N)]` — a dramatically narrower band at higher N. For N=10, the displayable range is only [0.05, 0.55]; a market with 100% consensus on one outcome displays that outcome at 55%. --- @@ -550,15 +634,15 @@ For α ≥ 1, the domain constraint disappears entirely, eliminating the boundar ### Single-Event Binary Markets -| Dimension | LMSR | QMSR | LS-LMSR | LS-QMSR (TODO: verify) | +| Dimension | LMSR | QMSR | LS-LMSR | LS-QMSR | |---|---|---|---|---| -| Properness | Unconditional | Conditional (boundary) | Unconditional | Unconditional (α≥1) | -| Bounded loss | `b·ln(2) ≈ 0.693b` | `b/4 = 0.25b` | `α·ln(2)·S` | `α·S/8` | +| Properness | Unconditional | Conditional (boundary) | Small bias ∝ 1/α (trilemma) | **50% bias toward uniform** (trilemma) | +| Bounded loss | `b·ln(2) ≈ 0.693b` | `b/4 = 0.25b` | `α·ln(2)·S` | `α·S/8` (verified) | | Subsidy efficiency vs LMSR | 1× | 2.77× better | Same as LMSR (per state) | 5.54× better than LS-LMSR | | Depth at extremes (p=0.05) | Very thick | Thin | Very thick | State-dependent | | Depth at center (p=0.5) | Thin | Uniform; thicker than LMSR for equal subsidy | Thin | Adaptive | | First-mover incentive | Strong | Moderate | Strong | Moderate | -| Domain constraint | None | `|q_Y - q_N| ≤ b` | None | None for α≥1 | +| Domain constraint | None | `\|q_Y - q_N\| ≤ b` | None | None for α≥1 | | Simplicity feasibility | Feasible (1D Merkle table) | Feasible (inline polynomial) | Feasible (2D table) | Feasible (inline polynomial) | | Per-swap witness size | ~3-5 kB | ~2-3 kB | Larger than LMSR | ~2-3 kB | | Adaptive liquidity | No | No | Yes (per total volume) | Yes (per outcome) | @@ -567,32 +651,32 @@ For α ≥ 1, the domain constraint disappears entirely, eliminating the boundar ### N-Outcome YES/NO Markets -| Dimension | LMSR (unified) | LMSR (composed binary) | QMSR | LS-QMSR (TODO: verify) | +| Dimension | LMSR (unified) | LMSR (composed binary) | QMSR | LS-QMSR | |---|---|---|---|---| | Implementable in Simplicity | Up to N=3, borderline N=4 | Any N | Any N | Any N | | Σp = 1 across YES tokens | Structural | Arbitrage-enforced | Structural | Structural | -| Bounded loss | `b·ln(N)` | `N · b · ln(2)` | `b(N-1)/(2N)` | `α(N-1)/(4N) · S` | +| Bounded loss | `b·ln(N)` | `N · b · ln(2)` | `b(N-1)/(2N)` | `α(N-1)/(4N) · S` (verified) | | Subsidy scaling with N | Logarithmic | Linear | Sub-linear | Sub-linear | | Subsidy efficiency at N=10 | Baseline | ~3× worse than unified LMSR | ~5× better than unified LMSR | ~10× better than LS-LMSR | | Per-outcome operator | No | Yes (independent pools) | No | No | | Per-swap witness size | Grows with N (Merkle proof depth) | O(1) per pool | ~8N bytes | ~8N bytes | | Domain constraint | None | None | Skew-bounded | None for α≥1 | -| Properness | Unconditional | Unconditional per pool | Conditional | Unconditional (α≥1) | +| Properness | Unconditional | Unconditional per pool | Conditional (boundary) | **50% bias toward uniform** (trilemma) | | Deployment history | Limited (N=2 mostly) | Limited | None | None | --- ## Open Questions -1. **LS-QMSR formal verification**: the construction needs an independent proof of strict properness, bounded-loss derivation, and path independence. The math has been worked through informally but hasn't been peer-reviewed or formally checked. +1. ~~**LS-QMSR formal verification**~~: **Resolved.** Path independence confirmed (degree-1 homogeneity). Bounded loss confirmed as upper bound (tight for α ≤ 2). Strict properness **disproven** — displayed prices exhibit a fixed 50% shrinkage toward uniform. See the trilemma section and the LS-QMSR verification banner at the top of this document. -2. **Smooth QMSR variants**: Nueve & Waggoner (NeurIPS 2025) propose a smoothed QMSR with a log-barrier term that eliminates the domain restriction at the cost of reintroducing transcendental arithmetic at the boundary. Could a polynomial-only smoothing exist? Could it apply to LS-QMSR similarly? +2. **Higher-degree polynomial scoring rules with LS construction**: a cubic scoring rule (e.g., `S(r, ω) = 3r_ω² - 2Σr_k³`) is strictly proper and has quadratic (nonlinear but polynomial) prices — Simplicity-compatible. Under the LS construction, the price bias should be α-dependent and smaller than LS-QMSR's 50%, because nonlinear price formulas give the extra `∇C - p` terms curvature to cancel against (same principle that makes LS-LMSR's bias small). This is genuinely novel territory: no existing literature on cubic market scoring rules. A full analysis would need to derive the bounded loss, depth profile, domain constraints, and subsidy efficiency from scratch. This is a research project, not an engineering task — but it's the most promising path to achieving LS-like adaptive liquidity with polynomial arithmetic and small price bias. -3. **Hybrid / multi-pool designs**: should a single market support multiple pools simultaneously (e.g., a QMSR pool for institutional flow plus per-outcome LMSR pools for retail)? What composition rules would be required? +3. **Smooth QMSR variants**: Nueve & Waggoner (NeurIPS 2025) propose a smoothed QMSR with a log-barrier term that eliminates the domain restriction at the cost of reintroducing transcendental arithmetic at the boundary. Could a polynomial-only smoothing exist? Could it apply to LS-QMSR similarly? -4. **LS-QMSR's α calibration**: for what α values does LS-QMSR provide the right balance of liquidity and price responsiveness? How should it be set in practice? +4. **Hybrid / multi-pool designs**: should a single market support multiple pools simultaneously (e.g., a QMSR pool for institutional flow plus per-outcome LMSR pools for retail)? What composition rules would be required? -5. **LS-QMSR loss analysis at high N**: the bounded-loss derivation assumes the symmetric configuration is the worst case. Is this true for all α and N? What happens at N → ∞? +5. ~~**LS-QMSR loss analysis at high N**~~: **Partially resolved.** The bound `α(N-1)/(4N) · S` is confirmed as correct for all N. It is tight for α ≤ 2. For α > 2, the true worst case is strictly less. The bound is conservative (not violated) at any (α, N) combination. 6. **Operator economics in production**: theoretical subsidy efficiency is one factor; actual operator P&L depends on trader behavior, fee structure, and adverse selection. None of the scoring rules have been operated at scale on Liquid in deadcat's specific setup. @@ -606,46 +690,53 @@ For α ≥ 1, the domain constraint disappears entirely, eliminating the boundar ### For binary markets (in this author's view) -Setting aside what's already built (none of the pool designs have been implemented yet at the time of writing), **LS-QMSR with α ≥ 1 appears to be the strongest choice on engineering merits** for binary markets in deadcat's environment: +The trilemma analysis (above) materially changes the earlier recommendation. LS-QMSR's 50% price-display bias disqualifies it as a source-of-truth price oracle without a correction mechanism that doesn't currently exist. + +**Standard QMSR appears to be the strongest choice** for binary markets in deadcat's environment: - Native to Simplicity (polynomial arithmetic, no Merkle tables) -- No domain restriction (α ≥ 1) -- Better subsidy efficiency than any alternative (TODO: verify) -- Adaptive depth (thick on the popular side, thin on the longshot) +- **Strictly proper**: displayed prices equal true belief (∇C = p), no bias +- Better subsidy efficiency than LMSR (2.77× for same depth) - Smaller per-swap witness than LMSR - Composes cleanly with the existing LOB +- Well-studied academically (Brier 1950, Selten 1998, Chen & Pennock 2007, Abramowicz 2007) The main caveats are: -- **Verification status**: LS-QMSR needs the formal verification flagged in the TODO comments above -- **Zero deployment history**: no production system uses LS-QMSR; this is genuine first-mover risk -- **Loss of LMSR's KL interpretation**: the elegant "price = Bayesian-optimal aggregate belief" theorem doesn't hold; QMSR-family prices reflect beliefs but not via KL geometry +- **Domain constraint**: `|q_YES - q_NO| ≤ b` limits the representable price range for a given b. Mitigated by rebasing (reduces S without changing prices) and pair-split (routes around the pool at extremes). The constraint binds on skew, not volume — balanced trading can have arbitrarily high S. +- **Fixed b**: the operator must choose b at pool creation. If b is too small, the pool runs thin; if too large, more subsidy is locked than necessary. Adaptive liquidity comes from the LOB (organic maker participation) and operator b-adjustments (admin mechanism), not from the scoring rule. See "Adaptive liquidity without LS" above. +- **Weaker first-mover incentives**: the strongest argument for LMSR. Because the AMM serves as the source of truth for both current and historical prices, the speed at which informed traders update the AMM state directly determines price signal quality. LMSR's exponential cost curve creates dramatically stronger pressure to trade immediately; QMSR's linear cost curve creates only proportional urgency. This recommendation implicitly judges subsidy efficiency and implementation simplicity as more binding than price-discovery speed — if that weighting is wrong, LMSR's overhead may be worth paying. +- **No production deployments**: QMSR has not been deployed in any prediction market despite ~20 years of academic availability. The non-adoption analysis (see design journal) suggests path-dependency rather than fatal flaws, but the operational risk is real. -If LS-QMSR's verification doesn't pan out or its first-mover risk is judged unacceptable, **standard QMSR is the next-best choice**: same Simplicity-native implementation, same subsidy efficiency advantages over LMSR, but with the domain constraint as a real (though mitigable) operational concern and slightly worse subsidy efficiency than LS-QMSR. +**LMSR remains a strong alternative** for binary markets specifically. It is unconditionally strictly proper with the KL-divergence interpretation, has extensive production history, and has stronger first-mover incentives. The tradeoffs are Merkle table generation, larger per-swap witnesses (~3-5 kB vs ~2-3 kB), and worse subsidy efficiency (2.77× more collateral for equal depth). For binary markets where LMSR's 1D table is fully feasible, these are genuine implementation costs, not blockers. -LMSR remains a defensible choice if unconditional properness with the KL-divergence interpretation is weighted heavily and the implementation overhead (Merkle table generation, larger witnesses) is acceptable. +**LS-QMSR is not recommended** in its current form due to the 50% price-display bias. It could be revisited if: (a) a display-layer correction is validated (showing `2p - 1/N` instead of `p`, but this has edge-case issues near 0 and 1), or (b) a higher-degree polynomial scoring rule with smaller LS bias is developed (see open questions). -LS-LMSR is hard to recommend in this environment: the per-trade table invalidation makes it one dimensional step less practical than standard LMSR at every N. Binary is feasible but strictly inferior to standard LMSR's 1D table (same adaptive-b solution could come from other mechanisms at lower implementation cost); for N ≥ 4 it's impractical. +**LS-LMSR is hard to recommend** in this environment: the per-trade table invalidation makes it one dimensional step less practical than standard LMSR at every N, and its small price bias (though much better than LS-QMSR) means it's not unconditionally proper. Binary is feasible but strictly dominated by standard LMSR on implementation simplicity. ### For N-outcome markets (in this author's view) -For N-outcome markets with YES/NO per outcome, the LMSR family is increasingly problematic as N grows due to dimensional explosion. The practical choice is between: +For N-outcome markets with YES/NO per outcome, the LMSR family is increasingly problematic as N grows due to dimensional explosion. **Standard QMSR is the clear primary choice**: -- **QMSR family** (standard or LS-) for unified pools — natural fit, structural Σp = 1 -- **Composed binary pools** (LMSR-based or QMSR-based) — sacrifices structural coherence for per-outcome operator flexibility +- Single unified pool covering any N, polynomial arithmetic, structural Σp = 1 +- Strictly proper (in the unconstrained domain) — no price-display bias +- Bounded loss `b(N-1)/(2N)`, sub-linear in N, ~5× more efficient than unified LMSR at N=10 +- Domain constraint mitigated by rebasing and pair-split; becomes the main operational concern at higher N where extreme prices are more common -If structural coherence is valued (and the analysis suggests it should be for events where exactly one outcome wins), QMSR-family scoring rules are essentially required for N ≥ 4-5. +Composed binary pools (LMSR-based or QMSR-based) remain an option where per-outcome operator flexibility is valued, at the cost of structural coherence (Σp = 1 is arbitrage-enforced rather than structural). -Within the QMSR family, LS-QMSR has the same advantages over standard QMSR as in the binary case, plus the additional benefit that the no-domain-restriction property (α ≥ 1) becomes more valuable at higher N where boundary binding is more frequent. +LS-QMSR's advantages at higher N (no domain constraint for α ≥ 1, adaptive per-outcome depth) are real, but the 50% price-display bias is equally real and arguably more damaging at higher N where the displayable price range narrows to `[1/(2N), 1/2 + 1/(2N)]` (e.g., [0.05, 0.55] at N=10). Standard QMSR with rebasing is preferred. ### Process recommendation -1. **Verify LS-QMSR formally** before any implementation commitment. The math should be checked rigorously, including the bounded-loss derivation, properness proof, and path independence argument. +1. **Prototype standard QMSR** as the primary pool design for both binary and N-outcome markets. Standard QMSR's properties are well-established academically; an implementation can validate the Simplicity-feasibility claims and operational characteristics. -2. **Prototype standard QMSR first** as a fallback. Standard QMSR's properties are well-established academically; an implementation can validate the Simplicity-feasibility claims and operational characteristics independently of LS-QMSR's verification. +2. **Investigate higher-degree polynomial scoring rules** as a research direction for achieving LS-like adaptive liquidity with polynomial arithmetic and small (α-dependent) price bias. A cubic scoring rule with quadratic prices is the most promising candidate (see open questions). This is a research project, not a near-term implementation dependency. 3. **Defer the binary vs N-outcome contract decision** until pool implementations are validated. The pool choice and the contract choice are technically orthogonal but interact through the pool's role in the overall trader experience. +4. **Revisit LS-QMSR** only if either a validated display-layer correction or a mechanism-level fix (e.g., via a higher-degree base rule) can address the price-display bias. + --- ## Key Files From 12e96057257dadb9e32866d50e930fbe17c33606 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Wed, 15 Apr 2026 23:38:55 -0400 Subject: [PATCH 002/117] feat: on-chain market seeder script and relay-cache discovery fix - Add seed_markets example binary for publishing real on-chain testnet prediction markets via DeadcatNode. Supports publish/list/delete commands with 22 market definitions across 6 categories. Auto-splits UTXOs when only one is available. Uses DEADCAT_SOURCE_NSEC and DEADCAT_MNEMONIC env vars. - Fix market discovery: markets fetched from relays but not yet promoted in the store (candidates table) are cached in memory and merged with store results so they don't flash and disappear between queries. - Add discovery:market-refresh event to notify frontend when background relay fetch finds new markets. - Add wallet-seed-generation-entropy.md documenting the wallet's entropy sources and encryption pipeline. --- docs/wallet-seed-generation-entropy.md | 164 +++++ src-tauri/Cargo.lock | 1 + src-tauri/crates/deadcat-sdk/Cargo.toml | 5 + .../deadcat-sdk/examples/seed_markets.rs | 624 ++++++++++++++++++ src-tauri/src/commands.rs | 51 +- src/hooks/useTauriEvents.ts | 2 +- 6 files changed, 839 insertions(+), 8 deletions(-) create mode 100644 docs/wallet-seed-generation-entropy.md create mode 100644 src-tauri/crates/deadcat-sdk/examples/seed_markets.rs diff --git a/docs/wallet-seed-generation-entropy.md b/docs/wallet-seed-generation-entropy.md new file mode 100644 index 00000000..11399d8f --- /dev/null +++ b/docs/wallet-seed-generation-entropy.md @@ -0,0 +1,164 @@ +# Deadcat Wallet Seed Generation Entropy - Technical Explanation + +## Overview + +Deadcat is a Tauri-based desktop application for trading prediction markets on the Liquid Network. It implements a non-custodial wallet with industry-standard entropy sources and encryption practices for seed generation and storage. + +--- + +## Entropy Sources + +### 1. Primary Entropy: BIP39 Mnemonic Generation + +**Location**: `src-tauri/crates/deadcat-sdk/src/sdk.rs` (lines 454-458) + +```rust +pub fn generate_mnemonic(is_mainnet: bool) -> Result<(String, SwSigner)> { + let (signer, mnemonic) = + SwSigner::random(is_mainnet).map_err(|e| Error::Signer(e.to_string()))?; + Ok((mnemonic.to_string(), signer)) +} +``` + +**How it works**: +- Calls `SwSigner::random()` from Blockstream's Liquid Wallet Kit (`lwk_signer 0.14`) +- The `lwk_signer` library uses Rust's `rand` crate (v0.8) internally +- The `rand` crate sources entropy from the operating system: + - **Linux**: `getrandom` syscall or `/dev/urandom` + - **macOS**: `SecRandomCopyBytes` (Security framework) + - **Windows**: `RtlGenRandom` (BCryptGenRandom) +- Generates **128 bits** of entropy for a 12-word mnemonic (or 256 bits for 24 words) +- The entropy is converted to a BIP39 mnemonic using the standard word list + +### 2. Encryption Entropy: Salt and Nonce Generation + +**Location**: `src-tauri/src/wallet/persister.rs` (lines 88-116) + +When the mnemonic is saved to disk, two additional random values are generated: + +```rust +pub fn save(&self, mnemonic: &str, password: &str) -> Result<(), WalletPersistError> { + let salt: [u8; 16] = rand::random(); // 128 bits of random salt + + // ... Argon2 key derivation ... + + let nonce_bytes: [u8; 12] = rand::random(); // 96 bits of random nonce + // ... AES-256-GCM encryption ... +} +``` + +| Random Value | Size | Purpose | +|--------------|------|---------| +| Salt | 16 bytes (128 bits) | Argon2 key derivation function input | +| Nonce | 12 bytes (96 bits) | AES-GCM initialization vector | + +Both use `rand::random()` which provides cryptographically secure random bytes from the OS. + +--- + +## Seed-to-Key Derivation Process + +### Step 1: Entropy to Mnemonic (BIP39) +1. 128 bits of random entropy are generated +2. A checksum (4 bits for 12 words) is appended +3. The 132 bits are split into 12 groups of 11 bits each +4. Each 11-bit value indexes into the BIP39 word list (2048 words) +5. Result: 12 human-readable words + +### Step 2: Mnemonic to Seed (BIP39) +1. The mnemonic words + optional passphrase are fed into PBKDF2-HMAC-SHA512 +2. 2048 iterations produce a 512-bit seed + +### Step 3: Seed to HD Keys (BIP32) +1. The seed is used to derive a master private key and chain code +2. Hierarchical derivation creates child keys for different purposes +3. Liquid-specific derivation paths are used (via `lwk_wollet`) + +--- + +## Mnemonic Encryption at Rest + +When the wallet is created, the mnemonic is encrypted before storage: + +### Encryption Pipeline + +``` +User Password + │ + ▼ +┌─────────────────────┐ +│ Argon2 KDF │ ← Random 16-byte salt +│ (memory-hard) │ +└─────────────────────┘ + │ + ▼ +256-bit AES Key + │ + ▼ +┌─────────────────────┐ +│ AES-256-GCM │ ← Random 12-byte nonce +│ (authenticated) │ +└─────────────────────┘ + │ + ▼ +Encrypted Mnemonic + Auth Tag +``` + +### Storage Format +Stored in `{app_data_dir}/{network}/wallet_encrypted.json`: +```json +{ + "salt": "base64-encoded-16-bytes", + "nonce": "base64-encoded-12-bytes", + "ciphertext": "base64-encoded-encrypted-mnemonic" +} +``` + +--- + +## Cryptographic Library Versions + +| Library | Version | Purpose | +|---------|---------|---------| +| `rand` | 0.8 | Cryptographic random number generation | +| `lwk_signer` | 0.14 | Blockstream's Liquid Wallet Kit (BIP39/BIP32) | +| `bip39` | 2.x | BIP39 mnemonic validation | +| `argon2` | 0.5 | Memory-hard password key derivation | +| `aes-gcm` | 0.10 | Authenticated encryption | + +--- + +## Security Characteristics + +### Strengths +1. **OS-level entropy**: Uses cryptographically secure random number generators from the operating system kernel +2. **Memory-hard KDF**: Argon2 resists brute-force attacks with GPU/ASIC hardware +3. **Authenticated encryption**: AES-GCM provides both confidentiality and integrity +4. **Unique salt per wallet**: Prevents rainbow table attacks +5. **Secure memory clearing**: Uses `Zeroizing` wrapper to clear mnemonic from memory +6. **Industry-standard derivation**: BIP39/BIP32 compatibility allows recovery in other wallets + +### Considerations +- Password minimum is 8 characters (enforced in UI) with no complexity requirements +- Entropy quality depends entirely on the underlying OS random source +- No hardware security module (HSM) integration for key storage + +--- + +## Entropy Summary Table + +| Stage | Source | Entropy (bits) | Purpose | +|-------|--------|----------------|---------| +| Mnemonic | OS RNG via `lwk_signer` | 128 (12 words) | Wallet master seed | +| Encryption salt | OS RNG via `rand::random()` | 128 | Argon2 KDF salt | +| Encryption nonce | OS RNG via `rand::random()` | 96 | AES-GCM IV | +| Address derivation | Deterministic from seed | N/A | HD key paths | + +--- + +## Key Files + +- **Mnemonic generation**: `src-tauri/crates/deadcat-sdk/src/sdk.rs:454-458` +- **Encryption/save**: `src-tauri/src/wallet/persister.rs:88-116` +- **Decryption/load**: `src-tauri/src/wallet/persister.rs:145-177` +- **Tauri commands**: `src-tauri/src/lib.rs:183-280` diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e269e237..1f9f3670 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1537,6 +1537,7 @@ dependencies = [ "nostr-sdk", "rand 0.8.5", "rayon", + "rustls", "serde", "serde_json", "sha2", diff --git a/src-tauri/crates/deadcat-sdk/Cargo.toml b/src-tauri/crates/deadcat-sdk/Cargo.toml index 7449b62a..91002f14 100644 --- a/src-tauri/crates/deadcat-sdk/Cargo.toml +++ b/src-tauri/crates/deadcat-sdk/Cargo.toml @@ -28,8 +28,13 @@ testing = ["simplicity-sys/test-utils"] version = "0.6" optional = true +[[example]] +name = "seed_markets" +required-features = ["testing"] + [dev-dependencies] deadcat-sdk = { path = ".", features = ["testing"] } +rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } deadcat-store = { path = "deadcat-store" } simplicity-sys = { version = "0.6", features = ["test-utils"] } bitcoincore-rpc = "0.19" diff --git a/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs new file mode 100644 index 00000000..aee5a50f --- /dev/null +++ b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs @@ -0,0 +1,624 @@ +//! Seed testnet prediction markets with real on-chain contracts. +//! +//! Usage: +//! cargo run --example seed_markets --features testing -- publish +//! cargo run --example seed_markets --features testing -- publish 1 (single market) +//! cargo run --example seed_markets --features testing -- list +//! cargo run --example seed_markets --features testing -- delete +//! +//! Required environment variables: +//! DEADCAT_SOURCE_NSEC — nsec for the source npub (signs events + acts as oracle) +//! DEADCAT_MNEMONIC — BIP-39 mnemonic for a funded Liquid testnet wallet +//! +//! Optional: +//! DEADCAT_ELECTRUM_URL — override default testnet Electrum (ssl://blockstream.info:465) +//! DEADCAT_DATADIR — wallet data directory (default: /tmp/deadcat-seed) + +use std::sync::Arc; +use std::time::Duration; + +// Ensure rustls uses the ring crypto backend (matches the main app's Cargo.toml). +use rustls as _; +fn install_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +use deadcat_sdk::{ + ContractMetadata, DeadcatNode, DiscoveryConfig, MinerFeePolicy, Network, NoopStore, TxOptions, + DEFAULT_RELAYS, NETWORK_TAG, +}; +use nostr_sdk::prelude::*; + +// --------------------------------------------------------------------------- +// Market definitions +// --------------------------------------------------------------------------- + +struct MarketDef { + question: &'static str, + description: &'static str, + category: &'static str, + resolution_source: &'static str, + /// Collateral per token pair in sats + cpt_sats: u64, + /// Settlement deadline as Unix timestamp + settlement_unix: u64, +} + +// Settlement ~6 months from mid-2026 +const DEC_2026_UNIX: u64 = 1_798_761_600; // 2027-01-01 00:00 UTC +const JUN_2027_UNIX: u64 = 1_814_400_000; // ~mid 2027 + +const MARKETS: &[MarketDef] = &[ + // Bitcoin + MarketDef { + question: "Will Bitcoin exceed $200k by end of 2026?", + description: "Resolves YES if BTC/USD spot price on any major exchange exceeds $200,000 at any point before January 1, 2027 00:00 UTC.", + category: "Bitcoin", + resolution_source: "Coinbase spot price", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Bitcoin dominance drop below 50% in 2026?", + description: "Resolves YES if Bitcoin market cap dominance (per CoinGecko) falls below 50% at any point during 2026.", + category: "Bitcoin", + resolution_source: "CoinGecko dominance chart", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will a Bitcoin spot ETF surpass $100B AUM?", + description: "Resolves YES if any single US-listed Bitcoin spot ETF reports assets under management exceeding $100 billion.", + category: "Bitcoin", + resolution_source: "Bloomberg ETF data", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the Bitcoin halving cycle peak occur before Q3 2026?", + description: "Resolves YES if BTC all-time high for this halving cycle is set before July 1, 2026.", + category: "Bitcoin", + resolution_source: "CoinGecko ATH tracker", + cpt_sats: 5000, + settlement_unix: 1_719_792_000, // 2026-07-01 + }, + // Politics + MarketDef { + question: "Will the US pass stablecoin legislation in 2026?", + description: "Resolves YES if a federal stablecoin bill is signed into law by the US President before January 1, 2027.", + category: "Politics", + resolution_source: "Congress.gov", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the EU implement MiCA enforcement actions in 2026?", + description: "Resolves YES if any EU member state issues formal enforcement action under MiCA regulation during 2026.", + category: "Politics", + resolution_source: "ESMA press releases", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will a G7 country adopt a Bitcoin strategic reserve?", + description: "Resolves YES if any G7 nation officially announces a government-held Bitcoin reserve by end of 2026.", + category: "Politics", + resolution_source: "Government press releases", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will El Salvador repay its 2027 bonds early?", + description: "Resolves YES if El Salvador repays or retires its January 2027 sovereign bonds before their maturity date.", + category: "Politics", + resolution_source: "Bloomberg bond data", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Sports + MarketDef { + question: "Will Real Madrid win the 2025-26 Champions League?", + description: "Resolves YES if Real Madrid CF wins the UEFA Champions League final in the 2025-26 season.", + category: "Sports", + resolution_source: "UEFA official results", + cpt_sats: 5000, + settlement_unix: 1_717_200_000, // ~Jun 2026 + }, + MarketDef { + question: "Will the Kansas City Chiefs three-peat at Super Bowl LXI?", + description: "Resolves YES if the Kansas City Chiefs win Super Bowl LXI (February 2027).", + category: "Sports", + resolution_source: "NFL official results", + cpt_sats: 5000, + settlement_unix: 1_739_232_000, // ~Feb 2027 + }, + MarketDef { + question: "Will a new 100m world record be set in 2026?", + description: "Resolves YES if a new men's 100m sprint world record (sub-9.58s) is ratified by World Athletics during 2026.", + category: "Sports", + resolution_source: "World Athletics records page", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Lewis Hamilton win a race for Ferrari in 2026?", + description: "Resolves YES if Lewis Hamilton wins at least one Formula 1 Grand Prix while driving for Scuderia Ferrari in 2026.", + category: "Sports", + resolution_source: "FIA official race results", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Culture + MarketDef { + question: "Will a Nostr client reach 10M monthly active users?", + description: "Resolves YES if any Nostr-based client reports or is independently verified to have 10 million monthly active users.", + category: "Culture", + resolution_source: "App store analytics / Nostr relay stats", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will an AI model pass the Turing test in 2026?", + description: "Resolves YES if a peer-reviewed study published in 2026 demonstrates an AI system passing a standard Turing test with >70% of judges unable to distinguish it from a human.", + category: "Culture", + resolution_source: "Peer-reviewed publication", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will a major studio release a fully AI-generated film?", + description: "Resolves YES if a major Hollywood studio releases a theatrical film where the primary creative content (script, visuals, audio) is AI-generated.", + category: "Culture", + resolution_source: "Studio press release / industry reporting", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Wikipedia adopt Nostr for contributor identity?", + description: "Resolves YES if the Wikimedia Foundation officially integrates Nostr-based identity for editor accounts.", + category: "Culture", + resolution_source: "Wikimedia Foundation announcements", + cpt_sats: 5000, + settlement_unix: JUN_2027_UNIX, + }, + // Weather + MarketDef { + question: "Will 2026 be the hottest year on record?", + description: "Resolves YES if NASA GISS or NOAA declares 2026 as the warmest year in their instrumental record.", + category: "Weather", + resolution_source: "NASA GISS / NOAA annual report", + cpt_sats: 5000, + settlement_unix: 1_801_353_600, // 2027-02-01 + }, + MarketDef { + question: "Will a Category 6 hurricane classification be adopted?", + description: "Resolves YES if NOAA or WMO officially adopts a Category 6 designation for the Saffir-Simpson Hurricane Wind Scale.", + category: "Weather", + resolution_source: "NOAA / WMO announcements", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Macro + MarketDef { + question: "Will the Fed cut rates below 3% by end of 2026?", + description: "Resolves YES if the Federal Reserve's target federal funds rate upper bound is below 3.00% on December 31, 2026.", + category: "Macro", + resolution_source: "Federal Reserve press releases", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will gold exceed $3,500/oz in 2026?", + description: "Resolves YES if the LBMA PM gold fix exceeds $3,500 per troy ounce at any point during 2026.", + category: "Macro", + resolution_source: "LBMA gold price", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will US national debt exceed $40 trillion?", + description: "Resolves YES if the US total public debt outstanding exceeds $40 trillion as reported by the Treasury.", + category: "Macro", + resolution_source: "US Treasury Debt to the Penny", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Ethereum flip Bitcoin in market cap?", + description: "Resolves YES if Ethereum's market capitalization exceeds Bitcoin's at any point during 2026 per CoinGecko.", + category: "Bitcoin", + resolution_source: "CoinGecko market cap", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn load_keys() -> Keys { + let nsec = std::env::var("DEADCAT_SOURCE_NSEC") + .expect("DEADCAT_SOURCE_NSEC env var required (nsec1...)"); + let secret = SecretKey::from_bech32(nsec.trim()).expect("invalid DEADCAT_SOURCE_NSEC"); + Keys::new(secret) +} + +fn oracle_pubkey_bytes(keys: &Keys) -> [u8; 32] { + let hex_str = keys.public_key().to_hex(); + let bytes = hex::decode(&hex_str).expect("pubkey hex decode"); + bytes.try_into().expect("pubkey must be 32 bytes") +} + +fn default_tx_options() -> TxOptions { + TxOptions { + fee_policy: MinerFeePolicy::RateSatPerVb { sat_per_vb: 0.5 }, + } +} + +fn electrum_url() -> String { + std::env::var("DEADCAT_ELECTRUM_URL") + .unwrap_or_else(|_| "ssl://blockstream.info:465".to_string()) +} + +fn datadir() -> std::path::PathBuf { + std::env::var("DEADCAT_DATADIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::path::PathBuf::from("/tmp/deadcat-seed")) +} + +/// Convert a settlement Unix timestamp to a block height estimate. +/// Liquid produces a block roughly every minute. +fn unix_to_block_height(settlement_unix: u64, current_height: u32, current_unix: u64) -> u32 { + if settlement_unix <= current_unix { + return current_height + 1; + } + let seconds_until = settlement_unix - current_unix; + let blocks_until = (seconds_until / 60) as u32; + current_height + blocks_until +} + +async fn setup_node(keys: Keys) -> Arc> { + let config = DiscoveryConfig { + relays: DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect(), + network_tag: NETWORK_TAG.to_string(), + source_author: Some(keys.public_key()), + ..Default::default() + }; + + let (node, _rx) = DeadcatNode::new(keys, Network::LiquidTestnet, config); + let node = Arc::new(node); + + // Unlock wallet + let mnemonic = std::env::var("DEADCAT_MNEMONIC") + .expect("DEADCAT_MNEMONIC env var required (12-word BIP-39 phrase)"); + let electrum = electrum_url(); + let dir = datadir(); + + println!("Unlocking wallet (electrum: {electrum})..."); + let node_clone = node.clone(); + let handle = tokio::task::spawn_blocking(move || { + node_clone + .unlock_wallet(&mnemonic, &electrum, &dir) + .expect("failed to unlock wallet"); + }); + handle.await.expect("wallet unlock task failed"); + + // Show balance + let balance = node.balance().unwrap_or_default(); + let policy_asset = Network::LiquidTestnet.into_lwk().policy_asset(); + let lbtc_sats = balance.get(&policy_asset).copied().unwrap_or(0); + println!("Wallet balance: {} sats ({:.8} tL-BTC)", lbtc_sats, lbtc_sats as f64 / 100_000_000.0); + + // Check if we have enough UTXOs — market creation needs at least 2 + let utxo_count = node + .utxos() + .unwrap_or_default() + .iter() + .filter(|u| u.unblinded.asset == policy_asset) + .count(); + + if utxo_count < 2 && lbtc_sats > 2000 { + println!("Only {} L-BTC UTXO(s) — splitting for market creation...", utxo_count); + let self_addr = node.address(None).await.expect("address").address().to_string(); + let split_amount = lbtc_sats / 2; + match node.send_lbtc(self_addr, split_amount, default_tx_options()).await { + Ok((txid, fee)) => { + println!("Split tx: {} (fee: {} sats)", txid, fee); + println!("Waiting for confirmation..."); + tokio::time::sleep(Duration::from_secs(5)).await; + if let Err(e) = node.sync_wallet().await { + eprintln!("Warning: sync after split failed: {e}"); + } + let balance = node.balance().unwrap_or_default(); + let lbtc_sats = balance.get(&policy_asset).copied().unwrap_or(0); + println!("Balance after split: {} sats", lbtc_sats); + } + Err(e) => { + eprintln!("Warning: failed to split UTXOs: {e}"); + eprintln!("Market creation may fail. Fund wallet with more UTXOs."); + } + } + } + println!(); + + // Start Nostr subscription + if let Err(e) = node.start_subscription().await { + eprintln!("Warning: failed to start Nostr subscription: {e}"); + } + + node +} + +// --------------------------------------------------------------------------- +// Commands +// --------------------------------------------------------------------------- + +async fn cmd_publish(keys: &Keys, count: Option) { + let node = setup_node(keys.clone()).await; + let oracle_pk = oracle_pubkey_bytes(keys); + let max = count.unwrap_or(MARKETS.len()).min(MARKETS.len()); + + // Get current chain tip for block height estimation + let current_height = 2_800_000u32; // approximate Liquid testnet height + let current_unix = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + println!("Creating {} market(s)...\n", max); + + for (i, market) in MARKETS.iter().take(max).enumerate() { + let expiry_height = unix_to_block_height(market.settlement_unix, current_height, current_unix); + let metadata = ContractMetadata { + question: market.question.to_string(), + description: market.description.to_string(), + category: market.category.to_string(), + resolution_source: market.resolution_source.to_string(), + }; + + println!("[{}/{}] Creating: {}", i + 1, max, market.question); + + // Prepare market creation + let prepared = match node + .prepare_create_market( + oracle_pk, + market.cpt_sats, + expiry_height, + 546, // min UTXO value (dust limit) + default_tx_options(), + metadata, + ) + .await + { + Ok(p) => p, + Err(e) => { + eprintln!(" FAILED to prepare: {e}"); + continue; + } + }; + + // Broadcast + announce on Nostr + let result = match node.broadcast_prepared_market_creation(prepared).await { + Ok(r) => r, + Err(e) => { + eprintln!(" FAILED to broadcast: {e}"); + continue; + } + }; + + println!(" Market ID: {}", result.market.market_id); + println!(" Creation TX: {}", result.market.anchor.creation_txid); + println!(" Event ID: {}", result.market.id); + println!(" Fee: {} sats", result.fee.amount_sat); + println!(" Category: {}", market.category); + println!(); + + // Sync wallet between markets so UTXOs are up to date + if i + 1 < max { + println!(" Syncing wallet..."); + tokio::time::sleep(Duration::from_secs(3)).await; + if let Err(e) = node.sync_wallet().await { + eprintln!(" Warning: wallet sync failed: {e}"); + } + } + } + + // Final balance + let balance = node.balance().unwrap_or_default(); + let policy_asset = Network::LiquidTestnet.into_lwk().policy_asset(); + let lbtc_sats = balance.get(&policy_asset).copied().unwrap_or(0); + println!("Remaining balance: {} sats\n", lbtc_sats); + println!("Done."); +} + +async fn cmd_list(keys: &Keys) { + let client = { + let c = Client::default(); + for url in DEFAULT_RELAYS { + let _ = c.add_relay(*url).await; + } + c.connect_with_timeout(Duration::from_secs(5)).await; + c + }; + + let pubkey = keys.public_key(); + let filter = Filter::new() + .kind(Kind::Custom(30078)) + .hashtag("deadcat-contract") + .author(pubkey); + + println!( + "Fetching markets from {} ...\n", + pubkey.to_bech32().unwrap_or_default() + ); + + match client + .fetch_events(vec![filter], Duration::from_secs(10)) + .await + { + Ok(events) => { + if events.is_empty() { + println!("No markets found."); + return; + } + for (i, event) in events.iter().enumerate() { + let d_tag = event + .tags + .iter() + .find_map(|t| { + let s = t.as_slice(); + if s.len() >= 2 && s[0] == "d" { + Some(s[1].clone()) + } else { + None + } + }) + .unwrap_or_default(); + + let question = serde_json::from_str::(&event.content) + .ok() + .and_then(|v| { + v.get("metadata") + .and_then(|m| m.get("question")) + .and_then(|q| q.as_str()) + .map(String::from) + }) + .unwrap_or_else(|| "(unparseable)".to_string()); + + println!( + "[{:2}] {} | d={}... | {}", + i + 1, + &event.id.to_hex()[..12], + &d_tag[..16.min(d_tag.len())], + question, + ); + } + println!("\nTotal: {} markets", events.iter().count()); + } + Err(e) => eprintln!("Failed to fetch: {e}"), + } +} + +async fn cmd_delete(keys: &Keys) { + let client = { + let c = Client::default(); + for url in DEFAULT_RELAYS { + let _ = c.add_relay(*url).await; + } + c.connect_with_timeout(Duration::from_secs(5)).await; + c + }; + + let pubkey = keys.public_key(); + let filter = Filter::new() + .kind(Kind::Custom(30078)) + .hashtag("deadcat-contract") + .author(pubkey); + + println!("Fetching existing markets to delete...\n"); + + let events = match client + .fetch_events(vec![filter], Duration::from_secs(10)) + .await + { + Ok(events) => events, + Err(e) => { + eprintln!("Failed to fetch: {e}"); + return; + } + }; + + if events.is_empty() { + println!("No markets found."); + return; + } + + let count = events.iter().count(); + println!( + "Found {} markets. Publishing tombstone replacements...\n", + count + ); + + for event in events.iter() { + let d_tag = event + .tags + .iter() + .find_map(|t| { + let s = t.as_slice(); + if s.len() >= 2 && s[0] == "d" { + Some(s[1].clone()) + } else { + None + } + }) + .unwrap_or_default(); + + let tombstone = EventBuilder::new(Kind::Custom(30078), "") + .tags(vec![ + Tag::identifier(&d_tag), + Tag::custom(TagKind::custom("deleted"), vec!["true".to_string()]), + ]) + .sign_with_keys(keys) + .expect("failed to sign tombstone"); + + match client.send_event(tombstone).await { + Ok(_) => println!( + " Deleted: d={}...", + &d_tag[..16.min(d_tag.len())] + ), + Err(e) => eprintln!( + " FAILED to delete d={}...: {e}", + &d_tag[..16.min(d_tag.len())] + ), + } + } + println!("\nDeleted {} markets.", count); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +#[tokio::main] +async fn main() { + install_crypto_provider(); + let args: Vec = std::env::args().collect(); + let command = args.get(1).map(|s| s.as_str()).unwrap_or("help"); + + if command == "help" { + print_usage(); + return; + } + + let keys = load_keys(); + println!( + "Source: {}\n", + keys.public_key().to_bech32().unwrap_or_default() + ); + + match command { + "publish" => { + let count = args.get(2).and_then(|s| s.parse::().ok()); + cmd_publish(&keys, count).await; + } + "list" => cmd_list(&keys).await, + "delete" => cmd_delete(&keys).await, + _ => print_usage(), + } +} + +fn print_usage() { + eprintln!("Usage: seed_markets [args]"); + eprintln!(); + eprintln!("Commands:"); + eprintln!(" publish [N] Create N markets on-chain + announce on Nostr (default: all {}))", MARKETS.len()); + eprintln!(" list List markets published by the source npub"); + eprintln!(" delete Delete all markets by publishing tombstone events"); + eprintln!(); + eprintln!("Environment:"); + eprintln!(" DEADCAT_SOURCE_NSEC nsec for the source npub (required)"); + eprintln!(" DEADCAT_MNEMONIC BIP-39 mnemonic for funded testnet wallet (required for publish)"); + eprintln!(" DEADCAT_ELECTRUM_URL Electrum server (default: ssl://blockstream.info:465)"); + eprintln!(" DEADCAT_DATADIR Wallet data dir (default: /tmp/deadcat-seed)"); +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 25ae24a7..a4acbd2a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,6 +18,12 @@ use crate::{NodeState, NostrAppState}; const ORDER_INDEX_AUTO_RESOLVE_SENTINEL: u32 = u32::MAX; +/// Cache of markets fetched from relays but not yet promoted in the store. +/// Persists across `discover_contracts` calls so markets don't flash and disappear. +static RELAY_MARKET_CACHE: std::sync::LazyLock< + std::sync::Mutex>, +> = std::sync::LazyLock::new(|| std::sync::Mutex::new(Vec::new())); + fn validate_request(request: &CreateContractRequest) -> Result<(), String> { if request.question.trim().is_empty() || request.question.len() > 140 { return Err("question must be 1-140 characters".to_string()); @@ -984,25 +990,50 @@ pub fn open_downloads_folder() -> Result<(), String> { #[tauri::command] pub async fn discover_contracts(app: tauri::AppHandle) -> Result, String> { - // Return cached store data immediately so the UI is responsive. - let result = list_contracts(app.clone()); + // Start with store data. + let mut result = list_contracts(app.clone()).unwrap_or_default(); + let store_ids: std::collections::HashSet = + result.iter().map(|m| m.market_id.clone()).collect(); + + // Merge any cached relay markets not yet in the store. + if let Ok(cache) = RELAY_MARKET_CACHE.lock() { + for m in cache.iter() { + if !store_ids.contains(&m.market_id) { + result.push(m.clone()); + } + } + } - // Fetch fresh data from Nostr relays in the background. - // New markets will appear on next render/sync cycle. + // Fetch from Nostr relays in the background and update the cache. let node = { let node_state = app.state::(); let guard = node_state.node.lock().await; guard.as_ref().cloned() }; if let Some(node) = node { + let app_handle = app.clone(); tauri::async_runtime::spawn(async move { - if let Err(e) = node.fetch_markets().await { - log::warn!("Background Nostr fetch failed: {e}"); + if let Ok(markets) = node.fetch_markets().await { + if let Ok(mut cache) = RELAY_MARKET_CACHE.lock() { + let cached_ids: std::collections::HashSet = + cache.iter().map(|m| m.market_id.clone()).collect(); + let mut new_count = 0; + for m in markets { + if !cached_ids.contains(&m.market_id) { + cache.push(m); + new_count += 1; + } + } + if new_count > 0 { + // Signal one refresh — won't loop because cache now contains these markets + let _ = app_handle.emit("discovery:market-refresh", ()); + } + } } }); } - result + Ok(result) } #[tauri::command] @@ -2310,6 +2341,12 @@ pub fn list_contracts(app: tauri::AppHandle) -> Result, St .list_markets(&MarketFilter::default()) .map_err(|e| format!("list markets: {e}"))?; + eprintln!("[debug] list_contracts: store has {} markets, source_pubkey_hex={:?}", infos.len(), source_pubkey_hex.as_deref().map(|s| &s[..16])); + for info in &infos { + let cpk = info.creator_pubkey.as_ref().map(|pk| hex::encode(pk)); + eprintln!("[debug] market creator_pubkey={:?}", cpk.as_deref().map(|s| &s[..16])); + } + // Filter by source author if configured let infos: Vec<_> = if let Some(ref author_hex) = source_pubkey_hex { infos diff --git a/src/hooks/useTauriEvents.ts b/src/hooks/useTauriEvents.ts index 768d5563..34525630 100644 --- a/src/hooks/useTauriEvents.ts +++ b/src/hooks/useTauriEvents.ts @@ -96,7 +96,7 @@ export function useTauriEvents(): void { ); // ── discovery events ───────────────────────────────────────── - for (const eventName of ["discovery:market", "discovery:attestation"]) { + for (const eventName of ["discovery:market", "discovery:market-refresh", "discovery:attestation"]) { register( listen(eventName, () => { if (disposed) return; From 17d0a001538098fff4da8c9fd6e93700e38cc05b Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 07:38:30 -0400 Subject: [PATCH 003/117] docs: move wallet entropy doc to architecture folder --- docs/{ => architecture}/wallet-seed-generation-entropy.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => architecture}/wallet-seed-generation-entropy.md (100%) diff --git a/docs/wallet-seed-generation-entropy.md b/docs/architecture/wallet-seed-generation-entropy.md similarity index 100% rename from docs/wallet-seed-generation-entropy.md rename to docs/architecture/wallet-seed-generation-entropy.md From b1300af5b1c4b07211c3a4ccd59d52ee06d18b6a Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 07:46:38 -0400 Subject: [PATCH 004/117] fix: cat loader for market fetch, remove unused nav and help UI Replace skeleton loading state with cat bag animation while markets are fetched from relays. Use React Query isLoading instead of manual marketsLoading flag. Remove dead Markets/Live/Social nav links and Help button/modal. Center search bar in header. --- src/components/home/HomePage.tsx | 104 ++++++++++++----------------- src/components/layout/TopShell.tsx | 90 ++----------------------- 2 files changed, 49 insertions(+), 145 deletions(-) diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index ff255cfe..22239ce5 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -166,74 +166,55 @@ function SidebarMarketItem({ ); } -// ── Loading skeleton ───────────────────────────────────────────────── -function HomeSkeleton() { +// ── Market loader (cat bag animation) ──────────────────────────────── +function MarketLoader() { return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
-
-
-
-
- ))} -
+
+
- +

+ Fetching markets... +

); } +function CatLoaderSvg() { + return ( + + ); +} + +function BagLoaderSvg() { + return ( + + ); +} + // ── Empty state ────────────────────────────────────────────────────── function EmptyState() { const walletStatus = useStore((s) => s.walletStatus); @@ -1003,10 +984,9 @@ function TrendingHomeView({ markets }: { markets: Market[] }) { export default function HomePage() { const activeCategory = useStore((s) => s.activeCategory); const search = useStore((s) => s.search); - const marketsLoading = useStore((s) => s.marketsLoading); const nostrPubkey = useStore((s) => s.nostrPubkey); - const { data: markets = [] } = useMarkets(); + const { data: markets = [], isLoading } = useMarkets(); // Determine which view to render based on activeCategory const isTrendingLike = @@ -1051,8 +1031,8 @@ export default function HomePage() { } // Trending (default home) - if (marketsLoading && markets.length === 0) { - return ; + if (isLoading && markets.length === 0) { + return ; } return ; diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index cd01a25c..2dc9e91e 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -118,44 +118,6 @@ export function categoryIcon( } } -/* ── Help modal ────────────────────────────────────────────────────── */ - -function HelpModal() { - const helpOpen = useStore((s) => s.helpOpen); - if (!helpOpen) return null; - - return ( -
-
-
-

Help

- -
-

Help content coming soon.

-
-
- ); -} - /* ── Logout modal ──────────────────────────────────────────────────── */ function LogoutModal() { @@ -489,26 +451,7 @@ function CategoryBar() { ); })} - + {/* Help button — hidden until content is ready */}
@@ -670,7 +613,6 @@ function Logo() { /* ── Main TopShell component ───────────────────────────────────────── */ export function TopShell() { - const view = useStore((s) => s.view); const nostrPubkey = useStore((s) => s.nostrPubkey); const activeCategory = useStore((s) => s.activeCategory); @@ -692,30 +634,13 @@ export function TopShell() { - {/* Nav links */} - - - {/* Right side: search + wallet + user menu */} -
+ {/* Center: search bar */} +
+
+ + {/* Right side: wallet + user menu */} +
{nostrPubkey && (
@@ -151,7 +161,17 @@ function TransactionRow({
Status - {confirmations} + = 0 + ? "text-emerald-400/80" + : "text-red-400/80" + : "text-slate-400" + }`} + > + {confirmations} +
diff --git a/src/components/wallet/WalletUnlocked.tsx b/src/components/wallet/WalletUnlocked.tsx index 78a0b4a8..cad8c08d 100644 --- a/src/components/wallet/WalletUnlocked.tsx +++ b/src/components/wallet/WalletUnlocked.tsx @@ -56,6 +56,11 @@ export function WalletUnlocked({ ? (walletData.balance[walletPolicyAssetId] ?? 0) : 0; + const hasUnconfirmed = useMemo( + () => (walletData?.transactions ?? []).some((tx) => tx.height == null), + [walletData?.transactions], + ); + // Map asset IDs to labels const assetLabel = useMemo(() => { const map = new Map(); @@ -333,7 +338,9 @@ export function WalletUnlocked({ {walletBalanceHidden ? ( ) : ( - + {formatLbtc(policyBalance, walletUnit, showLbtcLabel)} )} diff --git a/src/hooks/useTauriEvents.ts b/src/hooks/useTauriEvents.ts index 830db708..ff824b1e 100644 --- a/src/hooks/useTauriEvents.ts +++ b/src/hooks/useTauriEvents.ts @@ -77,16 +77,25 @@ export function useTauriEvents(): void { backupCopied: false, }; - // Toast for new transactions - if (base.transactions.length > 0) { - const knownTxids = new Set(base.transactions.map((t) => t.txid)); + // Detect new transactions for toasts + badge + const { walletSeenTxids, walletNewTxids } = useStore.getState(); + if (walletSeenTxids.size > 0) { + const freshNew = new Set(walletNewTxids); for (const tx of payload.transactions) { - if (!knownTxids.has(tx.txid)) { + if (!walletSeenTxids.has(tx.txid)) { toastForNewTx(tx); + freshNew.add(tx.txid); } } + if (freshNew.size !== walletNewTxids.size) { + useStore.setState({ walletNewTxids: freshNew }); + } } + // Update seen set to current txid list + const nextSeen = new Set(payload.transactions.map((t) => t.txid)); + useStore.setState({ walletSeenTxids: nextSeen }); + useStore.setState({ walletData: { ...base, @@ -111,7 +120,11 @@ export function useTauriEvents(): void { if (current !== "not_created") { useStore.setState({ walletStatus: "locked" }); } - useStore.setState({ walletData: null }); + useStore.setState({ + walletData: null, + walletSeenTxids: new Set(), + walletNewTxids: new Set(), + }); void queryClient.invalidateQueries({ queryKey: ["walletSnapshot"], }); diff --git a/src/store/index.ts b/src/store/index.ts index 63b0bc27..8222c46e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -266,6 +266,14 @@ export interface ChartUiSlice { chartAspectDetail: number; } +// ── Wallet badge slice ────────────────────────────────────────────── +export interface WalletBadgeSlice { + /** Txids the user has already seen (populated on each snapshot). */ + walletSeenTxids: Set; + /** Txids that appeared since last dismissal — shown as green badge. */ + walletNewTxids: Set; +} + // ── Persistence ───────────────────────────────────────────────────── export interface PersistenceSlice { recentTxLabels: Map; @@ -279,6 +287,7 @@ export type StoreState = NavigationSlice & MarketCreationSlice & OnboardingSlice & ChartUiSlice & + WalletBadgeSlice & PersistenceSlice; const inThirtyDays = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); @@ -492,6 +501,10 @@ export const useStore = create()(() => ({ chartAspectHome: 2.8, chartAspectDetail: 3.35, + // ── Wallet badge ──────────────────────────────────────────────── + walletSeenTxids: new Set(), + walletNewTxids: new Set(), + // ── Persistence ───────────────────────────────────────────────── recentTxLabels: new Map(), })); From bfb2edab33dd1a533f5a17a5c37395e7114bdc8e Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:09:48 -0400 Subject: [PATCH 009/117] fix: relay cache replacement and empty-response guard Replace append-only relay market cache with full replacement so deleted markets disappear on next discovery cycle. Skip cache replacement when relays return empty results to prevent market flashing during transient failures. --- src-tauri/src/commands.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a4acbd2a..2942feac 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1014,18 +1014,20 @@ pub async fn discover_contracts(app: tauri::AppHandle) -> Result = + let old_ids: std::collections::HashSet = cache.iter().map(|m| m.market_id.clone()).collect(); - let mut new_count = 0; - for m in markets { - if !cached_ids.contains(&m.market_id) { - cache.push(m); - new_count += 1; - } - } - if new_count > 0 { - // Signal one refresh — won't loop because cache now contains these markets + let new_ids: std::collections::HashSet = + markets.iter().map(|m| m.market_id.clone()).collect(); + let changed = old_ids != new_ids; + *cache = markets; + if changed { let _ = app_handle.emit("discovery:market-refresh", ()); } } From 47127dd816ff666a13319afba53159f77b0d93b4 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:09:55 -0400 Subject: [PATCH 010/117] fix: chain tip for time remaining and human-readable chart axis Fetch real chain tip in useMarkets so time-remaining shows correct months instead of raw block distance. Convert chart x-axis from block heights to timestamps and rename timescale buttons from block counts to minutes. --- src/components/chart/MarketChart.tsx | 24 ++++++++++------ src/queries/useMarkets.ts | 23 +++++++++++++-- src/utils-react/chart-series.ts | 43 +++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/components/chart/MarketChart.tsx b/src/components/chart/MarketChart.tsx index 46c6a864..ea874b0c 100644 --- a/src/components/chart/MarketChart.tsx +++ b/src/components/chart/MarketChart.tsx @@ -739,11 +739,8 @@ export default function MarketChart({ style={{ textShadow: "0 1px 1px rgba(2, 6, 23, 0.35)" }} > {xLabels.map((label) => ( - #{label.toLocaleString()} + {label} ))} - - BLOCK -
@@ -753,20 +750,29 @@ export default function MarketChart({ {volumeLabel}
- {(["10B", "25B", "50B", "100B"] as const).map((option) => ( + {( + [ + ["10B", "10m"], + ["25B", "25m"], + ["50B", "50m"], + ["100B", "100m"], + ] as const + ).map(([key, label]) => ( ))}
diff --git a/src/queries/useMarkets.ts b/src/queries/useMarkets.ts index 2a121a19..d33d2757 100644 --- a/src/queries/useMarkets.ts +++ b/src/queries/useMarkets.ts @@ -1,19 +1,36 @@ import { useQuery } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; import { discoveredToMarket } from "../services/markets"; -import type { DiscoveredMarket, Market } from "../types"; +import type { ChainTipResponse, DiscoveredMarket, Market } from "../types"; export function useMarkets() { return useQuery({ queryKey: ["markets"], queryFn: async (): Promise => { + // Fetch chain tip so time-remaining calculations are accurate. + let currentHeight = 0; + try { + const tip = await invoke("fetch_chain_tip", { + network: "liquid-testnet", + }); + currentHeight = tip.height; + } catch { + // Non-fatal — falls back to 0 (shows raw block distance) + } + try { const stored = await invoke("discover_contracts"); - return stored.map(discoveredToMarket); + return stored.map((d) => ({ + ...discoveredToMarket(d), + currentHeight, + })); } catch { try { const stored = await invoke("list_contracts"); - return stored.map(discoveredToMarket); + return stored.map((d) => ({ + ...discoveredToMarket(d), + currentHeight, + })); } catch (error) { console.warn("Failed to load markets:", error); return []; diff --git a/src/utils-react/chart-series.ts b/src/utils-react/chart-series.ts index fc558b0d..6b83b64e 100644 --- a/src/utils-react/chart-series.ts +++ b/src/utils-react/chart-series.ts @@ -11,7 +11,7 @@ export type ChartSeriesData = { pointCount: number; scaleBlocks: number; startBlockHeight: number; - xLabels: number[]; + xLabels: string[]; yesSeries: Array; }; @@ -43,15 +43,36 @@ function scaleConfig(timescale: ChartTimescale): { }; } +/** Format a block height as a relative time label (e.g. "12:30", "Apr 3"). */ +function blockHeightToTimeLabel( + blockHeight: number, + currentHeight: number, +): string { + const blocksAgo = currentHeight - blockHeight; + // ~1 block per minute on Liquid + const date = new Date(Date.now() - blocksAgo * 60_000); + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + if (sameDay) { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + return date.toLocaleDateString([], { month: "short", day: "numeric" }); +} + function buildXAxisLabels( startBlockHeight: number, scaleBlocks: number, -): number[] { + currentHeight: number, +): string[] { const fractions = scaleBlocks >= 100 ? [0, 0.25, 0.5, 0.75, 1] : [0, 1 / 3, 2 / 3, 1]; - return fractions.map((fraction) => - Math.round(startBlockHeight + fraction * scaleBlocks), - ); + return fractions.map((fraction) => { + const height = Math.round(startBlockHeight + fraction * scaleBlocks); + return blockHeightToTimeLabel(height, currentHeight); + }); } function sampleHistoryProbabilityAtHeight( @@ -115,7 +136,11 @@ export function buildChartSeriesData( market.currentHeight || scaleBlocks, ); const startBlockHeight = Math.max(0, endBlockHeight - scaleBlocks); - const xLabels = buildXAxisLabels(startBlockHeight, scaleBlocks); + const xLabels = buildXAxisLabels( + startBlockHeight, + scaleBlocks, + market.currentHeight || endBlockHeight, + ); return { endBlockHeight, @@ -142,7 +167,11 @@ export function buildChartFromHistory( latestHistoryHeight, ); const startBlockHeight = Math.max(0, endBlockHeight - scaleBlocks); - const xLabels = buildXAxisLabels(startBlockHeight, scaleBlocks); + const xLabels = buildXAxisLabels( + startBlockHeight, + scaleBlocks, + market.currentHeight || endBlockHeight, + ); const historyPoints = history .map((entry) => ({ blockHeight: entry.block_height, From 89a79b130ca5747967bd82e97931e3a4854b5ac4 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:10:01 -0400 Subject: [PATCH 011/117] =?UTF-8?q?feat:=20trading=20panel=20UX=20?= =?UTF-8?q?=E2=80=94=20insufficient=20funds,=20friendly=20errors,=20lock?= =?UTF-8?q?=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable buy buttons with "Insufficient funds" label when wallet balance is too low. Map SDK error strings to user-friendly messages. Show separate "Unlock wallet to trade" button with lock icon when wallet is locked instead of the generic account setup prompt. --- src/components/detail/TradingPanel.tsx | 74 ++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/components/detail/TradingPanel.tsx b/src/components/detail/TradingPanel.tsx index 9334cb87..e1940ba8 100644 --- a/src/components/detail/TradingPanel.tsx +++ b/src/components/detail/TradingPanel.tsx @@ -29,6 +29,21 @@ import { import OrderbookPanel from "./OrderbookPanel"; import PoolSection from "./PoolSection"; +function friendlyTradeError(raw: string): string { + const lower = raw.toLowerCase(); + if (lower.includes("no liquidity")) + return "No liquidity available for this market yet."; + if (lower.includes("insufficient taker funding")) + return "Insufficient funds to complete this trade."; + if (lower.includes("insufficient utxo")) + return "Not enough wallet outputs. Try syncing your wallet."; + if (lower.includes("insufficient fee")) + return "Not enough funds to cover the network fee."; + if (lower.includes("wallet locked") || lower.includes("not initialized")) + return "Wallet is locked. Unlock it to trade."; + return raw; +} + export default function TradingPanel({ market }: { market: Market }) { const selectedSide = useStore((s) => s.selectedSide); const orderType = useStore((s) => s.orderType); @@ -101,12 +116,22 @@ export default function TradingPanel({ market }: { market: Market }) { ], ); + const walletPolicyAssetId = useStore((s) => s.walletPolicyAssetId); const tradeBusy = tradeQuoteLoading || tradeExecuteLoading; const executionPriceSats = Math.round(preview.executionPriceSats); const ctaVerb = tradeIntent === "open" ? "Buy" : "Sell"; const ctaTarget = selectedSide === "yes" ? "Yes" : "No"; const ctaLabel = `${ctaVerb} ${ctaTarget}`; + const policyBalance = + walletData && walletPolicyAssetId + ? (walletData.balance[walletPolicyAssetId] ?? 0) + : 0; + const insufficientFunds = + walletStatus === "unlocked" && + tradeIntent === "open" && + policyBalance < tradeSizeSats; + const yesPrice = market.yesPrice; const yesDisplaySats = yesPrice != null @@ -599,15 +624,21 @@ export default function TradingPanel({ market }: { market: Market }) {
{tradeError && ( -

{tradeError}

+

+ {friendlyTradeError(tradeError)} +

)} ) : ( @@ -713,10 +744,12 @@ export default function TradingPanel({ market }: { market: Market }) { )} {tradeError && ( -

{tradeError}

+

+ {friendlyTradeError(tradeError)} +

)} - {walletStatus !== "unlocked" ? ( + {walletStatus === "not_created" ? (

Create an account to trade @@ -737,18 +770,41 @@ export default function TradingPanel({ market }: { market: Market }) { Set up account

+ ) : walletStatus === "locked" ? ( + ) : ( )} From f1d8c1056641e9f51d75eaf9a49573e6f20440c8 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:10:09 -0400 Subject: [PATCH 012/117] fix: wallet badge only triggers on incoming funds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Green badge no longer fires for outgoing transactions like market creation or sends — only for positive balance changes. --- src/hooks/useTauriEvents.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useTauriEvents.ts b/src/hooks/useTauriEvents.ts index ff824b1e..f3182cc0 100644 --- a/src/hooks/useTauriEvents.ts +++ b/src/hooks/useTauriEvents.ts @@ -84,7 +84,11 @@ export function useTauriEvents(): void { for (const tx of payload.transactions) { if (!walletSeenTxids.has(tx.txid)) { toastForNewTx(tx); - freshNew.add(tx.txid); + // Only badge for incoming funds — outgoing txs (sends, + // market creation, etc.) don't need a persistent badge. + if (tx.balanceChange > 0) { + freshNew.add(tx.txid); + } } } if (freshNew.size !== walletNewTxids.size) { From e68cda8e8072052634753a0ee9e3001311595ea7 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:10:16 -0400 Subject: [PATCH 013/117] feat: expanded market seeder with UTXO retry and docs Reorder markets so first 5 span different categories. Add 18 new markets across all categories (40 total). Fetch real chain tip instead of hardcoded estimate. Add UTXO retry loop between markets with auto-split fallback. Document seeder usage and full market list. --- docs/contracts/test-market-seeder.md | 107 +++++++ .../deadcat-sdk/examples/seed_markets.rs | 279 ++++++++++++++++-- 2 files changed, 354 insertions(+), 32 deletions(-) create mode 100644 docs/contracts/test-market-seeder.md diff --git a/docs/contracts/test-market-seeder.md b/docs/contracts/test-market-seeder.md new file mode 100644 index 00000000..4d0cd555 --- /dev/null +++ b/docs/contracts/test-market-seeder.md @@ -0,0 +1,107 @@ +# Test Market Seeder + +Seed testnet prediction markets with real on-chain contracts and Nostr announcements. + +## Prerequisites + +- **DEADCAT_SOURCE_NSEC** — nsec for the source npub (signs events + acts as oracle) +- **DEADCAT_MNEMONIC** — BIP-39 mnemonic for a funded Liquid testnet wallet + +Optional: +- **DEADCAT_ELECTRUM_URL** — override default testnet Electrum (`ssl://blockstream.info:465`) +- **DEADCAT_DATADIR** — wallet data directory (default: `/tmp/deadcat-seed`) + +## Commands + +Run from the SDK crate directory: + +```bash +cd src-tauri/crates/deadcat-sdk +``` + +### List existing markets + +```bash +DEADCAT_SOURCE_NSEC=nsec1... \ + cargo run --example seed_markets --features testing -- list +``` + +### Publish markets + +Publish all markets (40 total): + +```bash +DEADCAT_SOURCE_NSEC=nsec1... DEADCAT_MNEMONIC="word1 word2 ..." \ + cargo run --example seed_markets --features testing -- publish +``` + +Publish only the first N markets: + +```bash +DEADCAT_SOURCE_NSEC=nsec1... DEADCAT_MNEMONIC="word1 word2 ..." \ + cargo run --example seed_markets --features testing -- publish 5 +``` + +The first 5 markets are one per category (Bitcoin, Politics, Sports, Culture, Macro) for quick visual testing. + +### Delete all markets + +Publishes tombstone replacements that make the events unparseable: + +```bash +DEADCAT_SOURCE_NSEC=nsec1... \ + cargo run --example seed_markets --features testing -- delete +``` + +## Notes + +- Each market creation requires **2 L-BTC UTXOs** and takes ~1 minute (one Liquid block) between markets for the change output to confirm. +- The seeder automatically splits UTXOs if only 1 is available, and retries sync up to 90 seconds between markets. +- The seeder fetches the real Liquid testnet chain tip for accurate expiry height estimation. +- Collateral per token is 5,000 sats for all test markets. +- All markets are published by the source npub and use the same key as oracle for resolution. + +## Test Markets + +| # | Category | Question | +|---|----------|----------| +| 1 | Bitcoin | Will Bitcoin exceed $200k by end of 2026? | +| 2 | Politics | Will the US pass stablecoin legislation in 2026? | +| 3 | Sports | Will Lewis Hamilton win a race for Ferrari in 2026? | +| 4 | Culture | Will GTA VI be released in 2026? | +| 5 | Macro | Will the Fed cut rates below 3% by end of 2026? | +| 6 | Bitcoin | Will Bitcoin dominance drop below 50% in 2026? | +| 7 | Bitcoin | Will a Bitcoin spot ETF surpass $100B AUM? | +| 8 | Bitcoin | Will the Bitcoin halving cycle peak occur before Q3 2026? | +| 9 | Bitcoin | Will Ethereum flip Bitcoin in market cap? | +| 10 | Politics | Will the EU implement MiCA enforcement actions in 2026? | +| 11 | Politics | Will a G7 country adopt a Bitcoin strategic reserve? | +| 12 | Politics | Will El Salvador repay its 2027 bonds early? | +| 13 | Sports | Will Real Madrid win the 2025-26 Champions League? | +| 14 | Sports | Will the Kansas City Chiefs three-peat at Super Bowl LXI? | +| 15 | Sports | Will a new 100m world record be set in 2026? | +| 16 | Culture | Will a Nostr client reach 10M monthly active users? | +| 17 | Culture | Will an AI model pass the Turing test in 2026? | +| 18 | Culture | Will a major studio release a fully AI-generated film? | +| 19 | Culture | Will Wikipedia adopt Nostr for contributor identity? | +| 20 | Weather | Will 2026 be the hottest year on record? | +| 21 | Weather | Will a Category 6 hurricane classification be adopted? | +| 22 | Macro | Will gold exceed $3,500/oz in 2026? | +| 23 | Macro | Will US national debt exceed $40 trillion? | +| 24 | Bitcoin | Will Bitcoin transaction fees average over $50 in Q4 2026? | +| 25 | Bitcoin | Will Lightning Network capacity exceed 10,000 BTC? | +| 26 | Bitcoin | Will a Bitcoin L2 reach $1B TVL? | +| 27 | Politics | Will Texas pass a state Bitcoin reserve bill? | +| 28 | Politics | Will the SEC approve a Solana spot ETF in 2026? | +| 29 | Politics | Will China lift its cryptocurrency trading ban? | +| 30 | Sports | Will the US win the most gold medals at the 2026 Winter Olympics? | +| 31 | Sports | Will Lionel Messi retire from professional football in 2026? | +| 32 | Sports | Will an NBA team complete a 73+ win regular season? | +| 33 | Culture | Will a decentralized social protocol surpass 50M users? | +| 34 | Culture | Will an AI-generated song reach #1 on Billboard Hot 100? | +| 35 | Culture | Will GTA VI be released in 2026? | +| 36 | Weather | Will Arctic sea ice hit a new record minimum in 2026? | +| 37 | Weather | Will a US city record a temperature above 130F? | +| 38 | Macro | Will US unemployment exceed 5% in 2026? | +| 39 | Macro | Will the S&P 500 exceed 7,000 in 2026? | +| 40 | Macro | Will the US 10-year Treasury yield exceed 6%? | diff --git a/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs index aee5a50f..07c2b33d 100644 --- a/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs +++ b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs @@ -49,7 +49,7 @@ const DEC_2026_UNIX: u64 = 1_798_761_600; // 2027-01-01 00:00 UTC const JUN_2027_UNIX: u64 = 1_814_400_000; // ~mid 2027 const MARKETS: &[MarketDef] = &[ - // Bitcoin + // ── First 5: one per category for initial seeding ──────────── MarketDef { question: "Will Bitcoin exceed $200k by end of 2026?", description: "Resolves YES if BTC/USD spot price on any major exchange exceeds $200,000 at any point before January 1, 2027 00:00 UTC.", @@ -58,6 +58,40 @@ const MARKETS: &[MarketDef] = &[ cpt_sats: 5000, settlement_unix: DEC_2026_UNIX, }, + MarketDef { + question: "Will the US pass stablecoin legislation in 2026?", + description: "Resolves YES if a federal stablecoin bill is signed into law by the US President before January 1, 2027.", + category: "Politics", + resolution_source: "Congress.gov", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Lewis Hamilton win a race for Ferrari in 2026?", + description: "Resolves YES if Lewis Hamilton wins at least one Formula 1 Grand Prix while driving for Scuderia Ferrari in 2026.", + category: "Sports", + resolution_source: "FIA official race results", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will GTA VI be released in 2026?", + description: "Resolves YES if Grand Theft Auto VI is officially released for any platform during calendar year 2026.", + category: "Culture", + resolution_source: "Rockstar Games official release", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the Fed cut rates below 3% by end of 2026?", + description: "Resolves YES if the Federal Reserve's target federal funds rate upper bound is below 3.00% on December 31, 2026.", + category: "Macro", + resolution_source: "Federal Reserve press releases", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // ── Remaining: grouped by category ─────────────────────────── + // Bitcoin MarketDef { question: "Will Bitcoin dominance drop below 50% in 2026?", description: "Resolves YES if Bitcoin market cap dominance (per CoinGecko) falls below 50% at any point during 2026.", @@ -82,15 +116,15 @@ const MARKETS: &[MarketDef] = &[ cpt_sats: 5000, settlement_unix: 1_719_792_000, // 2026-07-01 }, - // Politics MarketDef { - question: "Will the US pass stablecoin legislation in 2026?", - description: "Resolves YES if a federal stablecoin bill is signed into law by the US President before January 1, 2027.", - category: "Politics", - resolution_source: "Congress.gov", + question: "Will Ethereum flip Bitcoin in market cap?", + description: "Resolves YES if Ethereum's market capitalization exceeds Bitcoin's at any point during 2026 per CoinGecko.", + category: "Bitcoin", + resolution_source: "CoinGecko market cap", cpt_sats: 5000, settlement_unix: DEC_2026_UNIX, }, + // Politics MarketDef { question: "Will the EU implement MiCA enforcement actions in 2026?", description: "Resolves YES if any EU member state issues formal enforcement action under MiCA regulation during 2026.", @@ -140,14 +174,6 @@ const MARKETS: &[MarketDef] = &[ cpt_sats: 5000, settlement_unix: DEC_2026_UNIX, }, - MarketDef { - question: "Will Lewis Hamilton win a race for Ferrari in 2026?", - description: "Resolves YES if Lewis Hamilton wins at least one Formula 1 Grand Prix while driving for Scuderia Ferrari in 2026.", - category: "Sports", - resolution_source: "FIA official race results", - cpt_sats: 5000, - settlement_unix: DEC_2026_UNIX, - }, // Culture MarketDef { question: "Will a Nostr client reach 10M monthly active users?", @@ -199,14 +225,6 @@ const MARKETS: &[MarketDef] = &[ settlement_unix: DEC_2026_UNIX, }, // Macro - MarketDef { - question: "Will the Fed cut rates below 3% by end of 2026?", - description: "Resolves YES if the Federal Reserve's target federal funds rate upper bound is below 3.00% on December 31, 2026.", - category: "Macro", - resolution_source: "Federal Reserve press releases", - cpt_sats: 5000, - settlement_unix: DEC_2026_UNIX, - }, MarketDef { question: "Will gold exceed $3,500/oz in 2026?", description: "Resolves YES if the LBMA PM gold fix exceeds $3,500 per troy ounce at any point during 2026.", @@ -223,11 +241,153 @@ const MARKETS: &[MarketDef] = &[ cpt_sats: 5000, settlement_unix: DEC_2026_UNIX, }, + // Bitcoin (additional) MarketDef { - question: "Will Ethereum flip Bitcoin in market cap?", - description: "Resolves YES if Ethereum's market capitalization exceeds Bitcoin's at any point during 2026 per CoinGecko.", + question: "Will Bitcoin transaction fees average over $50 in Q4 2026?", + description: "Resolves YES if the average Bitcoin on-chain transaction fee exceeds $50 USD for the full Q4 2026 quarter (Oct-Dec).", category: "Bitcoin", - resolution_source: "CoinGecko market cap", + resolution_source: "Mempool.space fee data", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will Lightning Network capacity exceed 10,000 BTC?", + description: "Resolves YES if the total public Lightning Network channel capacity exceeds 10,000 BTC at any point in 2026.", + category: "Bitcoin", + resolution_source: "Mempool.space Lightning explorer", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will a Bitcoin L2 reach $1B TVL?", + description: "Resolves YES if any Bitcoin Layer 2 protocol (excluding Lightning) exceeds $1 billion in total value locked per DefiLlama.", + category: "Bitcoin", + resolution_source: "DefiLlama", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Politics (additional) + MarketDef { + question: "Will Texas pass a state Bitcoin reserve bill?", + description: "Resolves YES if the Texas state legislature passes a bill establishing a state-held Bitcoin reserve, signed by the Governor.", + category: "Politics", + resolution_source: "Texas Legislature website", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the SEC approve a Solana spot ETF in 2026?", + description: "Resolves YES if the US SEC approves at least one Solana spot ETF application for listing on a US exchange.", + category: "Politics", + resolution_source: "SEC EDGAR filings", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will China lift its cryptocurrency trading ban?", + description: "Resolves YES if the People's Republic of China officially reverses its ban on cryptocurrency trading for retail investors.", + category: "Politics", + resolution_source: "PBoC / State Council announcements", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Sports (additional) + MarketDef { + question: "Will the US win the most gold medals at the 2026 Winter Olympics?", + description: "Resolves YES if the United States finishes with the most gold medals at the 2026 Milano Cortina Winter Olympics.", + category: "Sports", + resolution_source: "IOC official medal tally", + cpt_sats: 5000, + settlement_unix: 1_740_700_800, // ~Feb 28 2026 + }, + MarketDef { + question: "Will Lionel Messi retire from professional football in 2026?", + description: "Resolves YES if Lionel Messi officially announces his retirement from all professional club football during 2026.", + category: "Sports", + resolution_source: "Official club / player announcement", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will an NBA team complete a 73+ win regular season?", + description: "Resolves YES if any NBA team wins 73 or more regular season games in the 2025-26 season, tying or breaking the record.", + category: "Sports", + resolution_source: "NBA.com standings", + cpt_sats: 5000, + settlement_unix: 1_718_668_800, // ~Jun 2026 + }, + // Culture (additional) + MarketDef { + question: "Will a decentralized social protocol surpass 50M users?", + description: "Resolves YES if any decentralized social protocol (Nostr, AT Protocol, ActivityPub, etc.) verifiably exceeds 50 million total accounts.", + category: "Culture", + resolution_source: "Protocol stats / independent audits", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will an AI-generated song reach #1 on Billboard Hot 100?", + description: "Resolves YES if a song primarily created by AI (vocals, composition, or production) reaches #1 on the Billboard Hot 100.", + category: "Culture", + resolution_source: "Billboard Hot 100 chart", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will GTA VI be released in 2026?", + description: "Resolves YES if Grand Theft Auto VI is officially released for any platform during calendar year 2026.", + category: "Culture", + resolution_source: "Rockstar Games official release", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Weather (additional) + MarketDef { + question: "Will Arctic sea ice hit a new record minimum in 2026?", + description: "Resolves YES if the Arctic sea ice minimum extent in September 2026 sets a new all-time low per NSIDC satellite data.", + category: "Weather", + resolution_source: "NSIDC Arctic Sea Ice News", + cpt_sats: 5000, + settlement_unix: 1_793_491_200, // ~Oct 2026 + }, + MarketDef { + question: "Will a US city record a temperature above 130°F?", + description: "Resolves YES if any official US weather station records a temperature exceeding 130°F (54.4°C) during 2026.", + category: "Weather", + resolution_source: "NOAA weather station records", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + // Macro (additional) + MarketDef { + question: "Will US unemployment exceed 5% in 2026?", + description: "Resolves YES if the BLS-reported US unemployment rate (U-3) exceeds 5.0% in any monthly report during 2026.", + category: "Macro", + resolution_source: "Bureau of Labor Statistics", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the S&P 500 exceed 7,000 in 2026?", + description: "Resolves YES if the S&P 500 index closes above 7,000 on any trading day during 2026.", + category: "Macro", + resolution_source: "NYSE market data", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will the US 10-year Treasury yield exceed 6%?", + description: "Resolves YES if the US 10-year Treasury note yield exceeds 6.00% at any point during 2026.", + category: "Macro", + resolution_source: "US Treasury yield data", + cpt_sats: 5000, + settlement_unix: DEC_2026_UNIX, + }, + MarketDef { + question: "Will a BRICS common currency be announced?", + description: "Resolves YES if the BRICS bloc officially announces the creation of a common settlement currency or currency unit.", + category: "Macro", + resolution_source: "BRICS summit communiqués", cpt_sats: 5000, settlement_unix: DEC_2026_UNIX, }, @@ -359,12 +519,22 @@ async fn cmd_publish(keys: &Keys, count: Option) { let oracle_pk = oracle_pubkey_bytes(keys); let max = count.unwrap_or(MARKETS.len()).min(MARKETS.len()); - // Get current chain tip for block height estimation - let current_height = 2_800_000u32; // approximate Liquid testnet height + // Fetch actual chain tip for accurate expiry height estimation let current_unix = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); + let current_height: u32 = std::process::Command::new("curl") + .args(["-sf", "https://blockstream.info/liquidtestnet/api/blocks/tip/height"]) + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .and_then(|s| s.trim().parse().ok()) + .unwrap_or_else(|| { + eprintln!("Warning: could not fetch chain tip, using estimate"); + 2_400_000 + }); + println!("Current chain tip: block {}\n", current_height); println!("Creating {} market(s)...\n", max); @@ -414,12 +584,57 @@ async fn cmd_publish(keys: &Keys, count: Option) { println!(" Category: {}", market.category); println!(); - // Sync wallet between markets so UTXOs are up to date + // Wait for the change UTXO to become visible before creating the + // next market. Liquid blocks are ~1 min, so we retry sync up to + // 90 s waiting for at least 2 L-BTC UTXOs. if i + 1 < max { - println!(" Syncing wallet..."); - tokio::time::sleep(Duration::from_secs(3)).await; - if let Err(e) = node.sync_wallet().await { - eprintln!(" Warning: wallet sync failed: {e}"); + let policy_asset = Network::LiquidTestnet.into_lwk().policy_asset(); + println!(" Waiting for UTXOs..."); + let mut ready = false; + for attempt in 1..=18 { + tokio::time::sleep(Duration::from_secs(5)).await; + if let Err(e) = node.sync_wallet().await { + eprintln!(" Warning: sync failed: {e}"); + continue; + } + let utxo_count = node + .utxos() + .unwrap_or_default() + .iter() + .filter(|u| u.unblinded.asset == policy_asset && u.unblinded.value >= 546) + .count(); + if utxo_count >= 2 { + println!(" Ready ({} UTXOs after {}s)", utxo_count, attempt * 5); + ready = true; + break; + } + } + if !ready { + // Last resort: split the single UTXO + println!(" Still only 1 UTXO — splitting..."); + let balance = node.balance().unwrap_or_default(); + let lbtc = balance.get(&policy_asset).copied().unwrap_or(0); + if lbtc > 2000 { + let addr = node.address(None).await.expect("address").address().to_string(); + match node.send_lbtc(addr, lbtc / 2, default_tx_options()).await { + Ok((txid, _)) => { + println!(" Split tx: {}", txid); + // Wait for split to confirm + for _ in 1..=18 { + tokio::time::sleep(Duration::from_secs(5)).await; + let _ = node.sync_wallet().await; + let count = node + .utxos() + .unwrap_or_default() + .iter() + .filter(|u| u.unblinded.asset == policy_asset && u.unblinded.value >= 546) + .count(); + if count >= 2 { break; } + } + } + Err(e) => eprintln!(" Split failed: {e}"), + } + } } } } From 81344d9cadf5357cabd99c3b01b40e2bd36c3b19 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 11:14:16 -0400 Subject: [PATCH 014/117] style: formatting fixes for biome and cargo fmt --- .../deadcat-sdk/examples/seed_markets.rs | 65 ++++++++++++++----- src-tauri/src/commands.rs | 16 +++-- src/components/home/HomePage.tsx | 14 +++- 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs index 07c2b33d..3368f3aa 100644 --- a/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs +++ b/src-tauri/crates/deadcat-sdk/examples/seed_markets.rs @@ -24,8 +24,8 @@ fn install_crypto_provider() { } use deadcat_sdk::{ - ContractMetadata, DeadcatNode, DiscoveryConfig, MinerFeePolicy, Network, NoopStore, TxOptions, - DEFAULT_RELAYS, NETWORK_TAG, + ContractMetadata, DEFAULT_RELAYS, DeadcatNode, DiscoveryConfig, MinerFeePolicy, NETWORK_TAG, + Network, NoopStore, TxOptions, }; use nostr_sdk::prelude::*; @@ -468,7 +468,11 @@ async fn setup_node(keys: Keys) -> Arc> { let balance = node.balance().unwrap_or_default(); let policy_asset = Network::LiquidTestnet.into_lwk().policy_asset(); let lbtc_sats = balance.get(&policy_asset).copied().unwrap_or(0); - println!("Wallet balance: {} sats ({:.8} tL-BTC)", lbtc_sats, lbtc_sats as f64 / 100_000_000.0); + println!( + "Wallet balance: {} sats ({:.8} tL-BTC)", + lbtc_sats, + lbtc_sats as f64 / 100_000_000.0 + ); // Check if we have enough UTXOs — market creation needs at least 2 let utxo_count = node @@ -479,10 +483,21 @@ async fn setup_node(keys: Keys) -> Arc> { .count(); if utxo_count < 2 && lbtc_sats > 2000 { - println!("Only {} L-BTC UTXO(s) — splitting for market creation...", utxo_count); - let self_addr = node.address(None).await.expect("address").address().to_string(); + println!( + "Only {} L-BTC UTXO(s) — splitting for market creation...", + utxo_count + ); + let self_addr = node + .address(None) + .await + .expect("address") + .address() + .to_string(); let split_amount = lbtc_sats / 2; - match node.send_lbtc(self_addr, split_amount, default_tx_options()).await { + match node + .send_lbtc(self_addr, split_amount, default_tx_options()) + .await + { Ok((txid, fee)) => { println!("Split tx: {} (fee: {} sats)", txid, fee); println!("Waiting for confirmation..."); @@ -525,7 +540,10 @@ async fn cmd_publish(keys: &Keys, count: Option) { .unwrap() .as_secs(); let current_height: u32 = std::process::Command::new("curl") - .args(["-sf", "https://blockstream.info/liquidtestnet/api/blocks/tip/height"]) + .args([ + "-sf", + "https://blockstream.info/liquidtestnet/api/blocks/tip/height", + ]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) @@ -539,7 +557,8 @@ async fn cmd_publish(keys: &Keys, count: Option) { println!("Creating {} market(s)...\n", max); for (i, market) in MARKETS.iter().take(max).enumerate() { - let expiry_height = unix_to_block_height(market.settlement_unix, current_height, current_unix); + let expiry_height = + unix_to_block_height(market.settlement_unix, current_height, current_unix); let metadata = ContractMetadata { question: market.question.to_string(), description: market.description.to_string(), @@ -615,7 +634,12 @@ async fn cmd_publish(keys: &Keys, count: Option) { let balance = node.balance().unwrap_or_default(); let lbtc = balance.get(&policy_asset).copied().unwrap_or(0); if lbtc > 2000 { - let addr = node.address(None).await.expect("address").address().to_string(); + let addr = node + .address(None) + .await + .expect("address") + .address() + .to_string(); match node.send_lbtc(addr, lbtc / 2, default_tx_options()).await { Ok((txid, _)) => { println!(" Split tx: {}", txid); @@ -627,9 +651,14 @@ async fn cmd_publish(keys: &Keys, count: Option) { .utxos() .unwrap_or_default() .iter() - .filter(|u| u.unblinded.asset == policy_asset && u.unblinded.value >= 546) + .filter(|u| { + u.unblinded.asset == policy_asset + && u.unblinded.value >= 546 + }) .count(); - if count >= 2 { break; } + if count >= 2 { + break; + } } } Err(e) => eprintln!(" Split failed: {e}"), @@ -778,10 +807,7 @@ async fn cmd_delete(keys: &Keys) { .expect("failed to sign tombstone"); match client.send_event(tombstone).await { - Ok(_) => println!( - " Deleted: d={}...", - &d_tag[..16.min(d_tag.len())] - ), + Ok(_) => println!(" Deleted: d={}...", &d_tag[..16.min(d_tag.len())]), Err(e) => eprintln!( " FAILED to delete d={}...: {e}", &d_tag[..16.min(d_tag.len())] @@ -827,13 +853,18 @@ fn print_usage() { eprintln!("Usage: seed_markets [args]"); eprintln!(); eprintln!("Commands:"); - eprintln!(" publish [N] Create N markets on-chain + announce on Nostr (default: all {}))", MARKETS.len()); + eprintln!( + " publish [N] Create N markets on-chain + announce on Nostr (default: all {}))", + MARKETS.len() + ); eprintln!(" list List markets published by the source npub"); eprintln!(" delete Delete all markets by publishing tombstone events"); eprintln!(); eprintln!("Environment:"); eprintln!(" DEADCAT_SOURCE_NSEC nsec for the source npub (required)"); - eprintln!(" DEADCAT_MNEMONIC BIP-39 mnemonic for funded testnet wallet (required for publish)"); + eprintln!( + " DEADCAT_MNEMONIC BIP-39 mnemonic for funded testnet wallet (required for publish)" + ); eprintln!(" DEADCAT_ELECTRUM_URL Electrum server (default: ssl://blockstream.info:465)"); eprintln!(" DEADCAT_DATADIR Wallet data dir (default: /tmp/deadcat-seed)"); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2942feac..b85e2878 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -20,9 +20,8 @@ const ORDER_INDEX_AUTO_RESOLVE_SENTINEL: u32 = u32::MAX; /// Cache of markets fetched from relays but not yet promoted in the store. /// Persists across `discover_contracts` calls so markets don't flash and disappear. -static RELAY_MARKET_CACHE: std::sync::LazyLock< - std::sync::Mutex>, -> = std::sync::LazyLock::new(|| std::sync::Mutex::new(Vec::new())); +static RELAY_MARKET_CACHE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(Vec::new())); fn validate_request(request: &CreateContractRequest) -> Result<(), String> { if request.question.trim().is_empty() || request.question.len() > 140 { @@ -2343,10 +2342,17 @@ pub fn list_contracts(app: tauri::AppHandle) -> Result, St .list_markets(&MarketFilter::default()) .map_err(|e| format!("list markets: {e}"))?; - eprintln!("[debug] list_contracts: store has {} markets, source_pubkey_hex={:?}", infos.len(), source_pubkey_hex.as_deref().map(|s| &s[..16])); + eprintln!( + "[debug] list_contracts: store has {} markets, source_pubkey_hex={:?}", + infos.len(), + source_pubkey_hex.as_deref().map(|s| &s[..16]) + ); for info in &infos { let cpk = info.creator_pubkey.as_ref().map(|pk| hex::encode(pk)); - eprintln!("[debug] market creator_pubkey={:?}", cpk.as_deref().map(|s| &s[..16])); + eprintln!( + "[debug] market creator_pubkey={:?}", + cpk.as_deref().map(|s| &s[..16]) + ); } // Filter by source author if configured diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index 22239ce5..499789dc 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -191,7 +191,12 @@ function MarketLoader() { function CatLoaderSvg() { return ( -
- + )}
{resolvedBadge ?? ( <> @@ -245,7 +241,7 @@ export default function MarketCard({ {" · "} {timeLeft}

-
+ ); } diff --git a/src/components/layout/SettingsPanel.tsx b/src/components/layout/SettingsPanel.tsx index cbdaed48..effe00ef 100644 --- a/src/components/layout/SettingsPanel.tsx +++ b/src/components/layout/SettingsPanel.tsx @@ -89,6 +89,20 @@ function Toggle({ function NostrSection() { const nostrNpub = useStore((s) => s.nostrNpub); const nostrNsecRevealed = useStore((s) => s.nostrNsecRevealed); + const walletStatus = useStore((s) => s.walletStatus); + const [nsecPasswordInput, setNsecPasswordInput] = useState(""); + const [nsecPasswordPrompt, setNsecPasswordPrompt] = useState(false); + const [nsecPasswordError, setNsecPasswordError] = useState(""); + + // Auto-hide nsec when wallet locks + useEffect(() => { + if (walletStatus !== "unlocked") { + useStore.setState({ nostrNsecRevealed: null }); + setNsecPasswordPrompt(false); + setNsecPasswordInput(""); + setNsecPasswordError(""); + } + }, [walletStatus]); const copyNpub = useCallback(async () => { const npub = useStore.getState().nostrNpub; @@ -103,18 +117,49 @@ function NostrSection() { if (nsec) { await navigator.clipboard.writeText(nsec); useStore.setState({ nostrNsecRevealed: null }); + setNsecPasswordPrompt(false); + setNsecPasswordInput(""); showToast("Copied nsec to clipboard"); } }, []); - const revealNsec = useCallback(async () => { + const handleRevealClick = useCallback(() => { + // If wallet is unlocked with a session password, use it directly + const sessionPw = useStore.getState().walletSessionPassword; + if (walletStatus === "unlocked" && sessionPw) { + void (async () => { + try { + // Verify the session password is valid + await invoke("get_wallet_mnemonic", { + password: sessionPw, + }); + const nsec = await invoke("export_nostr_nsec"); + useStore.setState({ nostrNsecRevealed: nsec }); + } catch { + // Session password invalid or wallet issue — fall back to prompt + setNsecPasswordPrompt(true); + setNsecPasswordError(""); + } + })(); + } else { + setNsecPasswordPrompt(true); + setNsecPasswordError(""); + } + }, [walletStatus]); + + const handlePasswordSubmit = useCallback(async () => { + if (!nsecPasswordInput) return; try { + await invoke("get_wallet_mnemonic", { + password: nsecPasswordInput, + }); const nsec = await invoke("export_nostr_nsec"); useStore.setState({ nostrNsecRevealed: nsec }); + setNsecPasswordError(""); } catch { - /* ignore */ + setNsecPasswordError("Incorrect password"); } - }, []); + }, [nsecPasswordInput]); return (
@@ -183,6 +228,42 @@ function NostrSection() { + ) : nsecPasswordPrompt ? ( +
+
+ setNsecPasswordInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handlePasswordSubmit(); + }} + placeholder="Account password" + className="h-7 flex-1 min-w-0 rounded border border-slate-700 bg-slate-900 px-2 text-xs outline-none ring-emerald-400 transition focus:ring-1" + /> + + +
+ {nsecPasswordError && ( +

{nsecPasswordError}

+ )} +
) : ( <>

@@ -190,7 +271,7 @@ function NostrSection() {

{tradeError && (

- {friendlyTradeError(tradeError)} + {friendlyError(tradeError)}

)} +
+ ); + } else if (sendConfirm) { + const total = sendConfirm.amountSat + sendConfirm.feeSat; + content = ( +
+

+ Confirm Transaction +

+
+
+ To + + {sendConfirm.address} + +
+
+ Amount + + {sendConfirm.amountSat.toLocaleString()} sats + +
+
+ Network fee + + {sendConfirm.feeSat.toLocaleString()} sats + +
+
+ Total + + {total.toLocaleString()} sats + +
+
+
+ + +
); } else { @@ -264,24 +371,63 @@ export function SendModal() { placeholder="Liquid address" className="h-10 w-full rounded-lg border border-slate-700 bg-slate-900 px-3 text-sm outline-none ring-emerald-400 focus:ring-2" /> - - useStore.setState({ sendLiquidAmount: e.target.value }) - } - placeholder="Amount (sats)" - className="h-10 w-full rounded-lg border border-slate-700 bg-slate-900 px-3 text-sm outline-none ring-emerald-400 focus:ring-2" - /> +
+ + useStore.setState({ sendLiquidAmount: e.target.value }) + } + placeholder="Amount (sats)" + className="h-10 flex-1 rounded-lg border border-slate-700 bg-slate-900 px-3 text-sm outline-none ring-emerald-400 focus:ring-2" + /> + +
); @@ -363,7 +509,7 @@ export function SendModal() { {content} {sendError && (
- {sendError} + {friendlyError(sendError)}
)} diff --git a/src/types.ts b/src/types.ts index b854888d..eb5daf69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -536,3 +536,10 @@ export type LiquidSendResult = { feeSat: number; fee: ResolvedMinerFee; }; + +export type PrepareSendResult = { + address: string; + amountSat: number; + feeSat: number; + fee: ResolvedMinerFee; +}; diff --git a/src/utils-react/friendly-error.ts b/src/utils-react/friendly-error.ts new file mode 100644 index 00000000..19921bd4 --- /dev/null +++ b/src/utils-react/friendly-error.ts @@ -0,0 +1,26 @@ +/** Map raw SDK/API error strings to user-friendly messages. */ +export function friendlyError(raw: string): string { + const lower = raw.toLowerCase(); + if ( + lower.includes("insufficient funds") || + (lower.includes("missing") && lower.includes("units for asset")) + ) + return "Insufficient funds to cover the amount and network fee."; + if (lower.includes("no liquidity")) + return "No liquidity available for this market yet."; + if (lower.includes("insufficient taker funding")) + return "Insufficient funds to complete this trade."; + if (lower.includes("insufficient utxo")) + return "Not enough wallet outputs. Try syncing your wallet."; + if (lower.includes("insufficient fee")) + return "Not enough funds to cover the network fee."; + if (lower.includes("timed out")) + return "Request timed out. Please try again."; + if (lower.includes("wallet locked") || lower.includes("not initialized")) + return "Wallet is locked. Unlock it first."; + if (lower.includes("expected value at line")) + return "Service temporarily unavailable. Please try again later."; + if (lower.includes("boltz api error")) + return "Swap service error. Please try again later."; + return raw; +} From 31f3b51b98c1486bad6d023af06e201b80e7fba0 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 14:34:33 -0400 Subject: [PATCH 019/117] refactor: shared useLockScroll hook for all modals Extract body scroll lock into reusable hook. Apply to NostrEventModal (was missing), WalletPage, ProfilePage, OnboardingOverlay, and SettingsPanel. Fixes background scrolling while any modal is open. --- src/components/layout/SettingsPanel.tsx | 25 ++++++++----------- src/components/modals/NostrEventModal.tsx | 2 ++ .../onboarding/OnboardingOverlay.tsx | 18 +++---------- src/components/profile/ProfilePage.tsx | 12 +++------ src/components/wallet/WalletPage.tsx | 17 +++---------- src/hooks/useLockScroll.ts | 15 +++++++++++ 6 files changed, 37 insertions(+), 52 deletions(-) create mode 100644 src/hooks/useLockScroll.ts diff --git a/src/components/layout/SettingsPanel.tsx b/src/components/layout/SettingsPanel.tsx index effe00ef..034e28c0 100644 --- a/src/components/layout/SettingsPanel.tsx +++ b/src/components/layout/SettingsPanel.tsx @@ -1,6 +1,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useState } from "react"; import { baseCurrencyOptions } from "../../constants"; +import { useLockScroll } from "../../hooks/useLockScroll"; import { queryClient } from "../../queries/queryClient"; import { useStore } from "../../store"; import type { BaseCurrency, RelayEntry } from "../../types"; @@ -1209,22 +1210,18 @@ export function SettingsPanel() { const settingsOpen = useStore((s) => s.settingsOpen); const nostrReplacePanel = useStore((s) => s.nostrReplacePanel); - useEffect(() => { - if (settingsOpen) { - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - }; - }, [settingsOpen]); - if (!settingsOpen) return null; + return ; +} + +function SettingsPanelContent({ + nostrReplacePanel, +}: { + nostrReplacePanel: boolean; +}) { + useLockScroll(); + return (
diff --git a/src/components/modals/NostrEventModal.tsx b/src/components/modals/NostrEventModal.tsx index d59f8c4b..42a052ef 100644 --- a/src/components/modals/NostrEventModal.tsx +++ b/src/components/modals/NostrEventModal.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { hexToNpub } from "../../utils/crypto"; import { CloseButton } from "../shared/CloseButton"; @@ -54,6 +55,7 @@ function CopyableField({ } export function NostrEventModal() { + useLockScroll(); const nostrEventModal = useStore((s) => s.nostrEventModal); const nostrEventJson = useStore((s) => s.nostrEventJson); const nostrEventNevent = useStore((s) => s.nostrEventNevent); diff --git a/src/components/onboarding/OnboardingOverlay.tsx b/src/components/onboarding/OnboardingOverlay.tsx index 3f473185..e9fc419c 100644 --- a/src/components/onboarding/OnboardingOverlay.tsx +++ b/src/components/onboarding/OnboardingOverlay.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect } from "react"; +import { useCallback } from "react"; +import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { CloseButton } from "../shared/CloseButton"; import NostrSetupStep from "./NostrSetupStep"; @@ -10,20 +11,7 @@ export default function OnboardingOverlay() { const onboardingNostrDone = useStore((s) => s.onboardingNostrDone); const onboardingWalletOnly = useStore((s) => s.onboardingWalletOnly); const backupFileContent = useStore((s) => s.onboardingBackupFileContent); - // Lock background scroll when modal is open - useEffect(() => { - if (setupModalOpen) { - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - }; - }, [setupModalOpen]); + useLockScroll(); const handleClose = useCallback(() => { useStore.setState({ diff --git a/src/components/profile/ProfilePage.tsx b/src/components/profile/ProfilePage.tsx index ee7c766e..72862851 100644 --- a/src/components/profile/ProfilePage.tsx +++ b/src/components/profile/ProfilePage.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; import { readFile } from "@tauri-apps/plugin-fs"; import { useCallback, useEffect, useState } from "react"; +import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { generateAvatarDataUri } from "../../utils-react/avatar"; import { CloseButton } from "../shared/CloseButton"; @@ -95,24 +96,17 @@ export default function ProfilePage() { } }, [nostrProfile]); + useLockScroll(); + useEffect(() => { if (profileOpen) { - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; // Fetch latest profile from relays invoke("fetch_nostr_profile") .then((profile) => { if (profile) useStore.setState({ nostrProfile: profile }); }) .catch(() => {}); - } else { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; } - return () => { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - }; }, [profileOpen]); const close = useCallback(() => { diff --git a/src/components/wallet/WalletPage.tsx b/src/components/wallet/WalletPage.tsx index 69d2d7c0..e4972441 100644 --- a/src/components/wallet/WalletPage.tsx +++ b/src/components/wallet/WalletPage.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect } from "react"; +import { useCallback } from "react"; +import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { CloseButton } from "../shared/CloseButton"; import { WalletLocked } from "./WalletLocked"; @@ -23,19 +24,7 @@ export function WalletPage() { [], ); - useEffect(() => { - if (walletOpen) { - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; - }; - }, [walletOpen]); + useLockScroll(); if (!walletOpen) return null; diff --git a/src/hooks/useLockScroll.ts b/src/hooks/useLockScroll.ts new file mode 100644 index 00000000..978ce011 --- /dev/null +++ b/src/hooks/useLockScroll.ts @@ -0,0 +1,15 @@ +import { useEffect } from "react"; + +/** Lock body scroll while the calling component is mounted. */ +export function useLockScroll(): void { + useEffect(() => { + const prev = document.body.style.overflow; + const prevHtml = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + document.documentElement.style.overflow = prevHtml; + }; + }, []); +} From 03606b4871a9b59729792b65b786c975be97029f Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 14:34:42 -0400 Subject: [PATCH 020/117] fix: skeleton loading state replaces splash/empty-state race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace cat bag loader and splash timing hacks with skeleton cards that stay visible until markets actually arrive in the React query cache. marketsLoading only clears inside useMarkets queryFn when data is non-empty. No fallback timeout — skeletons persist until relay data arrives. Removes MarketLoader, CatLoaderSvg, BagLoaderSvg, and OverlayLoader usage from App.tsx. --- src/App.tsx | 4 +- src/components/home/HomePage.tsx | 123 +++++++++++++++---------------- src/hooks/useBootstrap.ts | 6 +- src/queries/useMarkets.ts | 7 +- 4 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4058c27e..a34566ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,7 @@ import { TopShell } from "./components/layout/TopShell"; import { NostrEventModal } from "./components/modals/NostrEventModal"; import OnboardingOverlay from "./components/onboarding/OnboardingOverlay"; import ProfilePage from "./components/profile/ProfilePage"; -import { OverlayLoader } from "./components/shared/OverlayLoader"; + import { ToastContainer } from "./components/shared/Toast"; import { WalletPage } from "./components/wallet/WalletPage"; import { useActivityTracking } from "./hooks/useActivityTracking"; @@ -78,7 +78,6 @@ export default function App() { const walletOpen = useStore((s) => s.walletOpen); const setupModalOpen = useStore((s) => s.setupModalOpen); const nostrEventModal = useStore((s) => s.nostrEventModal); - const marketsLoading = useStore((s) => s.marketsLoading); return ( <> @@ -96,7 +95,6 @@ export default function App() { {walletOpen && } {setupModalOpen && } {nostrEventModal && } - {marketsLoading && } diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index 499789dc..7b4c1e1e 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -166,65 +166,6 @@ function SidebarMarketItem({ ); } -// ── Market loader (cat bag animation) ──────────────────────────────── -function MarketLoader() { - return ( -
-
-
-
-
- -
-
-
- -
-
-

- Fetching markets... -

-
-
- ); -} - -function CatLoaderSvg() { - return ( - - ); -} - -function BagLoaderSvg() { - return ( - - ); -} - // ── Empty state ────────────────────────────────────────────────────── function EmptyState() { const walletStatus = useStore((s) => s.walletStatus); @@ -896,11 +837,67 @@ function CategoryPageView({ ); } +// ── Skeleton cards for loading state ──────────────────────────────── +function SkeletonCard() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +function SkeletonFeatured() { + return ( +
+
+
+
+
+
+
+
+
+ ); +} + +function MarketSkeletons() { + return ( +
+
+
+ +
+ {Array.from({ length: 4 }, (_, i) => ( + + ))} +
+
+ +
+
+ ); +} + // ── Main trending home view ────────────────────────────────────────── function TrendingHomeView({ markets }: { markets: Market[] }) { const trendingIndex = useStore((s) => s.trendingIndex); const search = useStore((s) => s.search); const nostrPubkey = useStore((s) => s.nostrPubkey); + const marketsLoading = useStore((s) => s.marketsLoading); const trending = useMemo(() => getTrendingMarkets(markets), [markets]); @@ -919,7 +916,7 @@ function TrendingHomeView({ markets }: { markets: Market[] }) { ); if (trending.length === 0) { - return ; + return marketsLoading ? : ; } const featured = trending[trendingIndex % trending.length]; @@ -996,7 +993,7 @@ export default function HomePage() { const search = useStore((s) => s.search); const nostrPubkey = useStore((s) => s.nostrPubkey); - const { data: markets = [], isLoading } = useMarkets(); + const { data: markets = [] } = useMarkets(); // Determine which view to render based on activeCategory const isTrendingLike = @@ -1041,9 +1038,5 @@ export default function HomePage() { } // Trending (default home) - if (isLoading && markets.length === 0) { - return ; - } - return ; } diff --git a/src/hooks/useBootstrap.ts b/src/hooks/useBootstrap.ts index be23f631..365b3825 100644 --- a/src/hooks/useBootstrap.ts +++ b/src/hooks/useBootstrap.ts @@ -160,12 +160,10 @@ export function useBootstrap(): void { // localStorage unavailable } - // 5. Dismiss splash + // 5. Dismiss splash — skeletons handle the loading state from here. + // marketsLoading is only cleared when useMarkets query returns data. await splashReady; dismissSplash(); - - // Mark loading done - useStore.setState({ marketsLoading: false }); } void boot(); diff --git a/src/queries/useMarkets.ts b/src/queries/useMarkets.ts index d33d2757..ab23f08f 100644 --- a/src/queries/useMarkets.ts +++ b/src/queries/useMarkets.ts @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; import { discoveredToMarket } from "../services/markets"; +import { useStore } from "../store"; import type { ChainTipResponse, DiscoveredMarket, Market } from "../types"; export function useMarkets() { @@ -20,10 +21,14 @@ export function useMarkets() { try { const stored = await invoke("discover_contracts"); - return stored.map((d) => ({ + const markets = stored.map((d) => ({ ...discoveredToMarket(d), currentHeight, })); + if (markets.length > 0 && useStore.getState().marketsLoading) { + useStore.setState({ marketsLoading: false }); + } + return markets; } catch { try { const stored = await invoke("list_contracts"); From ec67a59361d3723d35aba1875e0307a2a015d4b4 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 14:34:49 -0400 Subject: [PATCH 021/117] docs: update AGENTS.md and add CLAUDE.md with CI checks and conventions --- AGENTS.md | 63 +++++++++++++++++++++++++++++++++++++++---------------- CLAUDE.md | 15 +++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 75a3744d..adcfd143 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,25 @@ # Agents +## Mandatory CI Checks + +All agents MUST pass these checks before committing or creating PRs. Do NOT push code that fails any of these. + +```bash +# TypeScript type checking +npx tsc --noEmit + +# Biome lint + format (must pass with zero errors) +npx @biomejs/biome check --error-on-warnings . + +# Rust formatting +cd src-tauri && cargo fmt --all -- --check + +# Rust compilation +cd src-tauri && cargo check +``` + +Fix any issues before committing. Formatting fixes should be a separate `style:` commit. + ## Build & Development Commands **All commands must be run through the Nix dev shell:** @@ -10,42 +30,49 @@ nix develop --command just {command} - Install dependencies: `nix develop --command just install` - Dev server: `nix develop --command just dev` - Frontend only: `pnpm dev` (runs on localhost:1420) -- Build frontend: `pnpm run build` - Build native app: `cargo tauri build` - TypeScript check: `nix develop --command just tsc` - Lint: `nix develop --command just biome-lint` - Format: `nix develop --command just biome-format` - Auto-fix: `nix develop --command just biome-fix` -- Screenshot tests: `nix develop --command just screenshots` -- Update screenshots: `nix develop --command just screenshots-update` - Rust format check: `nix develop --command just cargo-fmt` - Rust clippy: `nix develop --command just cargo-clippy` - Rust tests: `nix develop --command just cargo-test` ## Code Style & Conventions + - **Language**: TypeScript (frontend), Rust (backend/Tauri) -- **Frontend**: Vanilla TypeScript with DOM manipulation (no framework — single `render()` function rebuilds the DOM on state change) -- **Styling**: Tailwind CSS with custom design tokens (golden-ratio spacing, custom typography scale) defined in `src/style.css` -- **Formatting/Linting**: Biome (not Prettier/ESLint) -- **Types**: All type definitions live at the top of `src/main.ts` -- **State**: Global `state` object; mutate state then call `render()` to update UI -- **Event handling**: Single event delegation on `#app` element using `data-*` attributes for actions -- **Naming**: camelCase for variables/functions, PascalCase for types, UPPER_SNAKE_CASE for constants -- **Rust**: Edition 2021, minimum rustc 1.77.2; Tauri commands in `src-tauri/src/lib.rs` +- **Frontend**: React with Zustand state management, TanStack Query for data fetching +- **Styling**: Tailwind CSS with custom design tokens defined in `src/style.css` +- **Formatting/Linting**: Biome v2.x — use `npx @biomejs/biome` (NOT `npx biome`, which resolves to old 0.3.3) +- **Types**: Type definitions in `src/types.ts`, store slices in `src/store/index.ts` +- **Naming**: camelCase for variables/functions, PascalCase for types/components, UPPER_SNAKE_CASE for constants +- **Rust**: Edition 2021, minimum rustc 1.77.2 (do not use `std::sync::LazyLock` — use `once_cell::sync::Lazy`) - **Error handling**: Rust commands return `Result`; frontend uses try/catch with `invoke()` +## Commit Conventions + +- Use conventional commit prefixes: `feat:`, `fix:`, `refactor:`, `style:`, `docs:` +- Do NOT add Claude as co-author or contributor +- Do NOT commit or push without explicit user permission +- Do NOT push to master/main without explicit permission +- Do not use nostr.band (defunct) + ## Architecture -- **Frontend entry**: `src/main.ts` — single-file app containing all state, types, rendering, and event handling -- **Backend entry**: `src-tauri/src/lib.rs` — Tauri command handlers for wallet operations (LWK) + +- **Tauri v2** desktop app: TypeScript frontend + Rust backend +- **Frontend entry**: `src/App.tsx` — React app with component tree +- **Backend entry**: `src-tauri/src/lib.rs` — Tauri command handlers +- **SDK**: `src-tauri/crates/deadcat-sdk/` — Liquid wallet (LWK 0.14), Nostr discovery, prediction market covenants +- **Store**: `src-tauri/crates/deadcat-sdk/deadcat-store/` — SQLite persistence via Diesel - **IPC**: Frontend calls Rust backend via `@tauri-apps/api/core` `invoke()` -- **Wallet**: LWK (`lwk_signer`, `lwk_wollet`) for Liquid Network wallet/signer management -- **Data**: Market data is currently mock data embedded in `main.ts` -- **Blockchain**: Blockstream esplora API for chain tip queries +- **Nostr**: Kind 30078 (NIP-78) for market announcements, orders, pools, attestations +- **Blockchain**: Blockstream esplora API for chain tip, Electrum for wallet sync ## Key Types + - `Market`: Core prediction market with covenant state, asset IDs, prices, UTXOs -- `CovenantState`: 0–3 representing settlement stages +- `CovenantState`: 0=Dormant, 1=Unresolved, 2=ResolvedYes, 3=ResolvedNo, 4=Expired - `Side`: "yes" | "no" outcome tokens - `OrderType`: "market" | "limit" -- `ActionTab`: "trade" | "issue" | "redeem" | "cancel" - `WalletNetwork`: "liquid" | "liquid-testnet" | "liquid-regtest" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d15da91d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# Claude Code Instructions + +Follow all instructions in [AGENTS.md](AGENTS.md) — especially the mandatory CI checks. + +## Additional Claude-Specific Rules + +- Do NOT add Claude as co-author or contributor in any commits or git metadata +- When creating PRs, use `🐱 Generated with [Claude Code](https://claude.com/claude-code)` in the PR body footer +- Do NOT commit or push without explicit user permission +- Do NOT push to master/main without explicit permission +- `.DS_Store` files are always untracked and gitignored — never stage or commit them +- Do NOT use nostr.band (defunct) +- Use `npx @biomejs/biome` (NOT `npx biome` which resolves to old 0.3.3) +- Rust MSRV is 1.77.2 — do NOT use `std::sync::LazyLock` (requires 1.80). Use `once_cell::sync::Lazy` +- Use `src/utils-react/friendly-error.ts` `friendlyError()` for all user-facing errors — never show raw SDK error strings From d0486410adb33a7c1cd0c97dec71a395289d4f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?The=20Daniel=20=F0=9F=96=96?= <3638467+dmnyc@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:42:59 -0400 Subject: [PATCH 022/117] fix: onboarding restore flow focus and password persistence bugs (#77) 1. Fix focus stealing bug in manual restore flow where typing in the password field caused focus to return to the seed phrase textarea. The ref callback was running on every re-render. Now uses useEffect to focus only when mnemonicExpanded changes to true. 2. Clear password fields when navigating to any password entry step to ensure users always start fresh. This prevents stale passwords from persisting between navigation flows. https://claude.ai/code/session_018cp7GAwLjq3upwZoQUoUwo Co-authored-by: Claude --- src/components/onboarding/NostrSetupStep.tsx | 24 +++++++++++++++---- src/components/onboarding/WalletSetupStep.tsx | 10 +++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 711d63dc..ca586860 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -2,7 +2,13 @@ import { invoke } from "@tauri-apps/api/core"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { open } from "@tauri-apps/plugin-dialog"; import { readFile, readTextFile } from "@tauri-apps/plugin-fs"; -import { type ReactNode, useCallback, useEffect, useState } from "react"; +import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { useStore } from "../../store"; import type { IdentityResponse, NostrProfile } from "../../types"; import { generateAvatarDataUri } from "../../utils-react/avatar"; @@ -45,6 +51,14 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const [dragOver, setDragOver] = useState(false); const [mnemonicExpanded, setMnemonicExpanded] = useState(false); const [restorePasswordRevealed, setRestorePasswordRevealed] = useState(false); + const mnemonicTextareaRef = useRef(null); + + // Focus the mnemonic textarea only when first expanded, not on every re-render + useEffect(() => { + if (mnemonicExpanded && mnemonicTextareaRef.current) { + mnemonicTextareaRef.current.focus(); + } + }, [mnemonicExpanded]); const nostrMode = useStore((s) => s.onboardingNostrMode); const nostrDone = useStore((s) => s.onboardingNostrDone); const loading = useStore((s) => s.onboardingLoading); @@ -1394,9 +1408,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { rows={3} className="w-full rounded-lg border border-slate-700 bg-slate-900 px-4 py-3 text-sm mono outline-none ring-emerald-400 transition focus:ring-2" disabled={loading} - ref={(el) => { - if (mnemonicExpanded && el) el.focus(); - }} + ref={mnemonicTextareaRef} onBlur={() => { if (!restoreMnemonic.trim()) setMnemonicExpanded(false); }} @@ -1672,6 +1684,8 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { onClick={() => useStore.setState({ onboardingNostrMode: "manual-restore", + onboardingRestorePassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }) } @@ -1736,6 +1750,8 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { onClick={() => useStore.setState({ onboardingNostrMode: "restore", + onboardingRestorePassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }) } diff --git a/src/components/onboarding/WalletSetupStep.tsx b/src/components/onboarding/WalletSetupStep.tsx index c127c0fd..807d38bf 100644 --- a/src/components/onboarding/WalletSetupStep.tsx +++ b/src/components/onboarding/WalletSetupStep.tsx @@ -344,9 +344,11 @@ export default function WalletSetupStep({ } return; } - // For restore/nostr-restore: go to password step + // For restore/nostr-restore: go to password step (clear passwords for fresh start) useStore.setState({ onboardingWalletPasswordStep: true, + onboardingWalletPassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }); }, [walletMode, mnemonic]); @@ -500,6 +502,8 @@ export default function WalletSetupStep({ useStore.setState({ onboardingWalletPasswordStep: true, onboardingMnemonicVerifyStep: false, + onboardingWalletPassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }); }, [mnemonic, verifyIndices, verifyInputs]); @@ -865,6 +869,8 @@ export default function WalletSetupStep({ onClick={() => useStore.setState({ onboardingWalletMode: "nostr-restore", + onboardingWalletPassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }) } @@ -927,6 +933,8 @@ export default function WalletSetupStep({ onClick={() => useStore.setState({ onboardingWalletMode: "restore", + onboardingWalletPassword: "", + onboardingWalletPasswordConfirm: "", onboardingError: "", }) } From c00da614e17becca60d9a2c097ddf32f94e75bde Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 16:59:36 -0400 Subject: [PATCH 023/117] =?UTF-8?q?perf:=20non-blocking=20startup=20?= =?UTF-8?q?=E2=80=94=20serve=20cached=20data=20instantly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Break the blocking dependency chain that caused 30+ second startup when a relay was down. Four changes: 1. Add 3s connect_timeout to all relay connection calls — dead relays are skipped instead of hanging indefinitely 2. Make construct_and_store_node non-blocking — node is stored immediately, start_subscription() runs in background tokio::spawn 3. Restructure bootstrap to not await init_nostr_identity — identity loads in background, splash dismisses unconditionally, parallel Promise.all for independent backend queries 4. Remove fetch_chain_tip from market query critical path — cached at module level in background, never blocks discover_contracts --- .../deadcat-sdk/src/discovery/config.rs | 5 + .../crates/deadcat-sdk/src/discovery/mod.rs | 4 +- .../deadcat-sdk/src/discovery/service.rs | 8 +- src-tauri/src/commands.rs | 20 +- src/hooks/useBootstrap.ts | 210 ++++++++---------- src/queries/useMarkets.ts | 46 ++-- 6 files changed, 137 insertions(+), 156 deletions(-) diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/config.rs b/src-tauri/crates/deadcat-sdk/src/discovery/config.rs index d2695ce2..868236ea 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/config.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/config.rs @@ -11,6 +11,10 @@ pub struct DiscoveryConfig { pub relays: Vec, /// Network tag value (e.g. "liquid-testnet"). pub network_tag: String, + /// Timeout for relay connection attempts. Relays that don't respond + /// within this window are skipped — the app proceeds with whichever + /// relays connected in time. + pub connect_timeout: Duration, /// Timeout for one-shot fetch operations. pub fetch_timeout: Duration, /// When set, only discover markets published by this author. @@ -22,6 +26,7 @@ impl Default for DiscoveryConfig { Self { relays: DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect(), network_tag: super::NETWORK_TAG.to_string(), + connect_timeout: Duration::from_secs(3), fetch_timeout: Duration::from_secs(5), source_author: None, } diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs b/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs index 6b42f477..2d6019a0 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs @@ -537,7 +537,9 @@ pub async fn connect_client(relay_url: Option<&str>) -> Result { .map_err(|e| format!("failed to add relay {url}: {e}"))?; } } - client.connect().await; + client + .connect_with_timeout(std::time::Duration::from_secs(3)) + .await; Ok(client) } diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/service.rs b/src-tauri/crates/deadcat-sdk/src/discovery/service.rs index c799daab..34bb30f2 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/service.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/service.rs @@ -137,7 +137,9 @@ impl DiscoveryService { .await .map_err(|e| format!("failed to add relay {url}: {e}"))?; } - self.client.connect().await; + self.client + .connect_with_timeout(self.config.connect_timeout) + .await; let client = self.client.clone(); let store = self.store.clone(); @@ -440,7 +442,9 @@ impl DiscoveryService { .await .map_err(|e| format!("failed to add relay {url}: {e}"))?; } - self.client.connect().await; + self.client + .connect_with_timeout(self.config.connect_timeout) + .await; } Ok(()) } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d516edf8..6af58d71 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -203,16 +203,18 @@ async fn construct_and_store_node( _t0.elapsed() ); - // Start the background Nostr subscription loop (guard is released) - if let Some(node) = node_arc.as_ref() { - if let Err(e) = node.start_subscription().await { - log::warn!("failed to start discovery subscription: {e}"); - } + // Start the Nostr subscription in the background — do NOT await. + // This connects to relays and starts the event loop. The node is + // already stored in NodeState so commands like discover_contracts + // can use it immediately (they handle missing relay data gracefully). + if let Some(node) = node_arc.clone() { + tokio::spawn(async move { + if let Err(e) = node.start_subscription().await { + log::warn!("failed to start discovery subscription: {e}"); + } + log::info!("discovery subscription started"); + }); } - log::info!( - "[restore-trace] construct_and_store_node: subscription started at {:?}", - _t0.elapsed() - ); // Forward discovery events to the frontend let app_handle = app.clone(); diff --git a/src/hooks/useBootstrap.ts b/src/hooks/useBootstrap.ts index 365b3825..a106f692 100644 --- a/src/hooks/useBootstrap.ts +++ b/src/hooks/useBootstrap.ts @@ -22,146 +22,118 @@ export function useBootstrap(): void { async function boot(): Promise { const splashReady = new Promise((r) => setTimeout(r, 1500)); - // 1. Load Nostr identity + // ── Immediate: non-network state ────────────────────────────── + // Load persisted tx labels (sync localStorage) try { - const identity = await invoke( - "init_nostr_identity", + const stored = JSON.parse( + localStorage.getItem("deadcat_tx_labels") ?? "{}", ); - if (identity) { - useStore.setState({ - nostrPubkey: identity.pubkey_hex, - nostrNpub: identity.npub, - }); - } - } catch (error) { - console.warn("Failed to load nostr identity:", error); - } - - // 2. Fetch Nostr profile (kind 0 metadata) - const { nostrPubkey: loadedPubkey } = useStore.getState(); - if (loadedPubkey) { - // First attempt — may fail if relays aren't connected yet - let fetched = false; - try { - const profile = await invoke( - "fetch_nostr_profile", - ); - if (profile) { - useStore.setState({ nostrProfile: profile }); - fetched = true; - } - } catch { - // Will retry below + const labels = new Map(); + for (const [txid, entry] of Object.entries(stored)) { + const { label, marketId } = entry as { + label: string; + marketId: string; + }; + labels.set(txid, { label, marketId }); } - // If first attempt failed, retry in background after relays settle - if (!fetched) { - setTimeout(async () => { - try { - const profile = await invoke( - "fetch_nostr_profile", - ); - if (profile) useStore.setState({ nostrProfile: profile }); - } catch { - // Give up - } - }, 3000); + if (labels.size > 0) { + useStore.setState({ recentTxLabels: labels }); } + } catch { + // localStorage unavailable } - // Load relay list - const { nostrNpub } = useStore.getState(); - if (nostrNpub) { - try { - const relays = - await invoke<{ url: string; has_backup: boolean }[]>( - "get_relay_list", - ); - if (relays.length > 0) { - useStore.setState({ relays }); - } else { - useStore.setState({ - relays: [ - { url: "wss://relay.damus.io", has_backup: false }, - { url: "wss://relay.primal.net", has_backup: false }, - ], - }); - } - } catch { - useStore.setState({ - relays: [ - { url: "wss://relay.damus.io", has_backup: false }, - { url: "wss://relay.primal.net", has_backup: false }, - ], - }); - } - } else { - useStore.setState({ - relays: [ - { url: "wss://relay.damus.io", has_backup: false }, - { url: "wss://relay.primal.net", has_backup: false }, - ], - }); - } + // ── Parallel: independent backend queries ───────────────────── + // None of these depend on each other. Fire all at once. + const [, , appState] = await Promise.all([ + // Source npub (needed for market filtering) + invoke("get_source_npub") + .then((npub) => + useStore.setState({ sourceNpub: npub, sourceNpubInput: npub }), + ) + .catch(() => {}), - // Load source npub - try { - const npub = await invoke("get_source_npub"); - useStore.setState({ sourceNpub: npub, sourceNpubInput: npub }); - } catch { - /* use default from store */ - } + // Relay list + invoke<{ url: string; has_backup: boolean }[]>("get_relay_list") + .then((relays) => { + if (relays.length > 0) { + useStore.setState({ relays }); + } + }) + .catch(() => {}), - // 3. Fetch wallet status - try { - const appState = await invoke<{ + // Wallet / network status + invoke<{ walletStatus: "not_created" | "locked" | "unlocked"; networkStatus: { network: "mainnet" | "testnet" | "regtest"; policyAssetId: string; }; - }>("get_app_state"); - const { nostrPubkey: currentPubkey } = useStore.getState(); - // If wallet is locked but no identity exists, the wallet can't be - // unlocked (the node requires an identity first). Treat as not_created - // so the user sees wallet setup instead of an unlock prompt. - const effectiveStatus = - appState.walletStatus === "locked" && !currentPubkey - ? "not_created" - : appState.walletStatus; + }>("get_app_state").catch(() => null), + ]); + + if (appState) { useStore.setState({ - walletStatus: effectiveStatus as - | "not_created" - | "locked" - | "unlocked", walletNetwork: appState.networkStatus.network, walletPolicyAssetId: appState.networkStatus.policyAssetId, }); - } catch (e) { - console.warn("Failed to fetch app state:", e); } - // 4. Load persisted tx labels - try { - const stored = JSON.parse( - localStorage.getItem("deadcat_tx_labels") ?? "{}", - ); - const labels = new Map(); - for (const [txid, entry] of Object.entries(stored)) { - const { label, marketId } = entry as { - label: string; - marketId: string; - }; - labels.set(txid, { label, marketId }); - } - if (labels.size > 0) { - useStore.setState({ recentTxLabels: labels }); - } - } catch { - // localStorage unavailable + // ── Background: identity + node (does NOT block splash) ─────── + // Node construction is fast now (relay connect is spawned in bg). + // Profile fetch retries after relays settle. + invoke("init_nostr_identity") + .then((identity) => { + if (identity) { + useStore.setState({ + nostrPubkey: identity.pubkey_hex, + nostrNpub: identity.npub, + }); + } + // Re-evaluate wallet status now that we know if identity exists + if (appState) { + const { nostrPubkey: pk } = useStore.getState(); + const effectiveStatus = + appState.walletStatus === "locked" && !pk + ? "not_created" + : appState.walletStatus; + useStore.setState({ + walletStatus: effectiveStatus as + | "not_created" + | "locked" + | "unlocked", + }); + } + // Fetch profile once relays are connected (retry after delay) + if (identity) { + invoke("fetch_nostr_profile") + .then((p) => { + if (p) useStore.setState({ nostrProfile: p }); + }) + .catch(() => { + setTimeout(() => { + invoke("fetch_nostr_profile") + .then((p) => { + if (p) useStore.setState({ nostrProfile: p }); + }) + .catch(() => {}); + }, 3000); + }); + } + }) + .catch((e) => console.warn("Failed to load nostr identity:", e)); + + // Set initial wallet status (without identity check) so UI isn't blank + if (appState) { + useStore.setState({ + walletStatus: appState.walletStatus as + | "not_created" + | "locked" + | "unlocked", + }); } - // 5. Dismiss splash — skeletons handle the loading state from here. - // marketsLoading is only cleared when useMarkets query returns data. + // ── Dismiss splash ──────────────────────────────────────────── await splashReady; dismissSplash(); } diff --git a/src/queries/useMarkets.ts b/src/queries/useMarkets.ts index ab23f08f..e2d93786 100644 --- a/src/queries/useMarkets.ts +++ b/src/queries/useMarkets.ts @@ -4,43 +4,39 @@ import { discoveredToMarket } from "../services/markets"; import { useStore } from "../store"; import type { ChainTipResponse, DiscoveredMarket, Market } from "../types"; +// Chain tip is fetched once and cached in module scope. +// Updated in the background — never blocks market display. +let cachedHeight = 0; +invoke("fetch_chain_tip", { network: "liquid-testnet" }) + .then((tip) => { + cachedHeight = tip.height; + }) + .catch(() => {}); + export function useMarkets() { return useQuery({ queryKey: ["markets"], queryFn: async (): Promise => { - // Fetch chain tip so time-remaining calculations are accurate. - let currentHeight = 0; - try { - const tip = await invoke("fetch_chain_tip", { - network: "liquid-testnet", - }); - currentHeight = tip.height; - } catch { - // Non-fatal — falls back to 0 (shows raw block distance) - } - + let stored: DiscoveredMarket[]; try { - const stored = await invoke("discover_contracts"); - const markets = stored.map((d) => ({ - ...discoveredToMarket(d), - currentHeight, - })); - if (markets.length > 0 && useStore.getState().marketsLoading) { - useStore.setState({ marketsLoading: false }); - } - return markets; + stored = await invoke("discover_contracts"); } catch { try { - const stored = await invoke("list_contracts"); - return stored.map((d) => ({ - ...discoveredToMarket(d), - currentHeight, - })); + stored = await invoke("list_contracts"); } catch (error) { console.warn("Failed to load markets:", error); return []; } } + + const markets = stored.map((d) => ({ + ...discoveredToMarket(d), + currentHeight: cachedHeight, + })); + if (markets.length > 0 && useStore.getState().marketsLoading) { + useStore.setState({ marketsLoading: false }); + } + return markets; }, staleTime: 30_000, }); From 3f67aef93b869a44ea9091acdfcd86a2a6eedb66 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 17:26:35 -0400 Subject: [PATCH 024/117] perf: persist relay market cache to disk for instant cold start Markets from relays were only stored in memory (RELAY_MARKET_CACHE) and lost on restart. Every cold start required a full relay fetch (5-30s depending on relay health). Now the cache is saved to relay_market_cache.json and loaded at app startup, giving sub-second market display on returning launches. --- src-tauri/src/commands.rs | 46 +++++++++++++++++++++++++++++++++++++-- src-tauri/src/lib.rs | 3 +++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6af58d71..747c50fb 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,11 +18,51 @@ use crate::{NodeState, NostrAppState}; const ORDER_INDEX_AUTO_RESOLVE_SENTINEL: u32 = u32::MAX; -/// Cache of markets fetched from relays but not yet promoted in the store. -/// Persists across `discover_contracts` calls so markets don't flash and disappear. +/// Cache of markets fetched from relays. Persisted to disk as JSON so +/// markets are available instantly on cold start without waiting for relays. static RELAY_MARKET_CACHE: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| std::sync::Mutex::new(Vec::new())); +const RELAY_CACHE_FILE: &str = "relay_market_cache.json"; + +fn relay_cache_path(app: &tauri::AppHandle) -> Option { + app.path() + .app_data_dir() + .ok() + .map(|d| d.join(RELAY_CACHE_FILE)) +} + +pub fn load_relay_cache(app: &tauri::AppHandle) { + let Some(path) = relay_cache_path(app) else { + return; + }; + let Ok(data) = std::fs::read_to_string(&path) else { + return; + }; + let Ok(markets) = serde_json::from_str::>(&data) else { + return; + }; + if let Ok(mut cache) = RELAY_MARKET_CACHE.lock() { + *cache = markets; + } +} + +fn save_relay_cache(app: &tauri::AppHandle) { + let Some(path) = relay_cache_path(app) else { + return; + }; + let Ok(cache) = RELAY_MARKET_CACHE.lock() else { + return; + }; + if cache.is_empty() { + return; + } + let Ok(json) = serde_json::to_string(&*cache) else { + return; + }; + let _ = std::fs::write(path, json); +} + fn validate_request(request: &CreateContractRequest) -> Result<(), String> { if request.question.trim().is_empty() || request.question.len() > 140 { return Err("question must be 1-140 characters".to_string()); @@ -1029,6 +1069,8 @@ pub async fn discover_contracts(app: tauri::AppHandle) -> Result Date: Thu, 16 Apr 2026 17:30:35 -0400 Subject: [PATCH 025/117] feat: add checkmark icon for Resolved category tab --- src/components/layout/TopShell.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index dfbe9c4f..e531dd13 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -106,6 +106,13 @@ export function categoryIcon( ); + case "Resolved": + return ( + + ); case "My Markets": return ( ); + case "Politics": + return ( + + ); + case "Sports": + return ( + + ); + case "Culture": + return ( + + ); + case "Bitcoin": + return ( + + ); + case "Weather": + return ( + + ); + case "Macro": + return ( + + ); + case "Resolved": + return ( + + ); case "My Markets": return (

- + + + {title}

@@ -330,7 +397,9 @@ function PortfolioView({ markets }: { markets: Market[] }) {

- + + + Portfolio

@@ -584,7 +653,9 @@ function MyMarketsView({ markets }: { markets: Market[] }) {

- + + + My Markets

{marketMakerMode && ( @@ -698,6 +769,9 @@ function CategoryPageView({

+ + + {category}

diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index e531dd13..c55259e9 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -454,7 +454,7 @@ function CategoryBar() { }`} > {icon} - {category} + {category === "Ending Soon" ? "Ending" : category} ); })} diff --git a/src/constants.ts b/src/constants.ts index 31eaaf73..537b1a40 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,7 +6,6 @@ export const SATS_PER_FULL_CONTRACT = 2; // multiplied by cptSats at runtime export const categories: NavCategory[] = [ "Trending", - "Ending Soon", "New", "Portfolio", "Politics", @@ -15,6 +14,7 @@ export const categories: NavCategory[] = [ "Bitcoin", "Weather", "Macro", + "Ending Soon", "Resolved", "My Markets", ]; From 649af2298ae122ef0b4fda7bb7ad2440609792b0 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Fri, 17 Apr 2026 14:21:09 -0400 Subject: [PATCH 027/117] fix: unlock body scroll when profile modal is closed useLockScroll() ran on every render of ProfilePage because the early-return guard came after the hook. That locked body/html overflow on app start and never unlocked. Split into a wrapper that guards profileOpen before any hooks and an inner ProfilePageContent that only mounts when the modal is open (same pattern as SettingsPanel). --- src/components/profile/ProfilePage.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/profile/ProfilePage.tsx b/src/components/profile/ProfilePage.tsx index 72862851..db58b121 100644 --- a/src/components/profile/ProfilePage.tsx +++ b/src/components/profile/ProfilePage.tsx @@ -65,6 +65,11 @@ async function pickAndUpload(mediaType: "avatar" | "banner"): Promise { export default function ProfilePage() { const profileOpen = useStore((s) => s.profileOpen); + if (!profileOpen) return null; + return ; +} + +function ProfilePageContent() { const nostrNpub = useStore((s) => s.nostrNpub); const nostrProfile = useStore((s) => s.nostrProfile); @@ -99,15 +104,12 @@ export default function ProfilePage() { useLockScroll(); useEffect(() => { - if (profileOpen) { - // Fetch latest profile from relays - invoke("fetch_nostr_profile") - .then((profile) => { - if (profile) useStore.setState({ nostrProfile: profile }); - }) - .catch(() => {}); - } - }, [profileOpen]); + invoke("fetch_nostr_profile") + .then((profile) => { + if (profile) useStore.setState({ nostrProfile: profile }); + }) + .catch(() => {}); + }, []); const close = useCallback(() => { useStore.setState({ profileOpen: false }); @@ -155,8 +157,6 @@ export default function ProfilePage() { setSaving(false); }, [name, displayName, picture, banner, about, website, nip05, lud16, close]); - if (!profileOpen) return null; - return (
Date: Fri, 17 Apr 2026 14:21:15 -0400 Subject: [PATCH 028/117] feat: embed macOS traffic lights via transparent title bar - hiddenTitle + titleBarStyle: Transparent in tauri.conf - core:window:allow-start-dragging capability for JS drag API - data-os attribute on for platform-scoped CSS - onMouseDown drag handler on empty header space (phi-container and the flex row) so users can drag the window from any empty area of the top bar. Scoped via event.target === currentTarget so interactive children still receive their normal clicks. --- src-tauri/capabilities/default.json | 1 + src-tauri/tauri.conf.json | 4 +++- src/App.tsx | 12 ++++++++++++ src/components/layout/TopShell.tsx | 18 ++++++++++++++++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 6f890e93..6d895f09 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ ], "permissions": [ "core:default", + "core:window:allow-start-dragging", "opener:default", "dialog:default", "fs:default" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f311084c..f45ed296 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,7 +18,9 @@ "minWidth": 900, "minHeight": 600, "resizable": true, - "fullscreen": false + "fullscreen": false, + "hiddenTitle": true, + "titleBarStyle": "Transparent" } ], "security": { diff --git a/src/App.tsx b/src/App.tsx index a34566ca..60dd21fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,6 +64,18 @@ export default function App() { useTauriEvents(); useActivityTracking(); + // Tag with the host OS so CSS can key off it (e.g. reserve space + // for macOS traffic lights without affecting other platforms). + useEffect(() => { + const ua = navigator.userAgent; + const os = /Mac/i.test(ua) + ? "macos" + : /Win/i.test(ua) + ? "windows" + : "linux"; + document.documentElement.dataset.os = os; + }, []); + // Listen for close-requested from Rust (Cmd+Q / window close) useEffect(() => { const unlisten = listen("close-requested", () => { diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index c55259e9..710ada0b 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { useCallback, useMemo } from "react"; import { categories } from "../../constants"; import { useStore } from "../../store"; @@ -9,6 +10,16 @@ import { SearchBar } from "./SearchBar"; import { SettingsPanel } from "./SettingsPanel"; import { UserMenu } from "./UserMenu"; +// Empty header space becomes a window drag handle. Children with their own +// interactive behavior (buttons, inputs) are not `event.target`, so they +// still receive their normal click events. +function onDragMouseDown(e: React.MouseEvent): void { + if (e.buttons !== 1) return; + if (e.target !== e.currentTarget) return; + e.preventDefault(); + void getCurrentWindow().startDragging(); +} + /* ── Category icon helper ──────────────────────────────────────────── */ const iconProps = (size: string) => @@ -655,8 +666,11 @@ export function TopShell() { return ( <>
-
-
+
+
{/* Logo */} diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index 2ef85bc5..54485ae7 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useCallback, useMemo } from "react"; import { categories } from "../../constants"; +import { useEscapeKey } from "../../hooks/useEscapeKey"; import { useStore } from "../../store"; import type { NavCategory } from "../../types"; import { formatCompactSats } from "../../utils-react/wallet"; @@ -158,6 +159,8 @@ function LogoutModal() { }); }, []); + useEscapeKey(logoutOpen, closeLogout); + const confirmLogout = useCallback(async () => { // Lock first to ensure clean state, then delete try { @@ -670,7 +673,7 @@ export function TopShell() { onMouseDown={onDragMouseDown} >
@@ -684,12 +687,18 @@ export function TopShell() { {/* Center: search bar */} -
+
{/* Right side: wallet + user menu */} -
+
{nostrPubkey && ( - + )}
diff --git a/src/components/onboarding/OnboardingOverlay.tsx b/src/components/onboarding/OnboardingOverlay.tsx index e9fc419c..da23c33b 100644 --- a/src/components/onboarding/OnboardingOverlay.tsx +++ b/src/components/onboarding/OnboardingOverlay.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { useEscapeKey } from "../../hooks/useEscapeKey"; import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { CloseButton } from "../shared/CloseButton"; @@ -38,6 +39,8 @@ export default function OnboardingOverlay() { }); }, []); + useEscapeKey(setupModalOpen && !backupFileContent, handleClose); + if (!setupModalOpen) return null; // Step indicator for multi-step onboarding (hidden in wallet-only mode) diff --git a/src/components/profile/ProfilePage.tsx b/src/components/profile/ProfilePage.tsx index db58b121..c4764f71 100644 --- a/src/components/profile/ProfilePage.tsx +++ b/src/components/profile/ProfilePage.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; import { readFile } from "@tauri-apps/plugin-fs"; import { useCallback, useEffect, useState } from "react"; +import { useEscapeKey } from "../../hooks/useEscapeKey"; import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { generateAvatarDataUri } from "../../utils-react/avatar"; @@ -101,7 +102,12 @@ function ProfilePageContent() { } }, [nostrProfile]); + const close = useCallback(() => { + useStore.setState({ profileOpen: false }); + }, []); + useLockScroll(); + useEscapeKey(true, close); useEffect(() => { invoke("fetch_nostr_profile") @@ -111,10 +117,6 @@ function ProfilePageContent() { .catch(() => {}); }, []); - const close = useCallback(() => { - useStore.setState({ profileOpen: false }); - }, []); - const handleBackdropClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) close(); diff --git a/src/components/wallet/WalletPage.tsx b/src/components/wallet/WalletPage.tsx index e4972441..bf2f7342 100644 --- a/src/components/wallet/WalletPage.tsx +++ b/src/components/wallet/WalletPage.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { useEscapeKey } from "../../hooks/useEscapeKey"; import { useLockScroll } from "../../hooks/useLockScroll"; import { useStore } from "../../store"; import { CloseButton } from "../shared/CloseButton"; @@ -25,6 +26,7 @@ export function WalletPage() { ); useLockScroll(); + useEscapeKey(walletOpen, close); if (!walletOpen) return null; diff --git a/src/hooks/useEscapeKey.ts b/src/hooks/useEscapeKey.ts new file mode 100644 index 00000000..e8797563 --- /dev/null +++ b/src/hooks/useEscapeKey.ts @@ -0,0 +1,19 @@ +import { useEffect } from "react"; + +/** Call `onEscape` when the user presses Escape while enabled. */ +export function useEscapeKey(enabled: boolean, onEscape: () => void): void { + useEffect(() => { + if (!enabled) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + onEscape(); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [enabled, onEscape]); +} diff --git a/src/style.css b/src/style.css index 40f5d7a8..6de3ddc5 100644 --- a/src/style.css +++ b/src/style.css @@ -64,6 +64,12 @@ select { color-scheme: dark; } +button:focus-visible, +[role="button"]:focus-visible { + outline: none; + box-shadow: inset 0 0 0 9999px rgba(51, 65, 85, 0.38); +} + /* ===== Form components ===== */ @layer components { @@ -109,7 +115,7 @@ select { the macOS traffic-light buttons (which the OS anchors at top-left). Scoped to the
's container so page-content .phi-container instances are unaffected. */ -:root[data-os="macos"] header .phi-container { +:root[data-os="macos"] .top-shell__title-row { padding-top: 32px; } From 54fd3bb9e48ddd5999d6f2dc1925ac216e4fb586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?The=20Daniel=20=F0=9F=96=96?= <3638467+dmnyc@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:12:41 -0400 Subject: [PATCH 032/117] Settings version footer + scrollbar and traffic-light polish (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: show app version and GitHub link in Settings footer Adds a small, muted footer to the Settings panel: the cat mark, the app version in SemVer build-metadata form (`0.1.0+abc1234`, with a trailing `.modified` if the tree is dirty), and a "Report bugs or contribute" link that opens the GitHub repo via tauri-plugin-opener. The version string is assembled from Cargo's package version plus a 7-char git short hash embedded at build time. build.rs declares `rerun-if-changed` on `.git/HEAD`, the current ref file, and `.git/index` so the hash and dirty flag stay accurate across commits and stage state changes without requiring a clean build. Falls back to "unknown" if git isn't available (e.g. source-tarball builds). * style: narrower scrollbar blended with the page gradient Thins the page scrollbar from 10px → 6px (less layout shift in WebKit's classic-mode scrollbar) and tints the track with a vertical gradient (#020617 → #0b1222 → #0f172a) that matches the body's diagonal gradient at both the top and bottom of the page. Previously the track was a single dark slab that read as grey where the body had scrolled to its lighter lower-right region. Also sets \`color-scheme: dark\` on so native form controls and scrollbar fallbacks render with light-on-dark contrast without the app having to specify it per element. * style: gradient hood behind macOS traffic lights Adds a fixed, viewport-wide 32px gradient strip at the top of the window on macOS (dark → transparent) so content that scrolls under the OS-anchored traffic lights reads as subordinate rather than bumping into them. \`pointer-events: none\` keeps the native drag region and traffic-light clicks unaffected. Scoped via \`:root[data-os="macos"]\` so Windows/Linux are untouched. * copy: truthful quit-confirm message Replaces the generic "Make sure you've saved your work" line in the quit dialog — Deadcat doesn't have documents to save. The wallet and identity are already persisted; the real risk is interrupting an in-flight transaction or swap. --------- Co-authored-by: The Daniel --- src-tauri/build.rs | 39 +++++++++++++++++ src-tauri/src/lib.rs | 30 +++++++++++++ src/App.tsx | 3 +- src/components/layout/SettingsPanel.tsx | 45 +++++++++++++++++++ src/style.css | 58 ++++++++++++++++--------- 5 files changed, 154 insertions(+), 21 deletions(-) diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e6..a81f54f6 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,42 @@ +use std::path::Path; +use std::process::Command; + fn main() { + emit_git_info(); tauri_build::build() } + +fn emit_git_info() { + let hash = run_git(&["rev-parse", "--short=7", "HEAD"]).unwrap_or_else(|| "unknown".into()); + let dirty = run_git(&["status", "--porcelain"]) + .map(|out| !out.is_empty()) + .unwrap_or(false); + + println!("cargo:rustc-env=GIT_HASH={hash}"); + println!( + "cargo:rustc-env=GIT_DIRTY={}", + if dirty { "1" } else { "0" } + ); + + // Re-run build.rs when the current commit changes. HEAD may be a symbolic + // ref (branch) or a detached commit; watch the ref file it points to so + // we catch commits that don't move HEAD itself. + let git_dir = run_git(&["rev-parse", "--git-dir"]).unwrap_or_else(|| ".git".into()); + println!("cargo:rerun-if-changed={git_dir}/HEAD"); + if let Ok(head) = std::fs::read_to_string(Path::new(&git_dir).join("HEAD")) { + if let Some(refname) = head.strip_prefix("ref: ").map(str::trim) { + println!("cargo:rerun-if-changed={git_dir}/{refname}"); + } + } + // `git status` reads the index; rerun when it changes so GIT_DIRTY stays + // accurate across stage/unstage. + println!("cargo:rerun-if-changed={git_dir}/index"); +} + +fn run_git(args: &[&str]) -> Option { + let out = Command::new("git").args(args).output().ok()?; + if !out.status.success() { + return None; + } + Some(String::from_utf8_lossy(&out.stdout).trim().to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dd160e59..a155173f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1402,6 +1402,35 @@ fn confirm_quit(app: AppHandle) { app.exit(0); } +#[derive(serde::Serialize)] +struct AppVersion { + version: &'static str, + commit: &'static str, + modified: bool, + /// SemVer build-metadata form: `0.1.0+abc1234` (or `+abc1234.modified`). + display: String, +} + +/// Version string assembled from Cargo package version + git short hash +/// embedded at build time. Shown in Settings. +#[tauri::command] +fn get_app_version() -> AppVersion { + let version = env!("CARGO_PKG_VERSION"); + let commit = env!("GIT_HASH"); + let modified = env!("GIT_DIRTY") == "1"; + let display = if modified { + format!("{version}+{commit}.modified") + } else { + format!("{version}+{commit}") + }; + AppVersion { + version, + commit, + modified, + display, + } +} + // ============================================================================ // App Entry Point // ============================================================================ @@ -1635,6 +1664,7 @@ pub fn run() { // Legacy fetch_chain_tip, confirm_quit, + get_app_version, // SDK / Nostr commands::init_nostr_identity, commands::generate_nostr_identity, diff --git a/src/App.tsx b/src/App.tsx index 82633cdf..797db8cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,7 +39,8 @@ function CloseConfirmDialog() { Quit Deadcat Live?

- Make sure you've saved your work before quitting. + Your wallet and identity are saved. Pending operations will be + interrupted.

+ )}
); } + +function VersionFooter() { + const [display, setDisplay] = useState(""); + + useEffect(() => { + invoke<{ display: string }>("get_app_version") + .then((v) => setDisplay(v.display)) + .catch(() => setDisplay("")); + }, []); + + if (!display) return null; + + return ( +
+ + v{display} + +
+ ); +} diff --git a/src/style.css b/src/style.css index 6de3ddc5..8f7f4016 100644 --- a/src/style.css +++ b/src/style.css @@ -24,7 +24,10 @@ } html { + /* Solid dark so the scrollbar gutter and any macOS elastic-overscroll + exposure match the top-left stop of the body gradient below. */ background: #020617; + color-scheme: dark; } html, @@ -119,6 +122,26 @@ button:focus-visible, padding-top: 32px; } +/* Soft "hood" behind the macOS traffic lights so content that scrolls + under them reads as subordinate. Fades to transparent over 32px so it + doesn't add a hard edge. Pointer-events:none lets the native drag + region and traffic-light clicks pass through. */ +:root[data-os="macos"]::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 32px; + background: linear-gradient( + to bottom, + rgba(2, 6, 23, 0.92), + rgba(2, 6, 23, 0) + ); + pointer-events: none; + z-index: 1000; +} + .phi-title { max-width: 34ch; } @@ -467,29 +490,24 @@ button:focus-visible, } } +/* Narrow, dark scrollbar. The track uses a vertical approximation of the + body's diagonal gradient so it blends with the page at both the top + (#020617) and the bottom (#0f172a) instead of reading as a dark slab + on the lighter part of the page. */ ::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 6px; + height: 6px; } - -::-webkit-scrollbar-thumb { - border-radius: 9999px; - border: 2px solid #0b1222; - background-color: #0b1222; -} - -body:hover::-webkit-scrollbar-thumb { - background-color: #1e293b; -} - -body:hover::-webkit-scrollbar-thumb:hover { - background-color: #334155; -} - ::-webkit-scrollbar-track { - background: #0b1222; + background: linear-gradient(to bottom, #020617 0%, #0b1222 45%, #0f172a 100%); } - ::-webkit-scrollbar-corner { - background: #0b1222; + background-color: #0f172a; +} +::-webkit-scrollbar-thumb { + background: #1e293b; + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: #334155; } From 5a5bd092f7244c20870d84b2d6292a3053aca6f3 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Sun, 19 Apr 2026 15:25:28 -0400 Subject: [PATCH 033/117] fix: logo click resets category to Trending The goHome handler on the app logo only reset view + selectedMarketId, leaving activeCategory on whatever tab the user had navigated to. So clicking the logo from Sports (or any non-default category) stayed in that category rather than returning to the default Trending view. --- src/components/layout/TopShell.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index 54485ae7..129231a5 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -663,7 +663,11 @@ export function TopShell() { const activeCategory = useStore((s) => s.activeCategory); const goHome = useCallback(() => { - useStore.setState({ view: "home", selectedMarketId: "" }); + useStore.setState({ + view: "home", + activeCategory: "Trending", + selectedMarketId: "", + }); }, []); return ( From 6fe959b9f7f84ddb60f8260d9471d5c6491e80c8 Mon Sep 17 00:00:00 2001 From: Uty-re Date: Tue, 21 Apr 2026 16:13:02 +0100 Subject: [PATCH 034/117] Feat/preview charts (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: real sparklines, chart timestamps, category filters, doc sync - MarketCard.tsx: ChartSparkline now fetches real price history via usePriceHistory() with mock fallback; color reflects trend direction - HomePage.tsx: wire CategoryPageView sort buttons (trending/frequency) and left sidebar filter pills (all/live/ending-soon); fix state mix labels to user-friendly names (Dormant, Live, Resolved YES/NO, Expired) - MarketChart.tsx: replace preserveAspectRatio=none with ResizeObserver to measure actual container; switch timescale buttons from block units (10B/25B/50B/100B) to time units (30m/1h/4h/1d); hover tooltip shows timestamp instead of raw block height - chart-series.ts: update scale/point maps for time-based timescales; add blockHeightToHoverLabel() export - mock-price-history.ts: 8 deterministic chart shapes per market (trending-up/down, volatile, v-shape, inverted-v, late-spike, early-peak, s-curve); 80 points; always produces data even when yesPrice is null (defaults to 50%) - store/index.ts: add categorySortMode and categoryFilter fields; default chartTimescale to "1h" - types.ts: ChartTimescale changed to "30m"|"1h"|"4h"|"1d" - docs/ux/views.md: full Home View section rewrite documenting all implemented features (wallet button states, logout modal, category page layout, portfolio view, my markets view, empty/loading states) - docs/ux/design.md, design-decisions.md: architecture updates Co-Authored-By: Claude Sonnet 4.6 * feat: preview charts — sparklines, timestamps, 3d/7d timescales, label fix - Add real sparklines to MarketCard using usePriceHistory + mock fallback - Rewrite mock price history with 8 deterministic shapes over last 7d window - Add ResizeObserver to MarketChart for correct aspect ratio (no squashing) - Convert x-axis from block heights to wall-clock timestamps - Extend ChartTimescale from 4 options to 1h/4h/1d/3d/7d - Fix empty chart: anchor mock data to currentHeight - 10080 blocks - Fix lines not starting from left: clamp pre-window samples + include anchor point - Fix repeated x-axis labels: timescale-aware formatting + key={i} not key={label} Co-Authored-By: Claude Sonnet 4.6 * fix: use date-only labels on 3d/7d chart x-axis Time precision isn't useful on multi-day views — show "Apr 20" not "Apr 20 02:30 PM". * fix: biome format — line length, import order, method chaining style * fix: replace .at(-1) with index access for pre-ES2022 TS compat --------- Co-authored-by: Claude Sonnet 4.6 --- docs/ux/design-decisions.md | 30 ++- docs/ux/design.md | 27 +- docs/ux/views.md | 358 ++++++++++++++++++-------- src/components/chart/MarketChart.tsx | 57 ++-- src/components/home/HomePage.tsx | 94 ++++--- src/components/home/MarketCard.tsx | 37 ++- src/store/index.ts | 6 +- src/types.ts | 2 +- src/utils-react/chart-series.ts | 113 ++++++-- src/utils-react/mock-price-history.ts | 208 +++++++++++++-- 10 files changed, 692 insertions(+), 240 deletions(-) diff --git a/docs/ux/design-decisions.md b/docs/ux/design-decisions.md index b7d1a80c..a355d241 100644 --- a/docs/ux/design-decisions.md +++ b/docs/ux/design-decisions.md @@ -6,23 +6,29 @@ Each entry follows the format from [deadcat-core-design.md](../architecture/dead ## Rendering & Architecture -### Vanilla HTML String Rendering Over Virtual DOM Framework +### React Over Vanilla HTML String Rendering -**Chosen**: Pure TypeScript functions returning HTML template strings. Full DOM replacement via `app.innerHTML = html` on each render cycle. -**Rejected**: React, Vue, Svelte, or any virtual DOM framework. -**Why**: The Deadcat UI is a single-page desktop app with a small DOM (~500-2000 nodes). Full re-render completes in <16ms — well within a single frame. The string rendering model eliminates: build toolchain complexity (JSX transpilation, framework-specific plugins), stale closure bugs (a common React pitfall with event handlers referencing old state), component lifecycle management, and the conceptual overhead of reconciliation. The trade-off is losing declarative component composition and fine-grained reactivity — accepted because the UI is small enough that brute-force re-rendering is fast, and the global state object provides a simpler mental model than component-local state. +**Chosen**: React 18 with TypeScript (TSX). Components are React function components; state changes trigger fine-grained virtual DOM reconciliation — only affected components re-render. +**Rejected (previously attempted)**: Pure TypeScript functions returning HTML template strings. Full DOM replacement via `app.innerHTML = html` on each render cycle. +**Why**: The vanilla string rendering approach worked at small scale but created friction as the component surface grew: no JSX type safety, no composable component patterns, all event handlers had to flow through a single root dispatcher, and full re-renders became noticeable as the DOM grew beyond ~2000 nodes. React provides declarative composition, fine-grained updates via hooks, and a standard ecosystem for async state (React Query). The trade-off — build toolchain complexity (Vite + TSX compilation) — is accepted because Vite's HMR and TSX type safety outweigh the setup cost for a codebase this size. See also: Zustand and React Query decisions below. -### Centralized Global State Over Component-Local State +### Zustand Over Manual Global State Object -**Chosen**: Single `state` object with ~280 properties. Event handlers mutate state directly, then call `render()`. -**Rejected**: Component-local state, Redux-style stores, signals/observables. -**Why**: Mirrors the core engine's architecture — the engine exclusively owns its store, and all reads/writes go through a single API. The UI `state` object is the frontend equivalent. With full re-rendering, there's no need for fine-grained subscriptions or memoization — every render reads the full state. The downside is that unrelated state changes trigger full re-renders, but this is acceptable at the current DOM size. If performance becomes an issue, targeted DOM patching can be introduced without changing the state model. +**Chosen**: Zustand store (`src/store/index.ts`) with ~280 properties in flat slices. Components subscribe via `useStore((s) => s.property)`. State is mutated via `useStore.setState({ ... })`. +**Rejected**: (a) Plain global `state` object with manual `render()` calls. (b) Redux-style reducers with dispatched actions. (c) Distributed component-local state. +**Why**: Preserves the original single-source-of-truth philosophy — all UI state lives in one place, readable in one file — while gaining selective re-rendering. With the vanilla approach, any state mutation triggered a full `innerHTML` replacement. With Zustand, only components that subscribe to the changed slice re-render. The flat structure (all properties at the top level, no nested reducers) keeps the mental model simple: `useStore.setState({ tradeSizeSats: 10000 })` is exactly as legible as `state.tradeSizeSats = 10000` was before. Zustand's minimal API (`create`, `useStore`, `setState`) adds no indirection beyond what React itself requires. -### Event Delegation Over Per-Component Listeners +### React Component Event Handlers Over Root Event Delegation -**Chosen**: Single click/input/keydown listeners on the root `#app` element. Actions identified via `data-action` attributes on elements. -**Rejected**: Per-component event listeners attached after render. -**Why**: Full DOM replacement via `innerHTML` destroys all attached listeners. Event delegation sidesteps this entirely — the root listener is attached once and never re-attached. The `data-action` pattern also creates a natural registry of all user interactions, making the action space auditable. The trade-off is that every click traverses the DOM upward to find a `data-action` — negligible for desktop use. +**Chosen**: Standard React `onClick`, `onChange`, `onSubmit` props on components. Mutations encapsulated in React Query hooks (`useTrading`, `useWalletOps`, etc.). +**Rejected**: Single click listener on root `#app` + `data-action` attribute dispatch pattern. +**Why**: The `data-action` pattern was designed to survive `innerHTML` re-renders — the root listener never needed re-attachment. With React, this constraint disappears: React manages listener lifecycle alongside component lifecycle. Per-component handlers are more discoverable (the handler is co-located with the element that triggers it), type-safe (no stringly-typed action names), and testable in isolation. The trade-off is losing the centralized action registry that `data-action` provided — mitigated by React Query's mutation hooks, which serve as the canonical list of all side-effectful operations. + +### TanStack React Query for Server State + +**Chosen**: React Query (`src/queries/`) for all data that originates from the Tauri backend: markets, wallet snapshots, price history, orders, pools. Queries are invalidated via Tauri events (`useTauriEvents`). +**Rejected**: Manual polling loops or Zustand for server state. +**Why**: Server state (markets, wallet data) has fundamentally different lifecycle requirements from UI state (which tab is selected, whether a modal is open). React Query's stale-while-revalidate model, automatic background refetching, and cache invalidation on mutation success eliminate entire categories of manual synchronization code that existed in the vanilla approach (polling intervals, manual `refreshWallet()` calls, etc.). The IPC bridge remains unchanged — `invoke()` calls are wrapped inside React Query's `queryFn`/`mutationFn` rather than called directly from event handlers. --- diff --git a/docs/ux/design.md b/docs/ux/design.md index eea8d379..7d8c24d7 100644 --- a/docs/ux/design.md +++ b/docs/ux/design.md @@ -135,7 +135,7 @@ A user restoring from mnemonic who holds YES/NO tokens but hasn't ingested the p ### Rendering Model -Deadcat Live uses a Tauri v2 desktop shell with a vanilla TypeScript frontend. Components are pure functions returning HTML strings. State mutations trigger a synchronous full re-render via `app.innerHTML`. This is intentional — the DOM is small enough that full re-renders are fast (<16ms), and the simplicity eliminates an entire class of stale-state bugs that plague virtual DOM diffing. +Deadcat Live uses a Tauri v2 desktop shell with a React 18 frontend (TypeScript + TSX). Components are React function components. State changes trigger React's virtual DOM reconciliation — only affected components re-render. The entry point is `src/main.tsx`, which wraps the app in a `QueryClientProvider` and renders ``. ``` ┌─────────────────────────────────────────────────┐ @@ -144,23 +144,36 @@ Deadcat Live uses a Tauri v2 desktop shell with a vanilla TypeScript frontend. C │ │ deadcat-node (Rust) │ │ │ │ ContractEngine + LWK Wallet + Nostr │ │ │ │ ─── IPC boundary (invoke / events) ─── │ │ -│ │ Frontend (TypeScript) │ │ -│ │ state.ts → components/*.ts → render() │ │ +│ │ Frontend (React + TypeScript) │ │ +│ │ Zustand store → components/*.tsx │ │ +│ │ TanStack React Query → invoke() calls │ │ │ └───────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘ ``` ### State Management -A single global `state` object holds all UI state. No stores, no reducers, no subscriptions. Event handlers mutate `state` directly, then call `render()`. This maps naturally to the core engine's own model: the engine exclusively owns its store, and all reads/writes go through the engine API. The UI mirrors this — `state` is the single source of truth, and all reads/writes go through handler functions. +UI state is held in a **Zustand store** (`src/store/index.ts`) organised into logical slices: Navigation, Trading, WalletUi, WalletModals, MarketCreation, Onboarding, ChartUi, WalletBadge, Persistence. Components subscribe to the slices they need via `useStore((s) => s.property)` and update state via `useStore.setState({ ... })`. The store remains flat and large (~280 properties), preserving the original single-source-of-truth philosophy while gaining selective re-rendering — components only re-render when the specific slice they subscribe to changes. -### Event Delegation +**Server state** (markets, wallet snapshots, price history, orders, pools) is managed by **TanStack React Query** (`src/queries/`). Queries are invalidated on Tauri events and after mutations, keeping UI data fresh without manual polling loops. -A single click listener on the root `#app` element handles all interactions via `data-action` attributes. This eliminates listener cleanup on re-render (innerHTML destroys old listeners, but the root listener persists). Input, keydown, and focusout handlers follow the same delegation pattern. +### Component Event Handling + +React components handle interactions directly via `onClick`, `onChange`, and `onSubmit` props. There are no `data-action` attributes and no root-level event dispatcher. Mutations (trade execution, wallet ops, market creation) are encapsulated in React Query mutation hooks (`useTrading`, `useWalletOps`, `useMarketOps`, `useMarketCreation`) which manage loading/error/success states automatically. + +### Lifecycle Hooks + +Three hooks run on app mount in `App.tsx`: + +| Hook | Responsibility | +| --- | --- | +| `useBootstrap()` | Load Nostr identity, fetch relays + profile, check wallet status, dismiss splash | +| `useTauriEvents()` | Subscribe to Tauri backend events; invalidate React Query caches on wallet/market/order updates | +| `useActivityTracking()` | Report user activity to backend every 30s to prevent wallet auto-lock | ### IPC Bridge -All Tauri commands (`invoke`) map 1:1 to core engine operations. The frontend never constructs PSETs, selects coins, or manages UTXOs — it sends intent ("buy 10 YES tokens on market X") and receives results ("txid, fee paid, tokens received"). The IPC boundary is the natural place for the protocol-to-UX translation described in Principle 1. +All Tauri commands (`invoke`) map 1:1 to core engine operations. The frontend never constructs PSETs, selects coins, or manages UTXOs — it sends intent ("buy 10 YES tokens on market X") and receives results ("txid, fee paid, tokens received"). The IPC boundary is the natural place for the protocol-to-UX translation described in Principle 1. Direct `invoke` calls are wrapped inside React Query queries and mutations — components never call `invoke` directly. ## Visual Design Language diff --git a/docs/ux/views.md b/docs/ux/views.md index ba18ec6a..a86fe10c 100644 --- a/docs/ux/views.md +++ b/docs/ux/views.md @@ -2,143 +2,274 @@ View-by-view interaction specs for Deadcat Live. Each view section documents the layout, data sources (mapped to `deadcat-core` types), state transitions, and conditional elements. -The app has four primary views (`ViewMode`): **home**, **detail**, **create**, **wallet** — plus a pre-auth **onboarding** flow. Navigation is state-driven: `state.view` determines which root component renders. +The app has four primary views (`ViewMode`): **home**, **detail**, **create**, **wallet** — plus optional **onboarding** and **profile** overlays. Navigation is state-driven: `useStore((s) => s.view)` determines which root component renders in `App.tsx`. Wallet, onboarding, and send/receive modals are overlaid on top of the current view. + +**Component locations**: + +| View | Component file | +| --- | --- | +| Home | `src/components/home/HomePage.tsx` | +| Detail | `src/components/detail/DetailPage.tsx` | +| Create | `src/components/create/CreateMarketPage.tsx` | +| Wallet overlay | `src/components/wallet/WalletPage.tsx` | +| Onboarding overlay | `src/components/onboarding/OnboardingOverlay.tsx` | +| Profile page | `src/components/profile/ProfilePage.tsx` | +| Shell/header | `src/components/layout/TopShell.tsx` | --- -## Onboarding Flow +## Onboarding Overlay -**Entry condition**: `state.onboardingStep !== null` (no Nostr identity or no wallet). +**Entry condition**: `setupModalOpen === true` in Zustand store. Triggered when the user attempts an action requiring identity or wallet. Never shown on first launch — the app opens directly to home in guest mode. -**Layout**: Centered card on a minimal dark background. Two-step progress bar at top. +**Layout**: `OnboardingOverlay.tsx` renders a fixed backdrop (`z-50`) over the current view, routing to `NostrSetupStep` or `WalletSetupStep` based on `onboardingStep`. -### Step 1: Nostr Identity (`onboardingStep === "nostr"`) +**Step routing**: -| Element | Source | Behavior | +| `onboardingStep` | Component | Shown when | | --- | --- | --- | -| Mode toggle | `onboardingNostrMode: "generate" \| "import"` | Switches between generate and import UI | -| Generated npub | `invoke("init_nostr_identity")` result | Displayed read-only after generation | -| Nsec reveal | `onboardingNsecRevealed` toggle | Hidden by default, revealed on click | -| Import input | `onboardingNostrNsec` | Free text, validated on change | -| Continue button | Gated by: nsec saved acknowledgment (generate) or valid nsec (import) | Advances to Step 2 | +| `"nostr"` | `NostrSetupStep.tsx` | User has no Nostr identity | +| `"wallet"` | `WalletSetupStep.tsx` | User has identity but no wallet | -### Step 2: Wallet (`onboardingStep === "wallet"`) +### Step 1: Nostr Identity (`NostrSetupStep.tsx`) -| Element | Source | Behavior | +| Element | Zustand source | Behavior | +| --- | --- | --- | +| Mode toggle | `onboardingNostrMode: "generate" \| "import"` | Switches between generate and import sub-views | +| Generate flow | — | Calls `invoke("generate_nostr_identity")` → stores in `onboardingPendingPubkey`/`onboardingPendingNpub`. Shows nsec reveal + acknowledgment checkbox | +| Import flow | `onboardingNostrNsec` | Paste field; validates via `invoke("import_nostr_nsec")`; shows derived npub | +| Nsec reveal | `onboardingNsecRevealed` | Amber warning + hidden value; `invoke("export_nostr_nsec")` on reveal | +| Acknowledgment checkbox | `onboardingNsecAcknowledged` | Required for generate flow to proceed | +| Continue button | Gated by mode-specific validation | Commits identity to `nostrPubkey`/`nostrNpub`; advances to wallet step if needed | +| Step indicator | `!onboardingWalletOnly` | Two-circle progress bar shown only for combined identity+wallet flows | + +### Step 2: Wallet (`WalletSetupStep.tsx`) + +| Element | Zustand source | Behavior | | --- | --- | --- | -| Mode selector | `onboardingWalletMode: "create" \| "restore" \| "nostr-restore"` | Three radio options | -| Nostr backup indicator | `check_nostr_backup` result | If backup found, pre-selects "nostr-restore" | -| Password fields | `onboardingWalletPassword`, `onboardingWalletPasswordConfirm` | Required for create/restore | +| Backup scan | `onboardingBackupScanning` | Runs automatically on open via `invoke("check_nostr_backup")`. Blocking spinner | +| Backup found | `onboardingBackupFound` | If true, shows restore-from-Nostr-backup card(s) as primary option | +| Mode selector | `onboardingWalletMode: "create" \| "restore" \| "nostr-restore"` | Options presented after scan completes | +| Password fields | `onboardingWalletPassword`, `onboardingWalletPasswordConfirm` | Min 8 chars. Confirm must match | +| Mnemonic display | Generated 12 words | Shown for create mode. Word verification step before continuing | | Mnemonic input | `onboardingWalletMnemonic` | 12/24 word textarea for restore mode | -| Finish button | Validates all fields, creates/restores wallet | Transitions to main app | +| Wallet-only indicator | `onboardingWalletOnly` | Hides step indicator when wallet modal opened independently by signed-in user | +| Finish button | Validates all fields | Calls `useCreateWallet` / `useRestoreWallet` mutation; on success: closes overlay, user returned to prior context | -**Exit**: `finishOnboarding()` clears onboarding state, fetches wallet status, loads markets, dismisses splash. +**Exit**: On success, `setupModalOpen` is set to `false`. The user sees exactly what they left — same view, same market, same trade inputs — because Zustand preserves `selectedMarketId`, `tradeSizeSats`, `selectedSide`, etc. across the overlay. --- -## Home View (`state.view === "home"`) +## Home View (`view === "home"`) -**Layout**: Top shell (header + category row) + main content area. +**Component**: `src/components/home/HomePage.tsx` +**Data**: `useMarkets()` React Query hook — calls `discover_contracts` (with identity) or `list_contracts` (guest/fallback). Markets are filtered/sorted client-side. -### Header +**Layout**: Top shell (`TopShell.tsx`) + category tab row + main content area. -| Element | Source | Behavior | +### Header (`TopShell.tsx`) + +| Element | Zustand source | Behavior | | --- | --- | --- | -| Logo | Static SVG | Click → `state.view = "home"` | -| Nav tabs | "Markets", "Live", "Social" | Markets is active; Live/Social are placeholders | -| Search input | `state.search` | Filters markets client-side by question text | -| Wallet button | `state.walletData?.balance` | Shows compact L-BTC balance; click → wallet view | -| User menu | `state.userMenuOpen` toggle | Dropdown: npub, currency selector, settings, logout | +| Logo | — | Click → `useStore.setState({ view: "home", selectedMarketId: "" })` | +| Search input | `search` | `SearchBar.tsx` filters markets client-side by question text | +| Portfolio icon | `activeCategory` | Icon button (bar chart SVG); visible only when `nostrPubkey` set; click → `activeCategory = "Portfolio"` | +| Wallet button | `walletStatus`, `walletData?.balance`, `walletNewTxids` | See wallet button states table below | +| User menu | `nostrPubkey` | Profile avatar → `UserMenu.tsx` dropdown; visible only when signed in | -### Category Row +**Wallet button states** (`WalletButton` component): + +| State | Condition | Appearance | Action | +| --- | --- | --- | --- | +| Guest | `nostrPubkey === null` | Filled emerald "Get started" button | Opens setup modal at identity step | +| Identity-only | `nostrPubkey` set, `walletStatus === "not_created"` | Text link "Set up wallet" (emerald) | `walletOpen = true` | +| Locked | `walletStatus === "locked"` | Circular icon button with lock badge | `walletOpen = true` | +| Unlocked | `walletStatus === "unlocked"` | Circular icon button + compact sats balance | `walletOpen = true` | -Horizontal tab bar: Trending, Politics, Sports, Culture, Bitcoin, Weather, Macro, Resolved, My Markets. +The wallet button border animates when transactions are pending or new: +- **Pulsing emerald border**: unconfirmed incoming transaction (`height == null && balanceChange > 0`) +- **Pulsing white border**: unconfirmed outgoing transaction +- **Solid emerald border**: confirmed transaction not yet seen (`walletNewTxids.size > 0`); clears on open + +**Header states (right side)**: + +| User state | Right side | +| --- | --- | +| Guest (no identity) | `[Get started]` filled emerald button | +| Signed in, no wallet | `[Set up wallet]` text link | +| Signed in, wallet locked | Portfolio icon · Wallet icon (with lock) · Avatar | +| Signed in, wallet unlocked | Portfolio icon · Wallet icon + sats balance · Avatar | -| Category | Data source | Filter logic | +**Logout modal** (`LogoutModal` component): Opens via user menu. Two variants: +- *Identity-only* (no wallet): simple confirmation, removes Nostr keys from device. +- *With wallet*: requires downloading a `.dcid` backup file (encrypted with wallet password) before the "Log Out" button enables. User must check "I have downloaded my backup" checkbox to confirm. + +### Category Row (`CategoryBar`) + +Horizontal scrollable pill bar. Active category stored in `activeCategory` (Zustand). Clicking a pill sets `activeCategory` and resets `selectedMarketId`. + +| Category | Visibility | Filter / sort logic | | --- | --- | --- | -| Trending | `getTrendingMarkets()` | Sorted by volume + change, top N | -| Topic categories | `getFilteredMarkets()` | `market.category === activeCategory` | -| Resolved | `list_markets(StateFilter::TerminalOnly)` | Markets in ResolvedYes/ResolvedNo/Expired states | -| My Markets | Wallet UTXOs matched via `identify_asset` | Markets where user holds YES or NO tokens | +| Trending | Always | `getTrendingMarkets()` — sorted by volume + 24h change; two-column layout with featured carousel + sidebar | +| Ending Soon | Always | `getEndingSoonMarkets()` — ascending `expiryHeight - currentHeight` | +| New | Always | `getNewMarkets()` — most recently created | +| Politics, Sports, Culture, Bitcoin, Weather, Macro | Always | `market.category === activeCategory`; category page three-column layout | +| My Markets | Only when `nostrPubkey && marketMakerMode` | Markets where `nostrPubkey === market.oraclePubkey` | +| Portfolio | Not in category bar — header icon button only | Markets where user holds YES/NO token positions | + +### Trending View (`TrendingHomeView`) -### Content Area +Two-column layout (`xl:grid-cols-[1.618fr_1fr]`). -**Trending view** (default): -- **Featured card**: Largest trending market. Full-width hero with embedded SVG price chart, question text, probability, 24h change indicator. -- **Top markets grid**: Next 6 markets in 2-column (desktop) or 1-column (mobile) card grid. Each card: question, probability pill, sparkline, volume badge. -- **Top movers sidebar**: 3 markets with highest absolute 24h change. +**Left column**: +- `FeaturedMarket.tsx`: Featured market carousel cycling through `getTrendingMarkets()` via `trendingIndex`. Prev/next arrows. Shows: category + LIVE badge + state badge, question, YES/NO buy buttons (or resolved/expired badge), volume · time-left · 24h change, description (2-line clamp), full `MarketChart.tsx`. Falls back to `generateMockPriceHistory(market)` when no real pool history exists. +- "Top Markets" grid: `md:grid-cols-2` grid of `MarketCard.tsx` — top 6 from `getFilteredMarkets("Trending")`, respects the `search` input. -**Category view**: Paginated card grid filtered to the selected category. Same card layout as top markets grid. +**Right column** (sidebar): +- **Trending** panel: Top 3 from `getTrendingMarkets()` as `SidebarMarketItem` rows. Each row shows question, Yes%/No% buy buttons, 24h change indicator. +- **Top Movers** panel: Top 3 markets by `|change24h|` as `SidebarMarketItem` rows. -**My Markets view**: Markets grouped by position type (YES holdings, NO holdings). Each shows position size and current estimated value. +**Empty state**: If `trending.length === 0`, renders `EmptyState` (see below). -### Market Card Anatomy +**Loading state**: While `isLoading && markets.length === 0`, renders `MarketLoader` — an animated cat-falls-into-bag SVG with "Fetching markets…" pulsing text. Not a skeleton — the full animation plays on first load only. + +### Market Card Anatomy (`MarketCard.tsx`) ``` ┌─────────────────────────────────────────┐ -│ [Category badge] [24h ±X%] │ +│ [Category] [· LIVE] [time left] │ │ │ │ Will BTC hit $200k by 2027? │ │ │ -│ ┌─────────────────────────────────────┐ │ -│ │ ~~~ sparkline chart ~~~ │ │ -│ └─────────────────────────────────────┘ │ +│ 72% │ +│ │ +│ ──── sparkline (real price history) ─── │ │ │ -│ YES 72% Vol: 0.5 BTC │ +│ [Yes 72%] [No 28%] [↑ +4.2%] │ +│ 0.42 BTC vol · 1,240 traders · 3d left │ └─────────────────────────────────────────┘ ``` -Click anywhere → `openMarket(marketId)` → transitions to detail view. +**Sparkline** (`ChartSparkline` component): Fetches `usePriceHistory(market.marketId)`. Falls back to `generateMockPriceHistory(market)` when no real pool history. Plots `implied_yes_price_bps / 10000` per entry as a polyline (80×24 SVG). Color: emerald when end ≥ start, rose when declining. + +**Resolved / expired state**: YES/NO buy buttons are replaced by a single "Resolved YES" / "Resolved NO" / "Expired" badge. + +**Footer**: `{volume} vol · {traderCount} traders · {timeLeft}`. `traderCount` row is omitted if `market.traderCount === 0`. + +Click card → `openMarket(market)` → `useStore.setState({ view: "detail", selectedMarketId: market.id })`. + +### Category Page View (`CategoryPageView`) + +Rendered for named category tabs (Politics, Sports, Culture, Bitcoin, Weather, Macro). Three-column layout (`xl:grid-cols-[233px_1fr_320px]`). + +**Left sidebar** (desktop only): +Three filter pills that set `categoryFilter` (Zustand): +- All markets (default) +- Live now (`m.isLive === true`) +- Ending soon (`m.isLive && expiryHeight - currentHeight ≤ 200 blocks`) + +Active filter pill: `bg-slate-900/70 text-emerald-300`. + +**Center column**: +- Heading + sort buttons: "Trending" (sort by `volumeBtc` desc) and "Frequency" (sort by `traderCount` desc). Active sort button: `border-slate-500 text-slate-100`. Sort stored in `categorySortMode` (Zustand). +- Stats row: Contracts (count after filter applied), Live now, 24h volume (sum of all category markets regardless of filter). +- `md:grid-cols-2` grid of `MarketCard.tsx` — filtered by `categoryFilter`, sorted by `categorySortMode`. + +**Right sidebar**: +- **Live contracts**: Up to 4 live markets; each shows question, Yes% price, volume. +- **Highest liquidity**: Top 4 by `liquidityBtc`; ranked list with liquidity value. +- **Market states**: Breakdown by state using user-friendly labels: Dormant, Live (emerald), Resolved YES, Resolved NO, Expired (amber). + +### Portfolio View (`PortfolioView`) + +Shown when `activeCategory === "Portfolio"` (requires `nostrPubkey`). Only accessible via the header Portfolio icon — not in the category bar. + +**Stats row** (3 cards): Portfolio Value (summed position values in L-BTC), Active Positions (count), Settled (count of resolved/expired positions held). + +**Active Positions section**: Cards for each market where user holds YES or NO tokens. Each card shows: +- Question (click → opens market detail) +- Yes% price +- YES token quantity / NO token quantity +- Total value in sats (`tokenCount × pricePerToken`) + +**Settled Positions section**: Cards for resolved/expired markets. Shows Won/Lost badge, YES/NO token quantities. "Won" when the market resolved in the direction the user holds. + +**Empty state**: If no positions found, shows descriptive message and "Browse Markets" button. + +### My Markets View (`MyMarketsView`) + +Shown when `activeCategory === "My Markets"`. Only visible in the category bar when `nostrPubkey && marketMakerMode`. Displays markets where `nostrPubkey === market.oraclePubkey`. + +**Stats row** (3 cards): Total markets, Active markets, Awaiting resolution (active markets past expiry). + +**State sections** (each section only rendered if non-empty): +- "Dormant — needs initial issuance" (`state === 0`) +- "Active" (`state === 1`) +- "Expired" (`state === 4`) +- "Resolved" (`state === 2 || 3`) + +Each market card shows: category, state badge (DORMANT / UNRESOLVED / RESOLVED YES / RESOLVED NO / EXPIRED), question, Yes%/No% prices. + +**Empty state**: "No markets created yet" with optional "+ Create Market" button when `marketMakerMode`. + +**Header action**: "+ Create New Market" button appears at top-right when `marketMakerMode`. + +### Empty State (`EmptyState`) + +Shown when `trending.length === 0` on the Trending view. Icon + "No markets yet" heading + description. Conditional CTAs: +- If no identity: "Set Up Account" → opens setup modal at identity step. +- If `marketMakerMode`: "Create Market" → `view = "create"`. --- -## Detail View (`state.view === "detail"`) +## Detail View (`view === "detail"`) -**Layout**: Two-column on desktop (chart + info left, trade composer right). Single column on mobile (chart → info → trade). +**Component**: `src/components/detail/DetailPage.tsx` +**Data**: Market from `useMarkets()` filtered by `selectedMarketId`. Price history from `usePriceHistory(marketId)`. Orders from `useMarketOrders(marketId)`. -### Left Column: Market Info + Chart +**Layout**: Two-column on desktop (chart + info left, trading panel right). Single column on mobile. + +### Left Column: Market Info + Chart (`MarketHeader.tsx`, `MarketChart.tsx`) | Element | Source | Behavior | | --- | --- | --- | -| Question | `market.question` | Large heading text | -| State badge | `MarketState` variant mapping | Green "Live", emerald "Resolved YES", rose "Resolved NO", amber "Expired" | +| Question | `market.question` | Large heading | +| State badge | `market.state` (`MarketState` variant) | "Live" (green), "Resolved YES" (emerald), "Resolved NO" (rose), "Expired" (amber) | | Description | `market.description` | Collapsible paragraph | -| Resolution source | `market.resolutionSource` | Link or text describing how outcome is determined | +| Resolution source | `market.resolutionSource` | Link or text | | Settlement date | `market.expiryHeight` → estimated date | "Settles ~June 15, 2027 (block 2,150,400)" | | Contract size | `market.cptSats` | "5,000 sats per contract" | -| Price chart | `PriceHistoryEntry[]` from `getPriceHistory` | SVG chart with hover tooltip | -| Orderbook | `getFullOrderbook(market)` | Toggleable bid/ask visualization | - -### Price Chart Spec +| Price chart | `PriceHistoryEntry[]` from `usePriceHistory()` | `MarketChart.tsx` SVG with hover, timescale buttons | +| Orderbook | `useMarketOrders()` | `OrderbookPanel.tsx` — toggleable bid/ask levels | -- **Axes**: Y = YES probability (0-100%), X = block height -- **Data**: Each `PriceHistoryEntry` maps to a point: `(block_height, implied_yes_price_bps / 100)` -- **Rendering**: SVG polyline with gradient fill below the line (emerald). Cat-paw SVG markers at data points. -- **Live indicator**: Pulsing dot at the latest data point with current probability label. -- **Hover**: Vertical crosshair line. Tooltip shows exact probability and block height. Tooltip tracks mouse via `chartHoverX` state. -- **Timescale**: Buttons for 10B/25B/50B/100B (blocks). Filters visible data range. -- **Aspect ratio**: Dynamically measured from container via `syncChartAspectFromLayout()`. +### Price Chart Spec (`MarketChart.tsx`) -### Right Column: Trade Composer +- **Axes**: Y = YES/NO probability (0–100%), X = block height +- **Series**: Two polylines — YES (emerald) and NO (rose) — with gradient fill +- **Markers**: Cat-paw SVG silhouettes along each series; cat-logo markers at endpoints +- **Live indicator**: Pulsing animation at the latest data point +- **Hover**: Vertical crosshair + tooltip (probability + block height). Tracked via `chartHoverMarketId` + `chartHoverX` in Zustand +- **Timescale**: 10B / 25B / 50B / 100B buttons. Updates `chartTimescale` in store +- **Aspect ratio**: Measured from container via `ResizeObserver` → `chartAspectDetail` in store -**Tab bar**: Trade | Issue | Redeem | Cancel +### Right Column: Trading Panel (`TradingPanel.tsx`) -Tab visibility is conditional on market state and user role: +**Action tabs** (conditional on market state and user role): -| Tab | Visible when | Maps to | +| Tab | Visible when | Core operation | | --- | --- | --- | -| Trade | Always (state is `Trading`) | `quote_trade` + `build_trade_pset` | -| Issue | Market maker mode + market is `Trading` | `build_issuance_pset` | -| Redeem | Market is `ResolvedYes`/`ResolvedNo`/`Expired` AND user holds tokens | `build_redemption_pset` | -| Cancel | Market maker mode + market is `Trading` + user holds both YES and NO tokens | `build_cancellation_pset` | +| Trade | Market is `Trading` | `useQuoteTrade` + `useExecuteTrade` | +| Issue | `marketMakerMode` + `Trading` | `useIssueTokens` | +| Redeem | `ResolvedYes`/`ResolvedNo`/`Expired` + user holds tokens | `useRedeemTokens` | +| Cancel | `marketMakerMode` + `Trading` + user holds YES+NO tokens | `useCancelTokens` | -### Trade Tab +**Trade tab layout**: ``` ┌─────────────────────────────────────┐ │ [YES ██████] [NO ░░░░░░] │ ← Side selector │ │ -│ [Market ▼] [Limit] │ ← Order type +│ [Market ▼] [Limit] │ ← Order type (marketMakerMode) │ │ │ [Open ○] [Close ○] │ ← Trade intent │ │ @@ -146,20 +277,18 @@ Tab visibility is conditional on market state and user role: │ │ │ ┌─────────────────────────────────┐│ │ │ Price: 72% ││ -│ │ You pay: 10,000 sats ││ ← Live quote +│ │ You pay: 10,000 sats ││ ← Quote result │ │ You get: ~13.8 contracts ││ │ │ Est. fee: ~250 sats ││ │ └─────────────────────────────────┘│ │ │ -│ [ Buy YES ] │ ← Action button (emerald) +│ [ Buy YES ] │ ← Opens QuoteModal └─────────────────────────────────────┘ ``` -**State flow**: Input change → debounce 300ms → `quote_trade` → display quote → user clicks Buy → confirm modal → `build_trade_pset` → sign → broadcast → pending toast → `step` confirms → success toast. +**State flow**: Amount input → `useQuoteTrade` mutation → quote stored in `tradeQuoteSnapshot` → "Buy YES" → `QuoteModal` opens → user confirms → `useExecuteTrade` mutation → `invoke("build_trade_pset")` → sign → broadcast → success toast + query invalidation. -### Quote Confirm Modal - -Triggered by the Buy/Sell button. Full-screen overlay with: +### Quote Confirm Modal (`QuoteModal.tsx`) | Field | Source | | --- | --- | @@ -168,13 +297,14 @@ Triggered by the Buy/Sell button. Full-screen overlay with: | Tokens | `TradeQuote.total_output` (formatted) | | Price | `TradeQuote.effective_price` as probability % | | Fee | `TradeQuote.estimated_fee` (with "estimate" disclaimer) | -| Route legs | `TradeQuote.legs` — shown only in advanced mode | -| Confirm button | Triggers `build_trade_pset` + sign + broadcast | -| Cancel button | Dismisses modal | +| Timer | 30-second countdown on quote validity. On expiry: auto-re-quote + "Price updated" flash | +| Route legs | `TradeQuote.legs` — advanced mode toggle | +| Confirm button | Triggers `useExecuteTrade` mutation | +| Cancel button | Closes modal, clears `tradeQuoteSnapshot` | ### Oracle Resolution Panel -**Visible only when**: `state.nostrPubkey === market.oraclePubkey` AND market state is `Trading`. +**Visible only when**: `nostrPubkey === market.oraclePubkey` AND market state is `Trading`. ``` ┌─────────────────────────────────────┐ @@ -185,45 +315,69 @@ Triggered by the Buy/Sell button. Full-screen overlay with: └─────────────────────────────────────┘ ``` +Calls `useMarketOps` mutation (oracle attest + execute resolution). + --- -## Create View (`state.view === "create"`) +## Create View (`view === "create"`) -**Entry**: "Create Market" button (visible in market maker mode). +**Component**: `src/components/create/CreateMarketPage.tsx` +**Entry**: "Create Market" button in header (visible when `marketMakerMode`). Requires identity + wallet. -**Layout**: Centered form card. +**Layout**: Centered form card with sub-components `CategoryDropdown.tsx` and `SettlementPicker.tsx`. | Field | Input type | Validation | Maps to | | --- | --- | --- | --- | | Question | Text input (required) | Min 10 chars | Nostr event content | | Description | Textarea | Optional | Nostr event content | -| Category | Dropdown | Required, from `MarketCategory` | Nostr event tag | +| Category | `CategoryDropdown` | Required | Nostr event tag | | Resolution source | Text input | Optional | Nostr event content | | Collateral per pair | Constrained dropdown (1-2-5 table) | Must be valid denomination | `MarketCreationParams.collateral_per_pair` | -| Settlement date | Calendar + time picker | Must be in the future | `MarketCreationParams.expiry_time` (snapped to 60-block boundary) | +| Settlement date | `SettlementPicker` (calendar + time) | Must be in the future | `MarketCreationParams.expiry_time` (snapped to 60-block boundary) | -**Submit flow**: Validate → `build_creation_pset(params, funding)` → sign `UnblindedPset` → broadcast → await confirmation → `ingest_market` → publish Nostr event → redirect to detail view. +**Submit flow**: Validate → `useCreateMarket` mutation → `invoke("build_creation_pset")` → sign → broadcast → `ingest_market` → publish Nostr event → navigate to detail view. --- -## Wallet View (`state.view === "wallet"`) +## Wallet Overlay (`walletOpen === true`) -**Layout**: Routed by `state.walletStatus`: +**Component**: `src/components/wallet/WalletPage.tsx` +**Layout**: Modal overlay. Routes by `walletStatus`: | Status | Component | Content | | --- | --- | --- | -| `not_created` | Setup | Password + mnemonic creation form | -| `locked` | Locked | Lock icon + password input | -| `unlocked` | Unlocked | Full wallet dashboard | +| `not_created` | `WalletSetup.tsx` | Create or restore wallet (same UI as onboarding step 2, but standalone) | +| `locked` | `WalletLocked.tsx` | Lock icon + password input → `useUnlockWallet` mutation | +| `unlocked` | `WalletUnlocked.tsx` | Full wallet dashboard | + +### Unlocked Wallet Dashboard (`WalletUnlocked.tsx`) + +| Section | Data source | Content | +| --- | --- | --- | +| Balance header | `useWalletSnapshot()` → `walletData.balance[policyAssetId]` | L-BTC amount + unit toggle (sats/BTC) + hide toggle | +| Token balances | `walletData.utxos` + `identify_asset` | YES/NO token positions grouped by market | +| Send / Receive | — | Buttons open `SendModal.tsx` / `ReceiveModal.tsx` | +| Activity list | `ActivityList.tsx` | `walletData.transactions` + `recentTxLabels` — chronological with deadcat labels + pagination | +| UTXO list | `UtxoList.tsx` (toggleable) | Raw UTXO list for advanced users | +| Pending swaps | `walletData` Boltz swap entries | Status indicators for in-progress Lightning/Bitcoin swaps | -### Unlocked Wallet Dashboard +### Send Modal (`SendModal.tsx`) — Three tabs -| Section | Source | Content | +| Tab | Operation | Flow | | --- | --- | --- | -| Balance header | `walletData.balance[policyAssetId]` | L-BTC balance + fiat equivalent + hide toggle | -| Action buttons | — | "Receive" and "Send" buttons opening modals | -| Activity list | `walletData.transactions` + `recentTxLabels` | Chronological tx list with deadcat labels | -| UTXO explorer | `walletData.utxos` (toggleable) | Raw UTXO list for advanced users | +| Lightning | Pay LN invoice via Boltz submarine swap | Paste invoice → decode amount → confirm → `invoke("pay_lightning_invoice")` | +| Liquid | Direct L-BTC transfer | Enter address + amount → confirm → `invoke("send_liquid")` | +| Bitcoin | On-chain BTC via Boltz chain swap | Enter address + amount → confirm → `invoke("create_bitcoin_send")` | + +### Receive Modal (`ReceiveModal.tsx`) — Three tabs + +| Tab | Operation | Flow | +| --- | --- | --- | +| Lightning | Generate LN invoice via Boltz reverse swap | Enter amount → `invoke("create_lightning_receive")` → show QR + invoice | +| Liquid | L-BTC address | `invoke("generate_liquid_address")` → show QR + address | +| Bitcoin | On-chain BTC via Boltz chain swap | Enter amount → `invoke("create_bitcoin_receive")` → show QR + BTC address | + +--- ### State Mapping: Core Types → UI States diff --git a/src/components/chart/MarketChart.tsx b/src/components/chart/MarketChart.tsx index ea874b0c..a630d074 100644 --- a/src/components/chart/MarketChart.tsx +++ b/src/components/chart/MarketChart.tsx @@ -1,8 +1,9 @@ import type React from "react"; -import { useCallback, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useStore } from "../../store"; -import type { ChartTimescale, Market, PriceHistoryEntry } from "../../types"; +import type { Market, PriceHistoryEntry } from "../../types"; import { + blockHeightToHoverLabel, buildChartFromHistory, buildChartSeriesData, lastDefinedChartProbability, @@ -229,13 +230,32 @@ export default function MarketChart({ const chartTimescale = useStore((s) => s.chartTimescale); const chartHoverMarketId = useStore((s) => s.chartHoverMarketId); const chartHoverX = useStore((s) => s.chartHoverX); - const chartAspect = useStore((s) => + const fallbackAspect = useStore((s) => mode === "home" ? s.chartAspectHome : s.chartAspectDetail, ); const hoverRef = useRef(null); + const svgContainerRef = useRef(null); const isHomeChart = mode === "home"; + // Measure the actual SVG container so the viewBox always matches the pixel + // ratio exactly — prevents non-uniform stretching with preserveAspectRatio="none". + const [measuredAspect, setMeasuredAspect] = useState(null); + useEffect(() => { + const el = svgContainerRef.current; + if (!el) return; + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + const { width, height } = entry.contentRect; + if (height > 0) setMeasuredAspect(width / height); + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const chartAspect = measuredAspect ?? fallbackAspect; + const hasHistory = priceHistory.length > 0; const hasPrice = market.yesPrice != null || hasHistory; const fallbackYes = market.yesPrice ?? 0.5; @@ -419,7 +439,10 @@ export default function MarketChart({ // ── Hover time box ──────────────────────────────────────────────── const hoverTimeX = Math.max(plotLeft + 18, Math.min(plotRight - 18, hoverX)); - const hoverTimeText = `Block ${hoverBlockHeight.toLocaleString()}`; + const hoverTimeText = blockHeightToHoverLabel( + hoverBlockHeight, + market.currentHeight || hoverBlockHeight, + ); const hoverTimeFontSize = isHomeChart ? 7.8 : 8.4; const hoverTimeStrokeWidth = isHomeChart ? 0.16 : 0.2; const hoverTimeBoxHeight = 15.8; @@ -503,7 +526,10 @@ export default function MarketChart({
{/* SVG chart */} -
+
- {xLabels.map((label) => ( - {label} + {xLabels.map((label, i) => ( + {label} ))}
@@ -750,29 +776,18 @@ export default function MarketChart({ {volumeLabel}
- {( - [ - ["10B", "10m"], - ["25B", "25m"], - ["50B", "50m"], - ["100B", "100m"], - ] as const - ).map(([key, label]) => ( + {(["1h", "4h", "1d", "3d", "7d"] as const).map((key) => ( ))}
diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index c7f1cd56..63333b1d 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -708,11 +708,30 @@ function CategoryPageView({ search: string; nostrPubkey: string | null; }) { + const categorySortMode = useStore((s) => s.categorySortMode); + const categoryFilter = useStore((s) => s.categoryFilter); + const categoryMarkets = useMemo( () => getFilteredMarkets(markets, search, category, nostrPubkey), [markets, search, category, nostrPubkey], ); + const filteredMarkets = useMemo(() => { + if (categoryFilter === "live") + return categoryMarkets.filter((m) => m.isLive); + if (categoryFilter === "ending-soon") + return categoryMarkets.filter( + (m) => m.isLive && m.expiryHeight - m.currentHeight <= 200, + ); + return categoryMarkets; + }, [categoryMarkets, categoryFilter]); + + const sortedMarkets = useMemo(() => { + if (categorySortMode === "frequency") + return [...filteredMarkets].sort((a, b) => b.traderCount - a.traderCount); + return [...filteredMarkets].sort((a, b) => b.volumeBtc - a.volumeBtc); + }, [filteredMarkets, categorySortMode]); + const liveContracts = useMemo( () => categoryMarkets.filter((m) => m.isLive).slice(0, 4), [categoryMarkets], @@ -738,30 +757,31 @@ function CategoryPageView({ [categoryMarkets], ); + const sidebarFilters = [ + { key: "all", label: "All markets" }, + { key: "live", label: "Live now" }, + { key: "ending-soon", label: "Ending soon" }, + ] as const; + return (
{/* Left sidebar */} @@ -775,25 +795,23 @@ function CategoryPageView({ {category}
- - + {(["trending", "frequency"] as const).map((mode) => ( + + ))}

Contracts

- {categoryMarkets.length} + {sortedMarkets.length}

@@ -815,7 +833,7 @@ function CategoryPageView({
- {categoryMarkets.map((market, idx) => ( + {sortedMarkets.map((market, idx) => ( ))}
@@ -880,27 +898,27 @@ function CategoryPageView({

- State mix + Market states

- State 0 · Uninitialized + Dormant {stateMix[0]}

- State 1 · Unresolved + Live {stateMix[1]}

- State 2 · Resolved YES + Resolved YES {stateMix[2]}

- State 3 · Resolved NO + Resolved NO {stateMix[3]}

- State 4 · Expired + Expired {stateMix[4]}

diff --git a/src/components/home/MarketCard.tsx b/src/components/home/MarketCard.tsx index d5bc88ee..9b670640 100644 --- a/src/components/home/MarketCard.tsx +++ b/src/components/home/MarketCard.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { usePriceHistory } from "../../queries/usePools"; import { useStore } from "../../store"; import type { Market, Side, TradeIntent } from "../../types"; import { @@ -12,6 +13,7 @@ import { getPositionContracts, setLimitPriceSats, } from "../../utils-react/market"; +import { generateMockPriceHistory } from "../../utils-react/mock-price-history"; // ── Inline SVG icons ───────────────────────────────────────────────── function TrendUpIcon() { @@ -117,17 +119,39 @@ function openMarket( }); } -// ── Chart sparkline placeholder ────────────────────────────────────── -function ChartSparkline() { +// ── Chart sparkline ────────────────────────────────────────────────── +function ChartSparkline({ market }: { market: Market }) { + const { data: rawHistory } = usePriceHistory(market.marketId); + const history = + rawHistory && rawHistory.length > 0 + ? rawHistory + : generateMockPriceHistory(market); + + if (history.length < 2) return
; + + const W = 80; + const H = 24; + const prices = history.map((e) => e.implied_yes_price_bps / 10000); + const points = prices + .map((p, i) => { + const x = (i / (prices.length - 1)) * W; + const y = H - p * H; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(" "); + + const isUp = prices[prices.length - 1] >= prices[0]; + const colorClass = isUp ? "text-emerald-400/60" : "text-rose-400/60"; + return (
+ +
{resolvedBadge ?? ( <> diff --git a/src/store/index.ts b/src/store/index.ts index 8222c46e..1eadf3ad 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -43,6 +43,8 @@ export interface NavigationSlice { view: ViewMode; previousView: ViewMode | null; activeCategory: NavCategory; + categorySortMode: "trending" | "frequency"; + categoryFilter: "all" | "live" | "ending-soon"; search: string; trendingIndex: number; userMenuOpen: boolean; @@ -297,6 +299,8 @@ export const useStore = create()(() => ({ view: "home", previousView: null, activeCategory: "Trending", + categorySortMode: "trending", + categoryFilter: "all", search: "", trendingIndex: 0, userMenuOpen: false, @@ -497,7 +501,7 @@ export const useStore = create()(() => ({ // ── Chart UI ──────────────────────────────────────────────────── chartHoverMarketId: null, chartHoverX: null, - chartTimescale: "25B", + chartTimescale: "1h", chartAspectHome: 2.8, chartAspectDetail: 3.35, diff --git a/src/types.ts b/src/types.ts index eb5daf69..c44a6d95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -508,7 +508,7 @@ export type PriceHistoryEntry = { block_height: number; }; -export type ChartTimescale = "10B" | "25B" | "50B" | "100B"; +export type ChartTimescale = "1h" | "4h" | "1d" | "3d" | "7d"; export type CreateLmsrPoolResponse = { txid: string; diff --git a/src/utils-react/chart-series.ts b/src/utils-react/chart-series.ts index 6b83b64e..fcbdb670 100644 --- a/src/utils-react/chart-series.ts +++ b/src/utils-react/chart-series.ts @@ -15,18 +15,21 @@ export type ChartSeriesData = { yesSeries: Array; }; +// Liquid: ~1 block per minute const SCALE_BLOCKS_BY_KEY: Record = { - "10B": 10, - "25B": 25, - "50B": 50, - "100B": 100, + "1h": 60, + "4h": 240, + "1d": 1440, + "3d": 4320, + "7d": 10080, }; const POINT_COUNT_BY_KEY: Record = { - "10B": 20, - "25B": 28, - "50B": 40, - "100B": 56, + "1h": 60, + "4h": 80, + "1d": 96, + "3d": 108, + "7d": 120, }; function clampProbability(value: number): number { @@ -43,22 +46,64 @@ function scaleConfig(timescale: ChartTimescale): { }; } -/** Format a block height as a relative time label (e.g. "12:30", "Apr 3"). */ -function blockHeightToTimeLabel( +/** Approximate wall-clock date for a given block height. */ +function blockHeightToDate(blockHeight: number, currentHeight: number): Date { + const blocksAgo = currentHeight - blockHeight; + // ~1 block per minute on Liquid + return new Date(Date.now() - blocksAgo * 60_000); +} + +/** Format a block height as a short x-axis label (e.g. "12:30", "Apr 3"). */ + +/** + * Format a block height as a precise hover tooltip timestamp. + * Always includes time; adds date when not today. + */ +export function blockHeightToHoverLabel( blockHeight: number, currentHeight: number, ): string { - const blocksAgo = currentHeight - blockHeight; - // ~1 block per minute on Liquid - const date = new Date(Date.now() - blocksAgo * 60_000); + const date = blockHeightToDate(blockHeight, currentHeight); const now = new Date(); const sameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate(); - if (sameDay) { - return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + const timeStr = date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + if (sameDay) return timeStr; + const dateStr = date.toLocaleDateString([], { + month: "short", + day: "numeric", + }); + return `${dateStr} ${timeStr}`; +} + +function blockHeightToAxisLabel( + blockHeight: number, + currentHeight: number, + timescale: ChartTimescale, +): string { + const date = blockHeightToDate(blockHeight, currentHeight); + const timeStr = date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + // Intraday views: time only + if (timescale === "1h" || timescale === "4h") return timeStr; + // 1d: time only (all labels fall within ~1 day, same or adjacent day) + if (timescale === "1d") { + const now = new Date(); + const sameDay = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + if (sameDay) return timeStr; + return date.toLocaleDateString([], { month: "short", day: "numeric" }); } + // 3d, 7d: date only — multi-day views don't need time precision return date.toLocaleDateString([], { month: "short", day: "numeric" }); } @@ -66,12 +111,12 @@ function buildXAxisLabels( startBlockHeight: number, scaleBlocks: number, currentHeight: number, + timescale: ChartTimescale, ): string[] { - const fractions = - scaleBlocks >= 100 ? [0, 0.25, 0.5, 0.75, 1] : [0, 1 / 3, 2 / 3, 1]; + const fractions = [0, 0.25, 0.5, 0.75, 1]; return fractions.map((fraction) => { const height = Math.round(startBlockHeight + fraction * scaleBlocks); - return blockHeightToTimeLabel(height, currentHeight); + return blockHeightToAxisLabel(height, currentHeight, timescale); }); } @@ -80,7 +125,10 @@ function sampleHistoryProbabilityAtHeight( sampleHeight: number, ): number | null { if (historyPoints.length === 0) return null; - if (sampleHeight < historyPoints[0].blockHeight) return null; + // Clamp to first known value instead of returning null — this fills the + // left portion of the chart when the first history point is mid-window. + if (sampleHeight <= historyPoints[0].blockHeight) + return historyPoints[0].probability; let leftPoint = historyPoints[0]; for (let idx = 1; idx < historyPoints.length; idx += 1) { @@ -140,6 +188,7 @@ export function buildChartSeriesData( startBlockHeight, scaleBlocks, market.currentHeight || endBlockHeight, + timescale, ); return { @@ -171,18 +220,32 @@ export function buildChartFromHistory( startBlockHeight, scaleBlocks, market.currentHeight || endBlockHeight, + timescale, ); - const historyPoints = history + // Sort all entries first so we can find the last point before the window. + const allSorted = history .map((entry) => ({ blockHeight: entry.block_height, probability: clampProbability(entry.implied_yes_price_bps / 10_000), })) - .filter( - (point) => - point.blockHeight >= startBlockHeight && - point.blockHeight <= endBlockHeight, - ) .sort((a, b) => a.blockHeight - b.blockHeight); + + // Include one point before startBlockHeight so left-edge interpolation works: + // without it, any sample between startBlockHeight and the first in-window + // point would clamp to that point rather than interpolating from before it. + const beforeWindow = allSorted.filter( + (p) => p.blockHeight < startBlockHeight, + ); + const lastBeforeWindow = + beforeWindow.length > 0 ? beforeWindow[beforeWindow.length - 1] : undefined; + + const inWindow = allSorted.filter( + (p) => p.blockHeight >= startBlockHeight && p.blockHeight <= endBlockHeight, + ); + + const historyPoints = lastBeforeWindow + ? [lastBeforeWindow, ...inWindow] + : inWindow; const yesSeries: Array = []; const fallbackYes = market.yesPrice ?? 0.5; diff --git a/src/utils-react/mock-price-history.ts b/src/utils-react/mock-price-history.ts index 72b87b00..4c55c211 100644 --- a/src/utils-react/mock-price-history.ts +++ b/src/utils-react/mock-price-history.ts @@ -9,41 +9,195 @@ function hashCode(s: string): number { return Math.abs(h); } +/** Seeded pseudo-random float in [0, 1). */ +function seededRand(seed: number): number { + const x = Math.sin(seed + 1) * 10000; + return x - Math.floor(x); +} + +/** Clamp a BPS value to valid range [100, 9900]. */ +function clampBps(v: number): number { + return Math.max(100, Math.min(9900, Math.round(v))); +} + +type ChartShape = + | "trending-up" + | "trending-down" + | "volatile" + | "v-shape" + | "inverted-v" + | "late-spike" + | "early-peak" + | "s-curve"; + +const SHAPES: ChartShape[] = [ + "trending-up", + "trending-down", + "volatile", + "v-shape", + "inverted-v", + "late-spike", + "early-peak", + "s-curve", +]; + +/** + * Compute the "raw" price path for a given shape, returning normalised + * values in [0, 1] at each of `pointCount` steps, ending at `endNorm`. + */ +function shapePath( + shape: ChartShape, + endNorm: number, + seed: number, + pointCount: number, +): number[] { + const noise = (i: number, amp: number) => + (seededRand(seed + i * 7) - 0.5) * 2 * amp; + + switch (shape) { + case "trending-up": { + // Starts 30–50% below end; smooth upward trend. + const startNorm = Math.max(0.05, endNorm - 0.3 - seededRand(seed) * 0.2); + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + return startNorm + (endNorm - startNorm) * t + noise(i, 0.025); + }); + } + + case "trending-down": { + // Starts 30–50% above end; smooth downward trend. + const startNorm = Math.min(0.95, endNorm + 0.3 + seededRand(seed) * 0.2); + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + return startNorm + (endNorm - startNorm) * t + noise(i, 0.025); + }); + } + + case "volatile": { + // High-noise random walk ending at endNorm. + const startNorm = 0.3 + seededRand(seed) * 0.4; + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + return startNorm + (endNorm - startNorm) * t + noise(i, 0.1); + }); + } + + case "v-shape": { + // Drops to a trough at 40% of series, then recovers to endNorm. + const troughNorm = Math.max( + 0.05, + endNorm - 0.35 - seededRand(seed) * 0.15, + ); + const startNorm = endNorm + (seededRand(seed + 1) - 0.5) * 0.1; + const troughAt = 0.4; + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + const base = + t < troughAt + ? startNorm + (troughNorm - startNorm) * (t / troughAt) + : troughNorm + + (endNorm - troughNorm) * ((t - troughAt) / (1 - troughAt)); + return base + noise(i, 0.02); + }); + } + + case "inverted-v": { + // Rises to a peak at ~50%, then falls to endNorm. + const peakNorm = Math.min(0.95, endNorm + 0.3 + seededRand(seed) * 0.15); + const startNorm = endNorm + (seededRand(seed + 1) - 0.5) * 0.1; + const peakAt = 0.45 + seededRand(seed + 2) * 0.1; + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + const base = + t < peakAt + ? startNorm + (peakNorm - startNorm) * (t / peakAt) + : peakNorm + (endNorm - peakNorm) * ((t - peakAt) / (1 - peakAt)); + return base + noise(i, 0.02); + }); + } + + case "late-spike": { + // Flat for 65% of the series, then rapid move to endNorm. + const flatNorm = + endNorm - (endNorm > 0.5 ? 0.25 : -0.25) - seededRand(seed) * 0.1; + const spikeStart = 0.65; + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + const base = + t < spikeStart + ? flatNorm + : flatNorm + + (endNorm - flatNorm) * ((t - spikeStart) / (1 - spikeStart)); + return base + noise(i, t < spikeStart ? 0.015 : 0.03); + }); + } + + case "early-peak": { + // Fast move in first 30%, then gradual drift to endNorm. + const peakNorm = + endNorm + (endNorm < 0.5 ? 0.3 : -0.3) + seededRand(seed) * 0.1; + const peakAt = 0.25 + seededRand(seed + 1) * 0.1; + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + const base = + t < peakAt + ? endNorm + (peakNorm - endNorm) * (1 - t / peakAt) + : peakNorm + (endNorm - peakNorm) * ((t - peakAt) / (1 - peakAt)); + return base + noise(i, 0.02); + }); + } + + case "s-curve": { + // Slow start, fast middle, slow end (logistic-like). + const startNorm = Math.max(0.05, endNorm - 0.4 - seededRand(seed) * 0.2); + return Array.from({ length: pointCount }, (_, i) => { + const t = i / (pointCount - 1); + // Logistic S-curve mapped to [0,1] + const s = 1 / (1 + Math.exp(-10 * (t - 0.5))); + return startNorm + (endNorm - startNorm) * s + noise(i, 0.02); + }); + } + } +} + /** * Generate a deterministic mock price history for a market. - * Creates a random-walk series ending at the market's current yesPrice, - * spanning the block range leading up to currentHeight. + * + * Each market gets a consistent chart shape (one of 8 patterns) derived + * from its market ID hash. The series always ends at the market's current + * `yesPrice`, so the sparkline and featured chart are visually coherent + * with the displayed probability. */ export function generateMockPriceHistory( market: Market, - pointCount = 40, + pointCount = 200, ): PriceHistoryEntry[] { - if (market.yesPrice == null) return []; - - const endBps = Math.round(market.yesPrice * 10000); const seed = hashCode(market.marketId); - const startOffset = (seed % 2000) - 1000; - const startBps = Math.max(200, Math.min(9800, endBps + startOffset)); + const shape = SHAPES[seed % SHAPES.length]; + // Default to 50% when no real price is known — chart shows "unresolved" state + const endNorm = market.yesPrice ?? 0.5; + + const rawPath = shapePath(shape, endNorm, seed, pointCount); - const rangeBlocks = Math.min( - 100, - Math.max(10, market.currentHeight - (market.expiryHeight - 200)), + // Always cover exactly the last 7 days (10,080 blocks) relative to + // currentHeight, so every timescale window (1h–7d) finds points to plot. + // Spanning from block 0 to currentHeight would spread 200 points across + // millions of blocks, leaving no points inside a 240-block (4h) window. + const rangeBlocks = 10080; + const startBlock = Math.max( + 0, + (market.currentHeight || rangeBlocks) - rangeBlocks, ); - const startBlock = market.currentHeight - rangeBlocks; - const entries: PriceHistoryEntry[] = []; - - for (let i = 0; i < pointCount; i++) { - const t = i / (pointCount - 1); - const base = startBps + (endBps - startBps) * t; - const noise = - ((hashCode(`${market.marketId}-${i}`) % 600) - 300) * (1 - t * 0.7); - const bps = Math.max(100, Math.min(9900, Math.round(base + noise))); - const blockHeight = Math.round(startBlock + rangeBlocks * t); - - entries.push({ + + return rawPath.map((norm, i) => { + const bps = clampBps(norm * 10000); + const blockHeight = Math.round( + startBlock + (i / (pointCount - 1)) * rangeBlocks, + ); + return { pool_id: `mock-pool-${market.marketId}`, market_id: market.marketId, - transition_txid: `mock-tx-${i}`, + transition_txid: `mock-tx-${market.marketId}-${i}`, old_s_index: i, new_s_index: i + 1, reserve_yes: 10000, @@ -51,8 +205,6 @@ export function generateMockPriceHistory( reserve_collateral: 50000, implied_yes_price_bps: bps, block_height: blockHeight, - }); - } - - return entries; + }; + }); } From 2fc7009bae45bba08d0b357776f198da4eb603da Mon Sep 17 00:00:00 2001 From: "Sergey B." Date: Thu, 16 Apr 2026 13:13:31 +0000 Subject: [PATCH 035/117] fix: avoiding repeated syncs during fee-convergence --- package-lock.json | 7694 ++++++++++++++++++++++ src-tauri/crates/deadcat-sdk/src/node.rs | 3 + src-tauri/crates/deadcat-sdk/src/sdk.rs | 25 +- 3 files changed, 7701 insertions(+), 21 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..833b1bde --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7694 @@ +{ + "name": "deadcat-live", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "deadcat-live", + "version": "0.1.0", + "dependencies": { + "@dicebear/collection": "^9.4.2", + "@dicebear/core": "^9.4.2", + "@tanstack/react-query": "^5.97.0", + "@tauri-apps/api": "^2.9.5", + "@tauri-apps/plugin-dialog": "^2.7.0", + "@tauri-apps/plugin-fs": "^2.5.0", + "@tauri-apps/plugin-log": "^2.0.0", + "@tauri-apps/plugin-opener": "^2.5.3", + "qrcode": "^1.5.4", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.9.5", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vitest": "^2.1.9" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dicebear/adventurer": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.4.2.tgz", + "integrity": "sha512-jqYp834ZmGDA9HBBDQAdgF1O2UTCwHF4vVrktXWa2Dppp1JczPL5HnVOWsjtrLmXNn61Wd6OLmBb2e6rhzp3ig==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/adventurer-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.4.2.tgz", + "integrity": "sha512-5xgkG/mNL4j3Q4SJGQLBU/KnU90tng8Ze5ofThD+55wi0oeY/nSAUowg6UFCmHrktjifj/MEx3CQqbpcPWtfIA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/avataaars": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.4.2.tgz", + "integrity": "sha512-3x9jKFkOkFSPmpTbt9xvhiU2E1GX7beCSsX0tXRUShj8x6+5Ks9yBRT1VlkySbnXrZ/GglADGg7vJ/D2uIx1Yw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/avataaars-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.4.2.tgz", + "integrity": "sha512-/eNrp0YCNJRwQXqOloLm1+3Ss2C+pMpUQIGkbEnGsP1UK+13Ge80ggDDof1HpdqvG9HAZcKa7hnbG/0HSwyDSw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/big-ears": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.4.2.tgz", + "integrity": "sha512-mNfz3ppNA7UBq0IO3nXCiV5pFPG7c1DfzRB0foNU2Wo1XXT8FIcSY2BvDlYqorZTOUOz7dHb0vx06hqvG0HP5w==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/big-ears-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.4.2.tgz", + "integrity": "sha512-M8Ozmzza4eY4hpLOYULgJxMYmBA0CsBnrE15/xw6LZkEREXnrX5z0NJsf8hUfdyF6BWZ+RBgzoiav32DAC5zcg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/big-smile": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.4.2.tgz", + "integrity": "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/bottts": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.4.2.tgz", + "integrity": "sha512-tsx+dII7EFUCVA8URj66G1GqORCCVduCAx4dY2prEY2IeFianVpkntXuFsWZ9BBGx1NZFndvDith5oTwKMQPbQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/bottts-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.4.2.tgz", + "integrity": "sha512-kFNwWt6j+gzZ5n5Pz7WVwePubREAQOF8ZwWA9ztwVYDVMLnOChWbAofy5FED4j5md2MXFH2EgLCFCMr5K2BmIA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/collection": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.4.2.tgz", + "integrity": "sha512-KArubv7if8H7j9sIfpDK2hJJqrdNVR5zMPAMOSpIU2JPyXx8TC9o5wsmXb8il5wOHgaS9Q/cla7jUNIiDD7Gsg==", + "dependencies": { + "@dicebear/adventurer": "9.4.2", + "@dicebear/adventurer-neutral": "9.4.2", + "@dicebear/avataaars": "9.4.2", + "@dicebear/avataaars-neutral": "9.4.2", + "@dicebear/big-ears": "9.4.2", + "@dicebear/big-ears-neutral": "9.4.2", + "@dicebear/big-smile": "9.4.2", + "@dicebear/bottts": "9.4.2", + "@dicebear/bottts-neutral": "9.4.2", + "@dicebear/croodles": "9.4.2", + "@dicebear/croodles-neutral": "9.4.2", + "@dicebear/dylan": "9.4.2", + "@dicebear/fun-emoji": "9.4.2", + "@dicebear/glass": "9.4.2", + "@dicebear/icons": "9.4.2", + "@dicebear/identicon": "9.4.2", + "@dicebear/initials": "9.4.2", + "@dicebear/lorelei": "9.4.2", + "@dicebear/lorelei-neutral": "9.4.2", + "@dicebear/micah": "9.4.2", + "@dicebear/miniavs": "9.4.2", + "@dicebear/notionists": "9.4.2", + "@dicebear/notionists-neutral": "9.4.2", + "@dicebear/open-peeps": "9.4.2", + "@dicebear/personas": "9.4.2", + "@dicebear/pixel-art": "9.4.2", + "@dicebear/pixel-art-neutral": "9.4.2", + "@dicebear/rings": "9.4.2", + "@dicebear/shapes": "9.4.2", + "@dicebear/thumbs": "9.4.2", + "@dicebear/toon-head": "9.4.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/core": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz", + "integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@dicebear/croodles": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.4.2.tgz", + "integrity": "sha512-6VoO0JviIf7dKKMBTL/SMXxWhnXHaZuzufX90G0nXxS77ELG1YkGNMaZzawizN4C09Gbya2gJkozqrWiJN/aGw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/croodles-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.4.2.tgz", + "integrity": "sha512-oG5IeUdtiYshQ89gkAVcl5w3xAEi5UZX2fTzIyelpBPCG176l7VuuFzlxi2umnB3E6LVHYy06DXvUo/p+rXB2Q==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/dylan": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.4.2.tgz", + "integrity": "sha512-1vQvRu9x9DrwFxhFaIU2rf0EUL04yDTbAt7fHyAjM0mEsKzTD4mRNf95tCRuavCoW6W48u7A/OY6jyIub6kxLQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/fun-emoji": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.4.2.tgz", + "integrity": "sha512-kqB6LPkdYCdEU/mwbyz34xLzoNUKL6ARcoo3fr5ASq9D6ZE07qIKybC3xv5+CPz7VmspJ1Q3c/VVWVMDRP7Twg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/glass": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.4.2.tgz", + "integrity": "sha512-z5qUogHQ1b6UJ2zCqT848mU2U9DKbVDhiX6GPDjD7tYLisCCJVisH9p6WyNdHvflUd4SHkA6gRqVJIh2v2HnTA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/icons": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.4.2.tgz", + "integrity": "sha512-QSMMz0NA03ypSGhXC8HQX8FSj8lYT+/5yqH+/N03OH2IjL0q7wwGZ7nqsrtlRp76O5WqMTwGfSbTUUYPjFr+Xw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/identicon": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.4.2.tgz", + "integrity": "sha512-JVDSmZsv11mSWqwAktK5x9Bslht2xY3TFUn8xzu6slAYe1Z7hEXZ76eb+UJ6F4qEzdwZ7xPWzAS6Nb0Y3A0pww==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/initials": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.4.2.tgz", + "integrity": "sha512-yePuIUasmwtl9IrtB6rEzE/zb5fImKP/neW0CdcTC2MwLgMuP1GLHEGRgg1zI8exIh+PMv1YdLGyyUuRTE2Qpw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/lorelei": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.4.2.tgz", + "integrity": "sha512-YMv6vnriW6VLFDsreKuOnUFFno6SRe7+7X7R7zPY0rZ+MaHX9V3jcioIG+1PSjIHEDfOLUHpr5vd1JBWv8y7UA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/lorelei-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.4.2.tgz", + "integrity": "sha512-yspanTthA5vh6iCdeLzn6xZ4yYMYRcfcxblcgSvHTF1ut0bjAXtw5SXzZ6aJTrJWiHkzYOQuTOR6GVYiW80Q7w==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/micah": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.4.2.tgz", + "integrity": "sha512-e4D3W/OlChSsLo7Llwsy0J18vk0azJqF/uFoY+EKACCNHBc1HGNsqVvu2CTf+OWOA8wTyAK6UkjBN5p01r7D+g==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/miniavs": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.4.2.tgz", + "integrity": "sha512-wLwyFNNUnDRd3BbhSBhXR0XEpX8sG0/xDA5M/OkDoapLqZnnI48YLUSDd2N5QTAVMmcSEuZOYxkcnj7WW79vlg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/notionists": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.4.2.tgz", + "integrity": "sha512-ZCySq+nxcD/x4xyYgytcj2N9uY3gxrL+qpnmOdp2BdA221KacVrxlsUPpIgEMqxS2rMmBQXfxg129Pzn4ycIpA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/notionists-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.4.2.tgz", + "integrity": "sha512-AyD9kEfVxQUwDGf4Op059gVmYIOAkTKg3dtE9h9mEKP7zl/kMy5B67BFFOo7sB0mXCjzAegZ6ekGU02E8+hIHw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/open-peeps": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.4.2.tgz", + "integrity": "sha512-i01tLgtp2g937T81sVeAOVlqsCtiTck/Kw20g7hN80+7xrXjOUepz2HPLy3HeiMjwjMGRy5o54kSd0/8Ht4Dqg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/personas": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.4.2.tgz", + "integrity": "sha512-NJlkvI5F5gugt6t2+7QrYNTwQC7+4IQZS3vG0dYk2BncxOHax0BuLovdSdiAesTL4ZkytFYIydWmKmV2/xcUwg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/pixel-art": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.4.2.tgz", + "integrity": "sha512-peHf7oKICDgBZ8dUyj+txPnS7VZEWgvKE+xW4mNQqBt6dYZIjmva2shOVHn0b1JU+FDxMx3uIkWVixKdUq4WGg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/pixel-art-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.4.2.tgz", + "integrity": "sha512-9e9Lz554uQvWaXV2P17ss+hPa6rTyuAKBtB8zk8ECjHiZzIl61N/KcTVLZ4dILVZwj7gYriaLo16QEqvL2GJCg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/rings": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.4.2.tgz", + "integrity": "sha512-Pc3ymWrRDQPJFNrbbLt7RJrzGvUuuxUiDkrfLhoVE+B6mZWEL1PC78DPbS1yUWYLErJOpJuM2GSwXmTbVjWf+g==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/shapes": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.4.2.tgz", + "integrity": "sha512-AFL6jAaiLztvcqyq+ds+lWZu6Vbp3PlGWhJeJRm842jxtiluJpl6r4f6nUXP2fdMz7MNpDzXfLooQK9E04NbUQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/thumbs": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.4.2.tgz", + "integrity": "sha512-ccWvDBqbkWS5uzHbsg5L6uML6vBfX7jT3J3jHCQksvz8haHItxTK02w+6e1UavZUsvza4lG5X/XY3eji3siJ4Q==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@dicebear/toon-head": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/toon-head/-/toon-head-9.4.2.tgz", + "integrity": "sha512-lwFeSXyAnaKnCfMt9TiJwnD1cXQUGkey/0h6i/+4TVHVMCz5/Ri5u1ynovPNHy1SnBf858QwoXHkxilGLwQX/g==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.0.tgz", + "integrity": "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.0.tgz", + "integrity": "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==", + "dependencies": { + "@tanstack/query-core": "5.99.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz", + "integrity": "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.0.tgz", + "integrity": "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ==", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@tauri-apps/plugin-log": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz", + "integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.339", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz", + "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + }, + "dependencies": { + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true + }, + "@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true + }, + "@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "requires": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, + "@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "requires": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + } + }, + "@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "requires": { + "@babel/types": "^7.29.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + } + }, + "@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + } + }, + "@dicebear/adventurer": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.4.2.tgz", + "integrity": "sha512-jqYp834ZmGDA9HBBDQAdgF1O2UTCwHF4vVrktXWa2Dppp1JczPL5HnVOWsjtrLmXNn61Wd6OLmBb2e6rhzp3ig==", + "requires": {} + }, + "@dicebear/adventurer-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.4.2.tgz", + "integrity": "sha512-5xgkG/mNL4j3Q4SJGQLBU/KnU90tng8Ze5ofThD+55wi0oeY/nSAUowg6UFCmHrktjifj/MEx3CQqbpcPWtfIA==", + "requires": {} + }, + "@dicebear/avataaars": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.4.2.tgz", + "integrity": "sha512-3x9jKFkOkFSPmpTbt9xvhiU2E1GX7beCSsX0tXRUShj8x6+5Ks9yBRT1VlkySbnXrZ/GglADGg7vJ/D2uIx1Yw==", + "requires": {} + }, + "@dicebear/avataaars-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.4.2.tgz", + "integrity": "sha512-/eNrp0YCNJRwQXqOloLm1+3Ss2C+pMpUQIGkbEnGsP1UK+13Ge80ggDDof1HpdqvG9HAZcKa7hnbG/0HSwyDSw==", + "requires": {} + }, + "@dicebear/big-ears": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.4.2.tgz", + "integrity": "sha512-mNfz3ppNA7UBq0IO3nXCiV5pFPG7c1DfzRB0foNU2Wo1XXT8FIcSY2BvDlYqorZTOUOz7dHb0vx06hqvG0HP5w==", + "requires": {} + }, + "@dicebear/big-ears-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.4.2.tgz", + "integrity": "sha512-M8Ozmzza4eY4hpLOYULgJxMYmBA0CsBnrE15/xw6LZkEREXnrX5z0NJsf8hUfdyF6BWZ+RBgzoiav32DAC5zcg==", + "requires": {} + }, + "@dicebear/big-smile": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.4.2.tgz", + "integrity": "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ==", + "requires": {} + }, + "@dicebear/bottts": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.4.2.tgz", + "integrity": "sha512-tsx+dII7EFUCVA8URj66G1GqORCCVduCAx4dY2prEY2IeFianVpkntXuFsWZ9BBGx1NZFndvDith5oTwKMQPbQ==", + "requires": {} + }, + "@dicebear/bottts-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.4.2.tgz", + "integrity": "sha512-kFNwWt6j+gzZ5n5Pz7WVwePubREAQOF8ZwWA9ztwVYDVMLnOChWbAofy5FED4j5md2MXFH2EgLCFCMr5K2BmIA==", + "requires": {} + }, + "@dicebear/collection": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.4.2.tgz", + "integrity": "sha512-KArubv7if8H7j9sIfpDK2hJJqrdNVR5zMPAMOSpIU2JPyXx8TC9o5wsmXb8il5wOHgaS9Q/cla7jUNIiDD7Gsg==", + "requires": { + "@dicebear/adventurer": "9.4.2", + "@dicebear/adventurer-neutral": "9.4.2", + "@dicebear/avataaars": "9.4.2", + "@dicebear/avataaars-neutral": "9.4.2", + "@dicebear/big-ears": "9.4.2", + "@dicebear/big-ears-neutral": "9.4.2", + "@dicebear/big-smile": "9.4.2", + "@dicebear/bottts": "9.4.2", + "@dicebear/bottts-neutral": "9.4.2", + "@dicebear/croodles": "9.4.2", + "@dicebear/croodles-neutral": "9.4.2", + "@dicebear/dylan": "9.4.2", + "@dicebear/fun-emoji": "9.4.2", + "@dicebear/glass": "9.4.2", + "@dicebear/icons": "9.4.2", + "@dicebear/identicon": "9.4.2", + "@dicebear/initials": "9.4.2", + "@dicebear/lorelei": "9.4.2", + "@dicebear/lorelei-neutral": "9.4.2", + "@dicebear/micah": "9.4.2", + "@dicebear/miniavs": "9.4.2", + "@dicebear/notionists": "9.4.2", + "@dicebear/notionists-neutral": "9.4.2", + "@dicebear/open-peeps": "9.4.2", + "@dicebear/personas": "9.4.2", + "@dicebear/pixel-art": "9.4.2", + "@dicebear/pixel-art-neutral": "9.4.2", + "@dicebear/rings": "9.4.2", + "@dicebear/shapes": "9.4.2", + "@dicebear/thumbs": "9.4.2", + "@dicebear/toon-head": "9.4.2" + } + }, + "@dicebear/core": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.4.2.tgz", + "integrity": "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w==", + "requires": { + "@types/json-schema": "^7.0.15" + } + }, + "@dicebear/croodles": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.4.2.tgz", + "integrity": "sha512-6VoO0JviIf7dKKMBTL/SMXxWhnXHaZuzufX90G0nXxS77ELG1YkGNMaZzawizN4C09Gbya2gJkozqrWiJN/aGw==", + "requires": {} + }, + "@dicebear/croodles-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.4.2.tgz", + "integrity": "sha512-oG5IeUdtiYshQ89gkAVcl5w3xAEi5UZX2fTzIyelpBPCG176l7VuuFzlxi2umnB3E6LVHYy06DXvUo/p+rXB2Q==", + "requires": {} + }, + "@dicebear/dylan": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.4.2.tgz", + "integrity": "sha512-1vQvRu9x9DrwFxhFaIU2rf0EUL04yDTbAt7fHyAjM0mEsKzTD4mRNf95tCRuavCoW6W48u7A/OY6jyIub6kxLQ==", + "requires": {} + }, + "@dicebear/fun-emoji": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.4.2.tgz", + "integrity": "sha512-kqB6LPkdYCdEU/mwbyz34xLzoNUKL6ARcoo3fr5ASq9D6ZE07qIKybC3xv5+CPz7VmspJ1Q3c/VVWVMDRP7Twg==", + "requires": {} + }, + "@dicebear/glass": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.4.2.tgz", + "integrity": "sha512-z5qUogHQ1b6UJ2zCqT848mU2U9DKbVDhiX6GPDjD7tYLisCCJVisH9p6WyNdHvflUd4SHkA6gRqVJIh2v2HnTA==", + "requires": {} + }, + "@dicebear/icons": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.4.2.tgz", + "integrity": "sha512-QSMMz0NA03ypSGhXC8HQX8FSj8lYT+/5yqH+/N03OH2IjL0q7wwGZ7nqsrtlRp76O5WqMTwGfSbTUUYPjFr+Xw==", + "requires": {} + }, + "@dicebear/identicon": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.4.2.tgz", + "integrity": "sha512-JVDSmZsv11mSWqwAktK5x9Bslht2xY3TFUn8xzu6slAYe1Z7hEXZ76eb+UJ6F4qEzdwZ7xPWzAS6Nb0Y3A0pww==", + "requires": {} + }, + "@dicebear/initials": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.4.2.tgz", + "integrity": "sha512-yePuIUasmwtl9IrtB6rEzE/zb5fImKP/neW0CdcTC2MwLgMuP1GLHEGRgg1zI8exIh+PMv1YdLGyyUuRTE2Qpw==", + "requires": {} + }, + "@dicebear/lorelei": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.4.2.tgz", + "integrity": "sha512-YMv6vnriW6VLFDsreKuOnUFFno6SRe7+7X7R7zPY0rZ+MaHX9V3jcioIG+1PSjIHEDfOLUHpr5vd1JBWv8y7UA==", + "requires": {} + }, + "@dicebear/lorelei-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.4.2.tgz", + "integrity": "sha512-yspanTthA5vh6iCdeLzn6xZ4yYMYRcfcxblcgSvHTF1ut0bjAXtw5SXzZ6aJTrJWiHkzYOQuTOR6GVYiW80Q7w==", + "requires": {} + }, + "@dicebear/micah": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.4.2.tgz", + "integrity": "sha512-e4D3W/OlChSsLo7Llwsy0J18vk0azJqF/uFoY+EKACCNHBc1HGNsqVvu2CTf+OWOA8wTyAK6UkjBN5p01r7D+g==", + "requires": {} + }, + "@dicebear/miniavs": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.4.2.tgz", + "integrity": "sha512-wLwyFNNUnDRd3BbhSBhXR0XEpX8sG0/xDA5M/OkDoapLqZnnI48YLUSDd2N5QTAVMmcSEuZOYxkcnj7WW79vlg==", + "requires": {} + }, + "@dicebear/notionists": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.4.2.tgz", + "integrity": "sha512-ZCySq+nxcD/x4xyYgytcj2N9uY3gxrL+qpnmOdp2BdA221KacVrxlsUPpIgEMqxS2rMmBQXfxg129Pzn4ycIpA==", + "requires": {} + }, + "@dicebear/notionists-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.4.2.tgz", + "integrity": "sha512-AyD9kEfVxQUwDGf4Op059gVmYIOAkTKg3dtE9h9mEKP7zl/kMy5B67BFFOo7sB0mXCjzAegZ6ekGU02E8+hIHw==", + "requires": {} + }, + "@dicebear/open-peeps": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.4.2.tgz", + "integrity": "sha512-i01tLgtp2g937T81sVeAOVlqsCtiTck/Kw20g7hN80+7xrXjOUepz2HPLy3HeiMjwjMGRy5o54kSd0/8Ht4Dqg==", + "requires": {} + }, + "@dicebear/personas": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.4.2.tgz", + "integrity": "sha512-NJlkvI5F5gugt6t2+7QrYNTwQC7+4IQZS3vG0dYk2BncxOHax0BuLovdSdiAesTL4ZkytFYIydWmKmV2/xcUwg==", + "requires": {} + }, + "@dicebear/pixel-art": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.4.2.tgz", + "integrity": "sha512-peHf7oKICDgBZ8dUyj+txPnS7VZEWgvKE+xW4mNQqBt6dYZIjmva2shOVHn0b1JU+FDxMx3uIkWVixKdUq4WGg==", + "requires": {} + }, + "@dicebear/pixel-art-neutral": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.4.2.tgz", + "integrity": "sha512-9e9Lz554uQvWaXV2P17ss+hPa6rTyuAKBtB8zk8ECjHiZzIl61N/KcTVLZ4dILVZwj7gYriaLo16QEqvL2GJCg==", + "requires": {} + }, + "@dicebear/rings": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.4.2.tgz", + "integrity": "sha512-Pc3ymWrRDQPJFNrbbLt7RJrzGvUuuxUiDkrfLhoVE+B6mZWEL1PC78DPbS1yUWYLErJOpJuM2GSwXmTbVjWf+g==", + "requires": {} + }, + "@dicebear/shapes": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.4.2.tgz", + "integrity": "sha512-AFL6jAaiLztvcqyq+ds+lWZu6Vbp3PlGWhJeJRm842jxtiluJpl6r4f6nUXP2fdMz7MNpDzXfLooQK9E04NbUQ==", + "requires": {} + }, + "@dicebear/thumbs": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.4.2.tgz", + "integrity": "sha512-ccWvDBqbkWS5uzHbsg5L6uML6vBfX7jT3J3jHCQksvz8haHItxTK02w+6e1UavZUsvza4lG5X/XY3eji3siJ4Q==", + "requires": {} + }, + "@dicebear/toon-head": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@dicebear/toon-head/-/toon-head-9.4.2.tgz", + "integrity": "sha512-lwFeSXyAnaKnCfMt9TiJwnD1cXQUGkey/0h6i/+4TVHVMCz5/Ri5u1ynovPNHy1SnBf858QwoXHkxilGLwQX/g==", + "requires": {} + }, + "@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "dev": true, + "optional": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "dev": true, + "optional": true + }, + "@tanstack/query-core": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.0.tgz", + "integrity": "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==" + }, + "@tanstack/react-query": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.0.tgz", + "integrity": "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==", + "requires": { + "@tanstack/query-core": "5.99.0" + } + }, + "@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==" + }, + "@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "requires": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "dev": true, + "optional": true + }, + "@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "dev": true, + "optional": true + }, + "@tauri-apps/plugin-dialog": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz", + "integrity": "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==", + "requires": { + "@tauri-apps/api": "^2.10.1" + } + }, + "@tauri-apps/plugin-fs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.0.tgz", + "integrity": "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ==", + "requires": { + "@tauri-apps/api": "^2.10.1" + } + }, + "@tauri-apps/plugin-log": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz", + "integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==", + "requires": { + "@tauri-apps/api": "^2.8.0" + } + }, + "@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "requires": { + "@tauri-apps/api": "^2.8.0" + } + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "requires": { + "undici-types": "~7.19.0" + } + }, + "@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "requires": { + "csstype": "^3.2.2" + } + }, + "@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "requires": {} + }, + "@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "requires": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + } + }, + "@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "requires": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "requires": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + } + }, + "@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + } + }, + "@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "requires": { + "tinyspy": "^3.0.2" + } + }, + "@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, + "autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "requires": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + } + }, + "baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "requires": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + } + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true + }, + "chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.5.339", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz", + "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true + }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true + }, + "node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true + }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, + "postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "requires": { + "lilconfig": "^3.1.1" + } + }, + "postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.1.1" + } + }, + "postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "requires": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==" + }, + "react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "requires": { + "scheduler": "^0.27.0" + } + }, + "react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "requires": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + } + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, + "tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true + }, + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true + }, + "tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true + }, + "undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "requires": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "fsevents": "~2.3.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, + "vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + } + } + }, + "vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "requires": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "requires": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + } + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + } + } + }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "requires": {} + } + } +} diff --git a/src-tauri/crates/deadcat-sdk/src/node.rs b/src-tauri/crates/deadcat-sdk/src/node.rs index 13bf1297..40cdeec2 100644 --- a/src-tauri/crates/deadcat-sdk/src/node.rs +++ b/src-tauri/crates/deadcat-sdk/src/node.rs @@ -2488,6 +2488,7 @@ mod tests { pool_id: announcement.lmsr_pool_id.clone(), market_id: announcement.market_id.clone(), creation_txid: announcement.creation_txid.clone(), + reserve_yes_outpoint: announcement.initial_reserve_outpoints[0].clone(), stored_initial_reserve_outpoints: Some( announcement .initial_reserve_outpoints @@ -2525,6 +2526,7 @@ mod tests { pool_id: announcement.lmsr_pool_id.clone(), market_id: announcement.market_id.clone(), creation_txid: announcement.creation_txid.clone(), + reserve_yes_outpoint: announcement.initial_reserve_outpoints[0].clone(), stored_initial_reserve_outpoints: Some([ format!("{}:7", announcement.creation_txid), format!("{}:8", announcement.creation_txid), @@ -2557,6 +2559,7 @@ mod tests { pool_id: announcement.lmsr_pool_id, market_id: announcement.market_id, creation_txid: announcement.creation_txid, + reserve_yes_outpoint: announcement.initial_reserve_outpoints[0].clone(), stored_initial_reserve_outpoints: None, witness_schema_version: announcement.witness_schema_version, current_s_index: announcement.current_s_index, diff --git a/src-tauri/crates/deadcat-sdk/src/sdk.rs b/src-tauri/crates/deadcat-sdk/src/sdk.rs index ae170b66..fc37e7b3 100644 --- a/src-tauri/crates/deadcat-sdk/src/sdk.rs +++ b/src-tauri/crates/deadcat-sdk/src/sdk.rs @@ -632,6 +632,7 @@ impl DeadcatSdk { F: FnMut(&mut Self, u64) -> Result>, B: FnOnce(PreparedTransaction, M) -> T, { + self.sync()?; if let MinerFeePolicy::ExactAmountSat { amount_sat } = tx_options.fee_policy.clone() { let computation = build(self, amount_sat)?; let fee = @@ -685,8 +686,6 @@ impl DeadcatSdk { amount_sat: u64, fee_rate_sat_per_kvb: Option, ) -> Result> { - self.sync()?; - let address: lwk_wollet::elements::Address = address_str .parse() .map_err(|e| Error::Query(format!("invalid address: {}", e)))?; @@ -779,6 +778,7 @@ impl DeadcatSdk { amount_sat: u64, tx_options: TxOptions, ) -> Result { + self.sync()?; match tx_options.fee_policy.clone() { MinerFeePolicy::ExactAmountSat { amount_sat: exact_fee_sat, @@ -905,7 +905,6 @@ impl DeadcatSdk { request: &CreateLmsrPoolRequest, fee_amount: u64, ) -> Result> { - self.sync()?; validate_create_lmsr_pool_request(request)?; let contract = CompiledLmsrPool::new(request.pool_params)?; @@ -1095,8 +1094,6 @@ impl DeadcatSdk { request: &AdjustLmsrPoolRequest, fee_amount: u64, ) -> Result> { - self.sync()?; - // Fetch the actual chain genesis hash for admin signature computation. // The Simplicity C runtime in elementsd uses the chain's real genesis // hash (via `uint256::data()` in LE byte order). For regtest, each @@ -1549,8 +1546,6 @@ impl DeadcatSdk { min_utxo_value: u64, fee_amount: u64, ) -> Result> { - self.sync()?; - let raw_utxos = self.utxos()?; let policy_asset = self.policy_asset(); let policy_bytes: [u8; 32] = policy_asset.into_inner().to_byte_array(); @@ -1915,6 +1910,8 @@ impl DeadcatSdk { /// Select wallet UTXOs for collateral and fee, returning unblinded UTXOs and change address. /// + /// Callers must ensure the wallet is synced before calling this method. + /// /// When the collateral asset is L-BTC (policy asset), a single UTXO can /// fund both collateral and fee. The returned UTXOs will share the same /// outpoint in that case — downstream PSET builders detect this and emit @@ -1925,7 +1922,6 @@ impl DeadcatSdk { pairs: u64, fee_amount: u64, ) -> Result<(UnblindedUtxo, UnblindedUtxo, lwk_wollet::elements::Address)> { - self.sync()?; let cpt = params.collateral_per_token; let required_collateral = pairs .checked_mul(2) @@ -2043,7 +2039,6 @@ impl DeadcatSdk { pairs_to_burn: u64, fee_amount: u64, ) -> Result> { - self.sync()?; let contract = CompiledPredictionMarket::new(*params)?; let (current_state, covenant_utxos) = self.scan_market_state(&contract, anchor)?; @@ -2173,7 +2168,6 @@ impl DeadcatSdk { oracle_signature: [u8; 64], fee_amount: u64, ) -> Result> { - self.sync()?; let contract = CompiledPredictionMarket::new(*params)?; let (current_state, covenant_utxos) = self.scan_market_state(&contract, anchor)?; @@ -2291,7 +2285,6 @@ impl DeadcatSdk { tokens_to_burn: u64, fee_amount: u64, ) -> Result> { - self.sync()?; let contract = CompiledPredictionMarket::new(*params)?; let (current_state, covenant_utxos) = self.scan_market_state(&contract, anchor)?; @@ -2391,7 +2384,6 @@ impl DeadcatSdk { anchor: &PredictionMarketAnchor, fee_amount: u64, ) -> Result> { - self.sync()?; let contract = CompiledPredictionMarket::new(*params)?; let (current_state, covenant_utxos) = self.scan_market_state(&contract, anchor)?; @@ -2464,7 +2456,6 @@ impl DeadcatSdk { tokens_to_burn: u64, fee_amount: u64, ) -> Result> { - self.sync()?; let contract = CompiledPredictionMarket::new(*params)?; let (current_state, covenant_utxos) = self.scan_market_state(&contract, anchor)?; @@ -2875,8 +2866,6 @@ impl DeadcatSdk { order_nonce: [u8; 32], fee_amount: u64, ) -> Result> { - self.sync()?; - // Build MakerOrderParams with a stable maker pubkey + nonce so // fee-resolution rebuilds do not mutate the public order identity. let (params, _p_order) = MakerOrderParams::new( @@ -3058,8 +3047,6 @@ impl DeadcatSdk { order_index: u32, fee_amount: u64, ) -> Result> { - self.sync()?; - // 1. Derive maker keypair let maker_keypair = self.derive_maker_keypair(order_index)?; @@ -3245,8 +3232,6 @@ impl DeadcatSdk { lots_to_fill: u64, fee_amount: u64, ) -> Result> { - self.sync()?; - // 1. Compile the contract let contract = CompiledMakerOrder::new(*params)?; @@ -3536,8 +3521,6 @@ impl DeadcatSdk { ) -> Result> { use crate::trade::pset::{TradePsetParams, build_trade_pset}; - self.sync()?; - if let Some(ref lmsr_leg) = plan.lmsr_pool_leg { self.validate_live_lmsr_reserve_anchors(lmsr_leg)?; } From ab36bf5c1b1b7829d7b1e1bd54cf50c73a707a32 Mon Sep 17 00:00:00 2001 From: Uty-re Date: Tue, 21 Apr 2026 21:11:59 +0100 Subject: [PATCH 036/117] feat: Polymarket-style probability chart with Yes/No toggles (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SVG chart with live probability line, crosshair, and hover pill - YES (teal) and NO (red) independent toggle buttons; at least one always active - NO line is the inverse of YES; cat markers always flipped 180° for NO - Hover-only pills ("Yes XX%" / "No XX%") with overlap prevention - Compact timestamp box on hover; y-axis labels as SVG text for exact alignment - 1h / 4h / 1d / 3d / 7d / 1M / ALL timescale buttons - 45-day mock price history (400 points, 8 deterministic shapes) covers all scales - Replaced Buy Yes/No buttons with large probability display in MarketHeader - Removed "Yes + No = N sats" text from chart legend --- src/components/chart/MarketChart.tsx | 502 +++++++++++++------------ src/components/detail/MarketHeader.tsx | 25 -- src/types.ts | 2 +- src/utils-react/chart-series.ts | 28 +- src/utils-react/mock-price-history.ts | 11 +- 5 files changed, 285 insertions(+), 283 deletions(-) diff --git a/src/components/chart/MarketChart.tsx b/src/components/chart/MarketChart.tsx index a630d074..fdac43c1 100644 --- a/src/components/chart/MarketChart.tsx +++ b/src/components/chart/MarketChart.tsx @@ -9,7 +9,6 @@ import { lastDefinedChartProbability, sampleChartProbabilityAtFraction, } from "../../utils-react/chart-series"; -import { fullContractSats } from "../../utils-react/market"; // ── Constants ─────────────────────────────────────────────────────── @@ -30,8 +29,6 @@ const PAW_PATHS = [ "M65.95,49.84c-2.36-2.86-4.3-6.01-6.45-9.02-.89-1.24-1.8-2.47-2.78-3.65-2.76-3.35-7.24-5.02-11.72-5.02s-8.96,1.68-11.72,5.02c-.98,1.19-1.89,2.41-2.78,3.65-2.15,3.01-4.08,6.15-6.45,9.02-1.77,2.15-4.25,3.82-6.11,5.92-4.14,4.69-4.72,9.96-1.94,15.3,2.79,5.37,8.01,7.6,14.41,7.9,4.82.23,9.23-1.95,13.98-2.16.22-.01.42-.01.62-.01s.4,0,.61.01c4.75.21,9.16,2.38,13.98,2.16,6.39-.3,11.62-2.53,14.41-7.9,2.77-5.34,2.2-10.61-1.94-15.3-1.87-2.1-4.35-3.77-6.12-5.92h0Z", ]; -const MIN_SERIES_SEPARATION = 6.2; - // ── Helper functions ──────────────────────────────────────────────── function markerSvg( @@ -39,16 +36,19 @@ function markerSvg( y: number, fill: string, scale = 1, + flip = false, ): React.JSX.Element { const width = MARKER_WIDTH * scale; const height = MARKER_HEIGHT * scale; - return ( + const inner = ( ); + if (!flip) return inner; + return {inner}; } function pulseSvg(x: number, y: number, toneClass: string): React.JSX.Element { @@ -70,35 +70,6 @@ function pulseSvg(x: number, y: number, toneClass: string): React.JSX.Element { ); } -function separateSeriesY( - yesYRaw: number, - noYRaw: number, - plotTop: number, - plotBottom: number, -): { yesY: number; noY: number } { - let yesY = yesYRaw; - let noY = noYRaw; - const gap = Math.abs(noY - yesY); - if (gap < MIN_SERIES_SEPARATION) { - const mid = (yesY + noY) / 2; - yesY = mid - MIN_SERIES_SEPARATION / 2; - noY = mid + MIN_SERIES_SEPARATION / 2; - } - const minY = plotTop + 0.9; - const maxY = plotBottom - 0.9; - if (yesY < minY) { - const shift = minY - yesY; - yesY += shift; - noY += shift; - } - if (noY > maxY) { - const shift = noY - maxY; - yesY -= shift; - noY -= shift; - } - return { yesY, noY }; -} - type PointXY = { x: number; y: number }; function buildPawTrail( @@ -204,11 +175,11 @@ function buildPawTrail( // ── Legend icon ────────────────────────────────────────────────────── -function LegendIcon({ fill }: { fill: string }) { +function LegendIcon({ fill, flip = false }: { fill: string; flip?: boolean }) { return (
@@ -507,22 +497,54 @@ export default function MarketChart({ className={`relative ${isHomeChart ? "h-[17.5rem]" : "h-[19.5rem]"} rounded-xl border border-slate-800 bg-slate-950/60 p-3`} > {/* Legend bar */} -
- - - Yes {legendYesPct != null ? `${legendYesPct}%` : "\u2014"} - - - - No {legendNoPct != null ? `${legendNoPct}%` : "\u2014"} - - Yes + No = {fc} sats +
+ {showYesLine && ( + + + {legendPct != null ? `Yes ${legendPct}%` : "\u2014"} + + )} + {showNoLine && ( + + + No {noReadoutPct != null ? `${noReadoutPct}%` : "\u2014"} + + )} {market.isLive && ( Live · Round 1 )} +
+ + +
{/* SVG chart */} @@ -536,23 +558,38 @@ export default function MarketChart({ className="h-full w-full" aria-hidden="true" > - {/* Guide lines */} - {guideLineYs.map((y) => ( - - ))} + {/* Guide lines + y-axis labels */} + {[1, 0.75, 0.5, 0.25, 0].map((level) => { + const y = yFromProbability(level); + return ( + + + + {`${level * 100}%`} + + + ); + })} {/* Series lines */} - {hasPrice && ( + {hasPrice && showYesLine && ( <> + {yesPawTrail} + + )} + {hasPrice && showNoLine && ( + <> - {yesPawTrail} {noPawTrail} )} - {/* Hover fade + crosshair */} {hoverAvailable && ( <> @@ -597,28 +637,27 @@ export default function MarketChart({ )} {/* Pulse animation at current point */} - {hasPrice && showCurrentPulse && ( - <> - {pulseSvg(yesEnd.x, yesEnd.y, "chartLivePulseYes")} - {pulseSvg(noEnd.x, noEnd.y, "chartLivePulseNo")} - - )} + {hasPrice && + showYesLine && + showCurrentPulse && + pulseSvg(yesEnd.x, yesEnd.y, "chartLivePulseYes")} {/* Endpoint markers */} {hasPrice && ( - {markerSvg(yesEnd.x, yesEnd.y, "#5eead4")} - {markerSvg(noEnd.x, noEnd.y, "#fb7185")} + {showYesLine && markerSvg(yesEnd.x, yesEnd.y, "#5eead4")} + {showNoLine && markerSvg(noEnd.x, noEnd.y, "#f87171", 1, true)} )} {/* Hover markers */} - {hoverAvailable && ( - <> - {markerSvg(yesHover?.x, yesHover?.y, "#5eead4", 1.16)} - {markerSvg(noHover?.x, noHover?.y, "#fb7185", 1.16)} - - )} + {hoverAvailable && + showYesLine && + markerSvg(yesHover?.x, yesHover?.y, "#5eead4", 1.16)} + {hoverAvailable && + showNoLine && + noHoverPoint && + markerSvg(noHoverPoint.x, noHoverPoint.y, "#f87171", 1.16, true)} {/* Hover time box */} {hoverAvailable && ( @@ -654,68 +693,50 @@ export default function MarketChart({ )} - {/* Readout labels */} - {readoutNoPct != null && ( + {/* Readout pills — hover only */} + {hoverAvailable && showYesLine && readoutPct != null && ( <> - - NO - + y={readoutBgY} + width={readoutEstWidth} + height={readoutBgHeight} + rx="2.2" + fill="#5eead4" + fillOpacity="0.92" + /> - {readoutNoPct}% + Yes {readoutPct}% - + )} + {hoverAvailable && showNoLine && noReadoutPct != null && ( + <> + - YES - + y={noBgY} + width={readoutEstWidth} + height={readoutBgHeight} + rx="2.2" + fill="#f87171" + fillOpacity="0.92" + /> - {readoutYesPct}% + No {noReadoutPct}% )} @@ -747,22 +768,15 @@ export default function MarketChart({ onMouseLeave={handleMouseLeave} /> - {/* Y-axis labels */} -
- 100% - 75% - 50% - 25% - 0% -
- {/* X-axis labels */}
{xLabels.map((label, i) => ( {label} @@ -776,7 +790,7 @@ export default function MarketChart({ {volumeLabel}
- {(["1h", "4h", "1d", "3d", "7d"] as const).map((key) => ( + {(["1h", "4h", "1d", "3d", "7d", "1M", "all"] as const).map((key) => ( ))}
diff --git a/src/components/detail/MarketHeader.tsx b/src/components/detail/MarketHeader.tsx index a138882a..a1cefa19 100644 --- a/src/components/detail/MarketHeader.tsx +++ b/src/components/detail/MarketHeader.tsx @@ -40,7 +40,6 @@ export default function MarketHeader({ market }: { market: Market }) { const walletData = useStore((s) => s.walletData); const walletNetwork = useStore((s) => s.walletNetwork); - const noPrice = market.yesPrice != null ? 1 - market.yesPrice : null; const estimatedSettlementDate = getEstimatedSettlementDate(market); const positions = getPositionContracts(market, walletData); @@ -121,30 +120,6 @@ export default function MarketHeader({ market }: { market: Market }) {

)} - {/* Yes / No quick-buy buttons */} -
- - -
- {/* Market stats */}
diff --git a/src/types.ts b/src/types.ts index c44a6d95..3c9a7adc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -508,7 +508,7 @@ export type PriceHistoryEntry = { block_height: number; }; -export type ChartTimescale = "1h" | "4h" | "1d" | "3d" | "7d"; +export type ChartTimescale = "1h" | "4h" | "1d" | "3d" | "7d" | "1M" | "all"; export type CreateLmsrPoolResponse = { txid: string; diff --git a/src/utils-react/chart-series.ts b/src/utils-react/chart-series.ts index fcbdb670..db126ae6 100644 --- a/src/utils-react/chart-series.ts +++ b/src/utils-react/chart-series.ts @@ -22,6 +22,8 @@ const SCALE_BLOCKS_BY_KEY: Record = { "1d": 1440, "3d": 4320, "7d": 10080, + "1M": 43200, + all: 0, // computed dynamically from history span }; const POINT_COUNT_BY_KEY: Record = { @@ -30,20 +32,27 @@ const POINT_COUNT_BY_KEY: Record = { "1d": 96, "3d": 108, "7d": 120, + "1M": 130, + all: 150, }; function clampProbability(value: number): number { return Math.max(0.02, Math.min(0.98, value)); } -function scaleConfig(timescale: ChartTimescale): { +function scaleConfig( + timescale: ChartTimescale, + historySpanBlocks = 0, +): { pointCount: number; scaleBlocks: number; } { - return { - scaleBlocks: SCALE_BLOCKS_BY_KEY[timescale], - pointCount: POINT_COUNT_BY_KEY[timescale], - }; + const pointCount = POINT_COUNT_BY_KEY[timescale]; + const scaleBlocks = + timescale === "all" + ? Math.max(SCALE_BLOCKS_BY_KEY["7d"], historySpanBlocks) + : SCALE_BLOCKS_BY_KEY[timescale]; + return { pointCount, scaleBlocks }; } /** Approximate wall-clock date for a given block height. */ @@ -103,7 +112,7 @@ function blockHeightToAxisLabel( if (sameDay) return timeStr; return date.toLocaleDateString([], { month: "short", day: "numeric" }); } - // 3d, 7d: date only — multi-day views don't need time precision + // 3d, 7d, 1M, all: date only — multi-day views don't need time precision return date.toLocaleDateString([], { month: "short", day: "numeric" }); } @@ -207,9 +216,14 @@ export function buildChartFromHistory( history: PriceHistoryEntry[], timescale: ChartTimescale, ): ChartSeriesData { - const { pointCount, scaleBlocks } = scaleConfig(timescale); + const firstHistoryHeight = history.length > 0 ? history[0].block_height : 0; const latestHistoryHeight = history.length > 0 ? history[history.length - 1].block_height : 0; + const historySpanBlocks = + latestHistoryHeight > firstHistoryHeight + ? latestHistoryHeight - firstHistoryHeight + : 0; + const { pointCount, scaleBlocks } = scaleConfig(timescale, historySpanBlocks); const endBlockHeight = Math.max( scaleBlocks, market.currentHeight || 0, diff --git a/src/utils-react/mock-price-history.ts b/src/utils-react/mock-price-history.ts index 4c55c211..e5c24b51 100644 --- a/src/utils-react/mock-price-history.ts +++ b/src/utils-react/mock-price-history.ts @@ -170,7 +170,7 @@ function shapePath( */ export function generateMockPriceHistory( market: Market, - pointCount = 200, + pointCount = 400, ): PriceHistoryEntry[] { const seed = hashCode(market.marketId); const shape = SHAPES[seed % SHAPES.length]; @@ -179,11 +179,10 @@ export function generateMockPriceHistory( const rawPath = shapePath(shape, endNorm, seed, pointCount); - // Always cover exactly the last 7 days (10,080 blocks) relative to - // currentHeight, so every timescale window (1h–7d) finds points to plot. - // Spanning from block 0 to currentHeight would spread 200 points across - // millions of blocks, leaving no points inside a 240-block (4h) window. - const rangeBlocks = 10080; + // Cover the last ~45 days (64,800 blocks) so all timescales including + // 1M (43,200 blocks) and ALL have enough data to fill the window. + // Points are dense enough (~162 blocks apart) to look smooth on 1h/4h views. + const rangeBlocks = 64800; const startBlock = Math.max( 0, (market.currentHeight || rangeBlocks) - rangeBlocks, From a9abe98f0f2634d4c25a2a9a45ffb8d809d92e55 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Tue, 21 Apr 2026 16:48:03 -0400 Subject: [PATCH 037/117] fix: split preview vs commit for Nostr identity to stop onboarding freeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The randomize button on the onboarding avatar screen called generate_nostr_identity on every click, which persisted a new key to disk and constructed a full DeadcatNode with three detached tokio tasks (relay subscription, discovery forwarder, wallet-snapshot forwarder). The handles were dropped, so old tasks kept holding Arc clones and never exited. After ~12 clicks the accumulated background loops froze the app. - Add preview_nostr_identity: pure in-memory key generation, returns {pubkey_hex, npub, nsec}. No disk write, no node. - Commit via import_nostr_nsec(nsec) in handleCreateAccount — single node construction per onboarding. - Capture the 3 per-node JoinHandles on NodeState; abort prior handles on replacement and on delete_nostr_identity so even legitimate rotations (e.g. import after delete) don't leak. - Add a useRef in-flight guard on the randomize button to prevent concurrent previews from rapid clicks. --- src-tauri/src/commands.rs | 68 ++++++++++++++++++-- src-tauri/src/lib.rs | 6 ++ src/components/onboarding/NostrSetupStep.tsx | 38 +++++++---- src/types.ts | 6 ++ 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 747c50fb..73075cb4 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -238,27 +238,40 @@ async fn construct_and_store_node( guard.as_ref().cloned() }; + // Abort any background tasks from the previous node. Without this, each + // replacement leaks the relay subscription + two forwarder loops until + // their channels close (which requires the Arc to drop — blocked by the + // tasks themselves holding clones). See fix/preview-nostr-identity. + { + let mut handles = node_state.task_handles.lock().await; + for h in handles.drain(..) { + h.abort(); + } + } + log::info!( "[restore-trace] construct_and_store_node: node stored, starting subscription at {:?}", _t0.elapsed() ); + let mut new_handles: Vec> = Vec::with_capacity(3); + // Start the Nostr subscription in the background — do NOT await. // This connects to relays and starts the event loop. The node is // already stored in NodeState so commands like discover_contracts // can use it immediately (they handle missing relay data gracefully). if let Some(node) = node_arc.clone() { - tokio::spawn(async move { + new_handles.push(tokio::spawn(async move { if let Err(e) = node.start_subscription().await { log::warn!("failed to start discovery subscription: {e}"); } log::info!("discovery subscription started"); - }); + })); } // Forward discovery events to the frontend let app_handle = app.clone(); - tokio::spawn(async move { + new_handles.push(tokio::spawn(async move { use deadcat_sdk::DiscoveryEvent; while let Ok(event) = rx.recv().await { match event { @@ -281,13 +294,13 @@ async fn construct_and_store_node( } } log::info!("discovery event forwarding loop ended"); - }); + })); // Forward wallet snapshot changes to the frontend, throttled to avoid // flooding the main thread with large serialized payloads during sync. let app_snapshot = app.clone(); let policy_asset = sdk_network.into_lwk().policy_asset(); - tokio::spawn(async move { + new_handles.push(tokio::spawn(async move { let mut last_emit = std::time::Instant::now() - std::time::Duration::from_secs(1); let min_interval = std::time::Duration::from_millis(500); @@ -324,7 +337,13 @@ async fn construct_and_store_node( last_emit = std::time::Instant::now(); } log::info!("wallet snapshot forwarding loop ended"); - }); + })); + + // Store handles so the next construct_and_store_node call can abort them. + { + let mut handles = node_state.task_handles.lock().await; + handles.extend(new_handles); + } Ok(()) } @@ -389,6 +408,37 @@ pub async fn generate_nostr_identity(app: tauri::AppHandle) -> Result Result { + let keys = Keys::generate(); + Ok(PreviewIdentityResponse { + pubkey_hex: keys.public_key().to_hex(), + npub: keys + .public_key() + .to_bech32() + .map_err(|e| format!("bech32 error: {e}"))?, + nsec: keys + .secret_key() + .to_bech32() + .map_err(|e| format!("bech32 error: {e}"))?, + }) +} + #[tauri::command] pub async fn get_nostr_identity(app: tauri::AppHandle) -> Result, String> { let node_state = app.state::(); @@ -466,7 +516,7 @@ pub async fn export_nostr_nsec(app: tauri::AppHandle) -> Result #[tauri::command] pub async fn delete_nostr_identity(app: tauri::AppHandle) -> Result<(), String> { - // Lock wallet and drop node + // Lock wallet, abort background tasks, and drop node { let node_state = app.state::(); let mut guard = node_state.node.lock().await; @@ -474,6 +524,10 @@ pub async fn delete_nostr_identity(app: tauri::AppHandle) -> Result<(), String> node.lock_wallet(); } *guard = None; + let mut handles = node_state.task_handles.lock().await; + for h in handles.drain(..) { + h.abort(); + } } // Clear wallet state and payment swaps diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a155173f..fc96897e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -38,12 +38,17 @@ pub struct NodeState { pub node: tokio::sync::Mutex< Option>>, >, + /// JoinHandles for background tasks spawned per node (relay subscription, + /// discovery event forwarding, wallet snapshot forwarding). When a node is + /// replaced or dropped these are aborted so tasks don't leak. + pub task_handles: tokio::sync::Mutex>>, } impl Default for NodeState { fn default() -> Self { Self { node: tokio::sync::Mutex::new(None), + task_handles: tokio::sync::Mutex::new(Vec::new()), } } } @@ -1668,6 +1673,7 @@ pub fn run() { // SDK / Nostr commands::init_nostr_identity, commands::generate_nostr_identity, + commands::preview_nostr_identity, commands::get_nostr_identity, commands::export_nostr_nsec, commands::delete_nostr_identity, diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 4da31050..3335774b 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -10,7 +10,11 @@ import { useState, } from "react"; import { useStore } from "../../store"; -import type { IdentityResponse, NostrProfile } from "../../types"; +import type { + IdentityResponse, + NostrProfile, + PreviewIdentityResponse, +} from "../../types"; import { generateAvatarDataUri } from "../../utils-react/avatar"; import { randomCatName } from "../../utils-react/random-name"; import { showToast } from "../shared/Toast"; @@ -52,6 +56,9 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const [mnemonicExpanded, setMnemonicExpanded] = useState(false); const [restorePasswordRevealed, setRestorePasswordRevealed] = useState(false); const mnemonicTextareaRef = useRef(null); + // Guard against rapid clicks of the identity-randomize button spawning + // concurrent preview_nostr_identity calls. + const randomizingRef = useRef(false); // Focus the mnemonic textarea only when first expanded, not on every re-render useEffect(() => { @@ -201,17 +208,18 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { }, [nostrDone, nostrMode]); // ── Generate handler ──────────────────────────────────────────────── + // Previews a fresh identity (no disk write, no node construction). The + // identity is only committed at handleCreateAccount via import_nostr_nsec. const handleGenerate = useCallback(async () => { useStore.setState({ onboardingLoading: true, onboardingError: "" }); try { - const identity = await invoke( - "generate_nostr_identity", + const identity = await invoke( + "preview_nostr_identity", ); - const nsec = await invoke("export_nostr_nsec"); useStore.setState({ onboardingPendingPubkey: identity.pubkey_hex, onboardingPendingNpub: identity.npub, - onboardingNostrGeneratedNsec: nsec, + onboardingNostrGeneratedNsec: identity.nsec, onboardingProfileStep: true, onboardingNostrDisplayName: randomCatName(), }); @@ -351,8 +359,13 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { }); // 2. Create wallet const mnemonic = await invoke("create_wallet", { password }); - // 3. Init nostr node + unlock wallet - await invoke("init_nostr_identity"); + // 3. Persist the previewed nsec + construct nostr node + // (preview_nostr_identity didn't touch disk or spawn tasks, so we + // commit it here via import_nostr_nsec — the single place that + // should ever build a DeadcatNode in the generate flow). + await invoke("import_nostr_nsec", { + nsec: s.onboardingNostrGeneratedNsec, + }); await invoke("unlock_wallet", { password }); useStore.setState({ walletSessionPassword: password }); // 4. Publish kind 0 with name (avatar is DiceBear, generated client-side) @@ -644,15 +657,16 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) {
)} - - {/* Advanced details toggle */} -
-
-

- Protocol Details — - oracle, covenant paths, and collateral mechanics -

- -
-
- - {/* Advanced details expanded */} - {showAdvancedDetails && ( - <> - - - - - )}
{/* Right column — Trading panel */} @@ -241,279 +211,3 @@ export default function DetailPage() {
); } - -// ── Advanced detail sub-components ────────────────────────────────── - -function AdvancedDetails({ market }: { market: import("../../types").Market }) { - const collateralPoolSats = market.collateralUtxos.reduce( - (sum, utxo) => sum + utxo.amountSats, - 0, - ); - - const npub = useMemo( - () => hexToNpub(market.oraclePubkey), - [market.oraclePubkey], - ); - - const truncatedNpub = `${npub.slice(0, 10)}...${npub.slice(-6)}`; - - return ( -
-
-

Oracle

-

Oracle Attestation

-
-
- Oracle - -
-
- Market ID - -
-
- Block target - - {market.expiryHeight.toLocaleString("en-US")} - -
-
- Current height - - {market.currentHeight.toLocaleString("en-US")} - -
-
- Message domain - SHA256(ID || outcome) -
-
- Outcome bytes - YES=0x01, NO=0x00 -
-
- Resolve status - - {market.resolveTx - ? `Attested ${market.resolveTx.outcome.toUpperCase()} @ ${market.resolveTx.height}` - : "Unresolved"} - -
- {market.resolveTx && ( - <> -
- Sig hash - -
-
- Resolve tx - -
- - )} -
-
- -
-

Integrity

-

Single-UTXO Integrity

-

- {market.collateralUtxos.length === 1 - ? "OK: exactly one collateral UTXO" - : "ALERT: fragmented collateral UTXO set"} -

-
- {market.collateralUtxos.map((utxo) => ( -
- - - {formatSats(utxo.amountSats)} - -
- ))} -
-

- Collateral pool: {formatSats(collateralPoolSats)} ·{" "} - {stateLabel(market.state)} -

-
-
- ); -} - -function CollateralMechanics() { - return ( -
-

Accounting

-

Collateral Mechanics

-
-
- Issuance: pairs * 2 * CPT -
-
- Post-resolution redeem:{" "} - tokens * 2 * CPT -
-
- Expiry redeem: tokens * CPT -
-
- Cancellation: pairs * 2 * CPT -
-
-
- ); -} - -function PathCard({ - label, - enabled, - formula, - next, -}: { - label: string; - enabled: boolean; - formula: string; - next: string; -}) { - return ( -
-
-

{label}

- - {enabled ? "Available" : "Locked"} - -
-

{formula}

-

{next}

-
- ); -} - -function CovenantPaths({ market }: { market: import("../../types").Market }) { - const paths = getPathAvailability(market); - const expired = isExpired(market); - - return ( -
-

State Machine

-

Covenant Paths

-
- - - - - - - -
-
- ); -} diff --git a/src/components/detail/MarketHeader.tsx b/src/components/detail/MarketHeader.tsx index a1cefa19..6946b938 100644 --- a/src/components/detail/MarketHeader.tsx +++ b/src/components/detail/MarketHeader.tsx @@ -21,7 +21,7 @@ function stateBadge(state: number) { }; const labels: Record = { 0: "Dormant", - 1: "Unresolved", + 1: "Trading", 2: "Resolved YES", 3: "Resolved NO", 4: "Expired", @@ -36,12 +36,22 @@ function stateBadge(state: number) { ); } -export default function MarketHeader({ market }: { market: Market }) { - const walletData = useStore((s) => s.walletData); +/** Above the chart: nav, title, probability, stats strip. */ +export function MarketHeaderTop({ market }: { market: Market }) { const walletNetwork = useStore((s) => s.walletNetwork); - const estimatedSettlementDate = getEstimatedSettlementDate(market); - const positions = getPositionContracts(market, walletData); + const blocksLeft = market.expiryHeight - market.currentHeight; + const closesColor = + blocksLeft < 2880 + ? "text-rose-400" + : blocksLeft < 10080 + ? "text-amber-400" + : "text-slate-200"; + + const changePct = + market.change24h !== 0 + ? `${market.change24h > 0 ? "+" : ""}${(market.change24h * 100).toFixed(1)}%` + : null; return ( <> @@ -105,101 +115,89 @@ export default function MarketHeader({ market }: { market: Market }) {
- {/* Title & description */} -

+ {/* Title */} +

{market.question}

-

{market.description}

- {/* Probability display */} + {/* Probability + 24h change */} {market.yesPrice != null && ( -

- {Math.round(market.yesPrice * 100)} - %{" "} +

+

+ {Math.round(market.yesPrice * 100)} + % +

chance -

+ {changePct && ( + 0 ? "text-emerald-400" : "text-rose-400"}`} + > + {changePct} today + + )} +
)} - {/* Market stats */} -
-
-

- Volume -

-

+ {/* Stats strip */} +

+ + Vol{" "} + {formatVolumeBtc(market.volumeBtc)} -

-
-
-

- Traders -

-

- {market.traderCount > 0 - ? market.traderCount.toLocaleString() - : "\u2014"} -

-
-
-

- Liquidity -

-

- {formatVolumeBtc(market.liquidityBtc)} -

-
-
-

- Ends -

-

- {formatTimeRemaining(market.expiryHeight - market.currentHeight)} -

-
-
- - {/* Resolution rules */} -
-
- - - Resolution Rules - -
-

{market.description}

-
- - Source:{" "} - {market.resolutionSource} - - Deadline:{" "} - - Est. {formatSettlementDateTime(estimatedSettlementDate)} - + + · + + Traders{" "} + + {market.traderCount > 0 ? market.traderCount.toLocaleString() : "—"} - - Block:{" "} - - {market.expiryHeight.toLocaleString()} - + + · + + Liquidity{" "} + + {formatVolumeBtc(market.liquidityBtc)} -
+ + · + + Closes{" "} + {formatTimeRemaining(blocksLeft)} +
+ + ); +} + +/** Below the chart: description, resolution metadata, position display. */ +export function MarketHeaderBottom({ market }: { market: Market }) { + const walletData = useStore((s) => s.walletData); + + const estimatedSettlementDate = getEstimatedSettlementDate(market); + const positions = getPositionContracts(market, walletData); + + return ( + <> + {/* Description */} +

{market.description}

+ + {/* Resolution metadata — compact single line */} +

+ + Resolution + + {" · "}Source:{" "} + {market.resolutionSource} + {" · "}Deadline:{" "} + + Est. {formatSettlementDateTime(estimatedSettlementDate)} + + {" · "}Block:{" "} + + {market.expiryHeight.toLocaleString()} + +

{/* Position display */} {(positions.yes > 0 || positions.no > 0) && ( @@ -220,3 +218,13 @@ export default function MarketHeader({ market }: { market: Market }) { ); } + +/** @deprecated Use MarketHeaderTop + MarketHeaderBottom split around the chart. */ +export default function MarketHeader({ market }: { market: Market }) { + return ( + <> + + + + ); +} diff --git a/src/components/detail/OrderbookPanel.tsx b/src/components/detail/OrderbookPanel.tsx index 92b6ca51..4bb913c7 100644 --- a/src/components/detail/OrderbookPanel.tsx +++ b/src/components/detail/OrderbookPanel.tsx @@ -3,6 +3,7 @@ import { useCancelLimitOrder } from "../../queries/mutations/useTrading"; import { useStore } from "../../store"; import type { Market } from "../../types"; import { getFullOrderbook } from "../../utils-react/market"; +import { generateMockOrderbook } from "../../utils-react/mock-orderbook"; export default function OrderbookPanel({ market }: { market: Market }) { const selectedSide = useStore((s) => s.selectedSide); @@ -11,18 +12,44 @@ export default function OrderbookPanel({ market }: { market: Market }) { const cancelLimitOrderMutation = useCancelLimitOrder(); - const book = useMemo( + const realBook = useMemo( () => getFullOrderbook(market, selectedSide), [market, selectedSide], ); + const book = useMemo(() => { + const hasOrders = realBook.asks.length > 0 || realBook.bids.length > 0; + return hasOrders ? realBook : generateMockOrderbook(market); + }, [realBook, market]); - const maxContracts = Math.max( - ...book.asks.map((l) => l.contracts), - ...book.bids.map((l) => l.contracts), + // Cumulative sums — asks accumulate away from spread (top of book outward), + // bids accumulate away from spread (top of book outward). Both grow outward + // from the spread, producing a V-curve depth shape. + const cumAsks = useMemo(() => { + // asks are sorted ascending; cumulate from best (lowest) upward + let running = 0; + return book.asks.map((l) => { + running += l.contracts; + return { ...l, cumulative: running }; + }); + }, [book.asks]); + + const cumBids = useMemo(() => { + // bids are sorted descending; cumulate from best (highest) downward + let running = 0; + return book.bids.map((l) => { + running += l.contracts; + return { ...l, cumulative: running }; + }); + }, [book.bids]); + + const maxCumulative = Math.max( + cumAsks.length > 0 ? cumAsks[cumAsks.length - 1].cumulative : 0, + cumBids.length > 0 ? cumBids[cumBids.length - 1].cumulative : 0, 1, ); - const askRows = useMemo(() => [...book.asks].reverse(), [book.asks]); + // Asks displayed highest price at top → reverse so highest ask is first row + const askRows = useMemo(() => [...cumAsks].reverse(), [cumAsks]); const myOrders = useMemo( () => @@ -65,9 +92,9 @@ export default function OrderbookPanel({ market }: { market: Market }) { Contracts
- {/* Asks (reversed — highest at top) */} + {/* Asks (reversed — highest at top, widest bar at top) */} {askRows.map((level) => { - const pct = (level.contracts / maxContracts) * 100; + const pct = (level.cumulative / maxCumulative) * 100; return (
{level.priceSats} - {level.contracts.toFixed(0)} + {level.cumulative.toFixed(0)}
); @@ -101,8 +128,9 @@ export default function OrderbookPanel({ market }: { market: Market }) { No bids
)} - {book.bids.map((level) => { - const pct = (level.contracts / maxContracts) * 100; + {/* Bids (best bid at top, widest bar at bottom) */} + {cumBids.map((level) => { + const pct = (level.cumulative / maxCumulative) * 100; return (
- {level.contracts.toFixed(0)} + {level.cumulative.toFixed(0)}
); diff --git a/src/components/detail/TradingPanel.tsx b/src/components/detail/TradingPanel.tsx index c38208f8..bca0151e 100644 --- a/src/components/detail/TradingPanel.tsx +++ b/src/components/detail/TradingPanel.tsx @@ -46,7 +46,7 @@ export default function TradingPanel({ market }: { market: Market }) { const tradeExecuteLoading = useStore((s) => s.tradeExecuteLoading); const tradeQuoteSnapshot = useStore((s) => s.tradeQuoteSnapshot); const tradeError = useStore((s) => s.tradeError); - const showFeeDetails = useStore((s) => s.showFeeDetails); + const showOrderbook = useStore((s) => s.showOrderbook); const walletStatus = useStore((s) => s.walletStatus); const walletData = useStore((s) => s.walletData); const marketMakerMode = useStore((s) => s.marketMakerMode); @@ -118,16 +118,21 @@ export default function TradingPanel({ market }: { market: Market }) { tradeIntent === "open" && policyBalance < tradeSizeSats; + const ctaBg = + insufficientFunds || tradeBusy + ? "bg-slate-700 text-slate-400" + : selectedSide === "yes" + ? "bg-emerald-300 text-slate-950 hover:bg-emerald-200" + : "bg-rose-400 text-slate-950 hover:bg-rose-300"; + const yesPrice = market.yesPrice; const yesDisplaySats = yesPrice != null ? clampContractPriceSats(Math.round(yesPrice * fc), fc) : null; const noDisplaySats = yesDisplaySats != null ? fc - yesDisplaySats : null; - const yesLabel = - yesDisplaySats != null ? `Yes ${yesDisplaySats} sats` : "Yes \u2014"; - const noLabel = - noDisplaySats != null ? `No ${noDisplaySats} sats` : "No \u2014"; + const yesPct = yesPrice != null ? Math.round(yesPrice * 100) : null; + const noPct = yesPct != null ? 100 - yesPct : null; const estimatedGrossPayoutSats = Math.floor(preview.requestedContracts * fc); @@ -404,28 +409,42 @@ export default function TradingPanel({ market }: { market: Market }) {
@@ -502,19 +521,43 @@ export default function TradingPanel({ market }: { market: Market }) { {/* Size input */} {tradeIntent === "open" ? ( - - useStore.setState({ tradeSizeSatsDraft: e.target.value }) - } - onBlur={() => { - const result = commitTradeSizeSatsDraft(tradeSizeSatsDraft); - useStore.setState(result); - }} - className="mb-2 w-full rounded-lg border border-slate-700 bg-slate-950/80 px-3 py-2 text-sm" - /> + <> +
+ + useStore.setState({ tradeSizeSatsDraft: e.target.value }) + } + onBlur={() => { + const result = commitTradeSizeSatsDraft(tradeSizeSatsDraft); + useStore.setState(result); + }} + className="h-12 w-full rounded-lg border border-slate-700 bg-slate-950/80 px-3 pr-14 text-sm" + /> + + sats + +
+ {tradeSizeSats > 0 && ( +
+ Potential payout + + {formatSats(estimatedGrossPayoutSats)} + {preview.notionalSats > 0 && + estimatedGrossPayoutSats > preview.notionalSats && ( + + {( + estimatedGrossPayoutSats / preview.notionalSats + ).toFixed(2)} + x + + )} + +
+ )} + ) : ( <>
@@ -614,11 +657,16 @@ export default function TradingPanel({ market }: { market: Market }) { {friendlyError(tradeError)}

)} + {market.traderCount > 0 && ( +

+ {market.traderCount.toLocaleString()} traders in this market +

+ )} -
- - {/* Inline order book */} - - - {showFeeDetails && ( -
+ {/* Fee details */} +
+ + Fee details + +

Execution fee: 1% of matched notional.

Winning PnL fee: 2% of positive payout minus entry cost (buy only).

Final fee depends on actual matched fills.

- )} +
+ + {/* Collapsible order book */} +
+ + {showOrderbook && } +
{/* Market maker pool & advanced actions sections */} {marketMakerMode && } diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index 63333b1d..f1def3ba 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -210,14 +210,14 @@ function SidebarMarketItem({ onClick={() => openMarket(market, { side: "yes", intent: "open" }) } - className="rounded-full bg-emerald-500 px-2.5 py-0.5 text-xs font-medium text-white transition hover:bg-emerald-400" + className="rounded-full bg-emerald-500/15 px-2.5 py-0.5 text-xs font-medium text-emerald-400 transition hover:bg-emerald-500/25" > {yesPct != null ? `Yes ${yesPct}%` : "Buy Yes"} diff --git a/src/components/home/MarketCard.tsx b/src/components/home/MarketCard.tsx index 9b670640..33bf17ac 100644 --- a/src/components/home/MarketCard.tsx +++ b/src/components/home/MarketCard.tsx @@ -243,14 +243,14 @@ export default function MarketCard({ diff --git a/src/utils-react/mock-orderbook.ts b/src/utils-react/mock-orderbook.ts new file mode 100644 index 00000000..038907f1 --- /dev/null +++ b/src/utils-react/mock-orderbook.ts @@ -0,0 +1,81 @@ +import type { FullOrderbook, Market, OrderbookLevel } from "../types"; +import { fullContractSats } from "./market"; + +/** Simple deterministic hash for seeding. */ +function hashCode(s: string): number { + let h = 0; + for (let i = 0; i < s.length; i++) { + h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; + } + return Math.abs(h); +} + +/** Seeded pseudo-random float in [0, 1). */ +function seededRand(seed: number): number { + const x = Math.sin(seed + 1) * 10000; + return x - Math.floor(x); +} + +/** + * Generate a deterministic mock orderbook for a market side. + * + * Asks cluster above the current mid-price, bids cluster below it. + * The spread, depth, and level sizes all vary deterministically per market so + * each card looks distinct but stays stable across re-renders. + */ +export function generateMockOrderbook(market: Market): FullOrderbook { + const seed = hashCode(market.marketId); + const fc = fullContractSats(market); + // Mid-price in sats based on current yes probability + const midPriceSats = Math.round((market.yesPrice ?? 0.5) * fc); + + // Spread: 1–4% of full contract, at least 1 sat + const spreadFraction = 0.01 + seededRand(seed) * 0.03; + const halfSpread = Math.max(1, Math.round((spreadFraction * fc) / 2)); + + const bestAsk = Math.min(fc - 1, midPriceSats + halfSpread); + const bestBid = Math.max(1, midPriceSats - halfSpread); + + // Number of levels: 4–7 per side + const askLevelCount = 4 + Math.floor(seededRand(seed + 1) * 4); + const bidLevelCount = 4 + Math.floor(seededRand(seed + 2) * 4); + + // Tick size: 1–3 sats between levels + const askTick = 1 + Math.floor(seededRand(seed + 3) * 3); + const bidTick = 1 + Math.floor(seededRand(seed + 4) * 3); + + // Max contracts at the deepest level: 50–300 + const maxDepth = 50 + Math.floor(seededRand(seed + 5) * 250); + + const asks: OrderbookLevel[] = []; + for (let i = 0; i < askLevelCount; i++) { + const priceSats = Math.min(fc - 1, bestAsk + i * askTick); + // Volume tapers away from best price (deeper = more liquidity) + const depthFraction = 0.3 + (i / (askLevelCount - 1)) * 0.7; + const contracts = Math.round( + depthFraction * maxDepth * (0.7 + seededRand(seed + 10 + i) * 0.6), + ); + if (contracts > 0) asks.push({ priceSats, contracts }); + } + + const bids: OrderbookLevel[] = []; + for (let i = 0; i < bidLevelCount; i++) { + const priceSats = Math.max(1, bestBid - i * bidTick); + const depthFraction = 0.3 + (i / (bidLevelCount - 1)) * 0.7; + const contracts = Math.round( + depthFraction * maxDepth * (0.7 + seededRand(seed + 20 + i) * 0.6), + ); + if (contracts > 0) bids.push({ priceSats, contracts }); + } + + // Sort: asks ascending (lowest ask first), bids descending (highest bid first) + asks.sort((a, b) => a.priceSats - b.priceSats); + bids.sort((a, b) => b.priceSats - a.priceSats); + + const spread = + asks.length > 0 && bids.length > 0 + ? asks[0].priceSats - bids[0].priceSats + : null; + + return { asks, bids, spread }; +} From 18618dd0fcb2d5bca4cbc4c15e6358d4cab1514c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 04:03:37 +0000 Subject: [PATCH 039/117] refactor: introduce NostrSigner trait abstraction for NIP-46 remote signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct `Keys` usage with `Arc` throughout the SDK and app layer, laying the foundation for NIP-46 remote signing support. Key changes: - DeadcatNode and DiscoveryService now hold `Arc` instead of `Keys` - Event builder functions return `EventBuilder` instead of `Event`, separating construction from signing (callers use `client.send_event_builder()`) - Oracle attestation signing separated from event publishing (requires local keys) - App-layer commands use `get_signer_and_client()` for generic operations and `get_keys_and_client()` for operations requiring secret key access - Add `nostr-connect` v0.38 dependency and enable `nip44` feature on `nostr-sdk` All existing behavior is preserved — `Keys` implements `NostrSigner`, so the local-keys path works identically. This refactor enables Milestone 2: wiring in `NostrConnect` as an alternative signer for bunker:// URI login. https://claude.ai/code/session_0198cw5FjyRcQMrB15tqt3WA --- src-tauri/Cargo.lock | 17 ++++ src-tauri/Cargo.toml | 1 + src-tauri/crates/deadcat-sdk/Cargo.toml | 2 +- .../deadcat-sdk/src/discovery/attestation.rs | 14 +-- .../deadcat-sdk/src/discovery/market.rs | 25 ++--- .../crates/deadcat-sdk/src/discovery/mod.rs | 68 ++++++------- .../crates/deadcat-sdk/src/discovery/pool.rs | 34 +++---- .../deadcat-sdk/src/discovery/service.rs | 82 ++++++++-------- src-tauri/crates/deadcat-sdk/src/node.rs | 71 ++++++++++---- .../crates/deadcat-sdk/tests/discovery.rs | 26 +++-- .../crates/deadcat-sdk/tests/lmsr_pool.rs | 3 +- .../crates/deadcat-sdk/tests/lmsr_trade.rs | 3 +- src-tauri/crates/deadcat-sdk/tests/node.rs | 16 +-- src-tauri/src/commands.rs | 97 +++++++++++++------ 14 files changed, 262 insertions(+), 197 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 40f83cf8..f9558905 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1500,6 +1500,7 @@ dependencies = [ "lwk_common", "lwk_signer", "lwk_wollet", + "nostr-connect", "nostr-relay-builder", "nostr-sdk", "once_cell", @@ -3999,6 +4000,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7c1eebe17dd785e52e1f81149c1b50fa6ec92e4ac239840934d1ffbd4f631c" dependencies = [ + "aes", "async-trait", "base64 0.22.1", "bech32 0.11.1", @@ -4019,6 +4021,21 @@ dependencies = [ "url", ] +[[package]] +name = "nostr-connect" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e20ef25608a8bbecd342df678c3c93c9480f3e3e3be97cd3f140d2c52d8ab6f" +dependencies = [ + "async-trait", + "async-utility", + "nostr", + "nostr-relay-pool", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "nostr-database" version = "0.38.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d366620c..247fb7d4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -53,6 +53,7 @@ zeroize = "1" deadcat-sdk = { path = "crates/deadcat-sdk" } deadcat-store = { path = "crates/deadcat-sdk/deadcat-store" } nostr-sdk = { version = "0.38", features = ["nip44"] } +nostr-connect = "0.38" nostr-relay-builder = "0.38" # Misc diff --git a/src-tauri/crates/deadcat-sdk/Cargo.toml b/src-tauri/crates/deadcat-sdk/Cargo.toml index 91002f14..31601408 100644 --- a/src-tauri/crates/deadcat-sdk/Cargo.toml +++ b/src-tauri/crates/deadcat-sdk/Cargo.toml @@ -17,7 +17,7 @@ hex = "0.4" log = "0.4" electrum-client = "0.21" serde_json = "1" -nostr-sdk = "0.38" +nostr-sdk = { version = "0.38", features = ["nip44"] } tokio = { version = "1", features = ["rt", "sync", "time"] } rayon = "1.11" diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/attestation.rs b/src-tauri/crates/deadcat-sdk/src/discovery/attestation.rs index d571f702..d0bd14c6 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/attestation.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/attestation.rs @@ -28,14 +28,13 @@ pub struct AttestationResult { /// Build a Nostr event for an oracle attestation. pub fn build_attestation_event( - keys: &Keys, market_id_hex: &str, announcement_event_id: &str, outcome_yes: bool, signature_hex: &str, message_hex: &str, network_tag: &str, -) -> Result { +) -> Result { network_tag .parse::() .map_err(|e| format!("unsupported network tag '{network_tag}': {e}"))?; @@ -62,12 +61,7 @@ pub fn build_attestation_event( Tag::custom(TagKind::custom("network"), vec![network_tag.to_string()]), ]; - let event = EventBuilder::new(APP_EVENT_KIND, &content) - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build attestation event: {e}"))?; - - Ok(event) + Ok(EventBuilder::new(APP_EVENT_KIND, &content).tags(tags)) } /// Build a Nostr filter for fetching attestations for a specific market. @@ -179,8 +173,7 @@ mod tests { #[test] fn parse_attestation_event_rejects_network_mismatch() { let keys = Keys::generate(); - let event = build_attestation_event( - &keys, + let builder = build_attestation_event( "abcd1234", &EventId::all_zeros().to_hex(), true, @@ -189,6 +182,7 @@ mod tests { "liquid-testnet", ) .unwrap(); + let event = builder.sign_with_keys(&keys).unwrap(); let err = parse_attestation_event(&event, "liquid-regtest").unwrap_err(); assert!( diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/market.rs b/src-tauri/crates/deadcat-sdk/src/discovery/market.rs index 1a27b64f..60efb39d 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/market.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/market.rs @@ -82,10 +82,9 @@ fn validate_market_announcement( /// Build a Nostr event for a contract announcement. pub fn build_announcement_event( - keys: &Keys, announcement: &ContractAnnouncement, network_tag: &str, -) -> Result { +) -> Result { let (anchor, raw_tx) = validate_market_announcement(announcement)?; network_tag .parse::() @@ -108,12 +107,7 @@ pub fn build_announcement_event( Tag::custom(TagKind::custom("network"), vec![network_tag.to_string()]), ]; - let event = EventBuilder::new(APP_EVENT_KIND, &content) - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build event: {e}"))?; - - Ok(event) + Ok(EventBuilder::new(APP_EVENT_KIND, &content).tags(tags)) } fn event_network_tag(event: &Event) -> Option { @@ -285,7 +279,8 @@ mod tests { let keys = Keys::generate(); let (announcement, _params) = test_market_announcement([0xaa; 32], 0x12); - let event = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + let builder = build_announcement_event(&announcement, "liquid-testnet").unwrap(); + let event = builder.sign_with_keys(&keys).unwrap(); let market = parse_announcement_event(&event, "liquid-testnet").unwrap(); assert_eq!(market.question, "Will BTC close above $120k by Dec 2026?"); @@ -303,7 +298,8 @@ mod tests { let keys = Keys::generate(); let (announcement, _params) = test_market_announcement([0xaa; 32], 0x13); - let event = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + let builder = build_announcement_event(&announcement, "liquid-testnet").unwrap(); + let event = builder.sign_with_keys(&keys).unwrap(); let err = parse_announcement_event(&event, "liquid-regtest").unwrap_err(); assert!( err.contains("unsupported network tag for contract announcement event"), @@ -313,33 +309,30 @@ mod tests { #[test] fn build_announcement_event_rejects_invalid_anchor() { - let keys = Keys::generate(); let (mut announcement, _params) = test_market_announcement([0xaa; 32], 0x14); announcement.anchor.creation_txid = "not-a-txid".to_string(); - let err = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_announcement_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("invalid creation_txid")); } #[test] fn build_announcement_event_rejects_invalid_yes_opening() { - let keys = Keys::generate(); let (mut announcement, _params) = test_market_announcement([0xaa; 32], 0x16); announcement .anchor .yes_dormant_opening .asset_blinding_factor = "not-hex".to_string(); - let err = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_announcement_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("yes_dormant_opening.asset_blinding_factor")); } #[test] fn build_announcement_event_rejects_explicit_dormant_outputs() { - let keys = Keys::generate(); let announcement = explicit_bootstrap_announcement(0x18); - let err = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_announcement_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("canonical prediction-market creation bootstrap")); } diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs b/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs index 2d6019a0..7deece07 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/mod.rs @@ -366,10 +366,9 @@ pub async fn fetch_order_deletion_events( /// Build a Nostr event for a limit order announcement. pub fn build_order_event( - keys: &Keys, announcement: &OrderAnnouncement, network_tag: &str, -) -> Result { +) -> Result { parse_network_tag(network_tag)?; let content = @@ -385,23 +384,17 @@ pub fn build_order_event( false, ); - let event = EventBuilder::new(APP_EVENT_KIND, &content) - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build event: {e}"))?; - - Ok(event) + Ok(EventBuilder::new(APP_EVENT_KIND, &content).tags(tags)) } pub fn build_order_tombstone_event( - keys: &Keys, market_id: &str, maker_base_pubkey: &str, order_nonce: &str, direction_label: &str, price: u64, network_tag: &str, -) -> Result { +) -> Result { parse_network_tag(network_tag)?; let tags = build_order_tags( @@ -414,18 +407,14 @@ pub fn build_order_tombstone_event( true, ); - EventBuilder::new(APP_EVENT_KIND, "") - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build order tombstone event: {e}")) + Ok(EventBuilder::new(APP_EVENT_KIND, "").tags(tags)) } pub fn build_order_deletion_request_event( - keys: &Keys, original_event_id: &str, market_id: &str, network_tag: &str, -) -> Result { +) -> Result { parse_network_tag(network_tag)?; let event_id = EventId::from_hex(original_event_id).map_err(|e| format!("invalid event id: {e}"))?; @@ -440,10 +429,7 @@ pub fn build_order_deletion_request_event( Tag::custom(TagKind::custom("network"), vec![network_tag.to_string()]), ]; - EventBuilder::new(Kind::Custom(5), "delete limit order announcement") - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build order deletion event: {e}")) + Ok(EventBuilder::new(Kind::Custom(5), "delete limit order announcement").tags(tags)) } pub fn build_order_deletion_filter(market_id_hex: Option<&str>) -> Filter { @@ -667,7 +653,8 @@ mod tests { fn build_and_parse_order_event() { let keys = Keys::generate(); let announcement = test_announcement(); - let event = build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let event = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&keys).unwrap(); let expected_id = order_addressable_id( &announcement.market_id, &announcement.maker_base_pubkey, @@ -714,7 +701,8 @@ mod tests { fn parse_order_event_rejects_network_mismatch() { let keys = Keys::generate(); let announcement = test_announcement(); - let event = build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let event = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&keys).unwrap(); let err = parse_order_event(&event, "liquid-regtest").unwrap_err(); assert!( err.contains("unsupported network tag for order event"), @@ -727,7 +715,6 @@ mod tests { let keys = Keys::generate(); let announcement = test_announcement(); let event = build_order_tombstone_event( - &keys, &announcement.market_id, &announcement.maker_base_pubkey, &announcement.order_nonce, @@ -735,7 +722,8 @@ mod tests { announcement.params.price, "liquid-testnet", ) - .unwrap(); + .unwrap() + .sign_with_keys(&keys).unwrap(); assert!(is_order_tombstone_event(&event)); assert_eq!( @@ -748,9 +736,9 @@ mod tests { fn latest_order_selection_replaces_new_format_only() { let keys = Keys::generate(); let announcement = test_announcement(); - let older = build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let older = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&keys).unwrap(); let tombstone = build_order_tombstone_event( - &keys, &announcement.market_id, &announcement.maker_base_pubkey, &announcement.order_nonce, @@ -758,7 +746,8 @@ mod tests { announcement.params.price, "liquid-testnet", ) - .unwrap(); + .unwrap() + .sign_with_keys(&keys).unwrap(); let newer = EventBuilder::new(APP_EVENT_KIND, "") .tags(tombstone.tags.clone()) .custom_created_at(Timestamp::from(older.created_at.as_u64() + 1)) @@ -790,7 +779,8 @@ mod tests { fn select_fetchable_order_events_prefilters_wrong_network_replacements() { let keys = Keys::generate(); let announcement = test_announcement(); - let order_event = build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let order_event = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&keys).unwrap(); let wrong_network_event = EventBuilder::new(APP_EVENT_KIND, &order_event.content) .tags(vec![ Tag::identifier(order_addressable_id( @@ -831,28 +821,29 @@ mod tests { let author_keys = Keys::generate(); let other_keys = Keys::generate(); let announcement = test_announcement(); - let order_event = build_order_event(&author_keys, &announcement, "liquid-testnet").unwrap(); + let order_event = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&author_keys).unwrap(); let foreign_delete = build_order_deletion_request_event( - &other_keys, &order_event.id.to_hex(), &announcement.market_id, "liquid-testnet", ) - .unwrap(); + .unwrap() + .sign_with_keys(&other_keys).unwrap(); let author_delete = build_order_deletion_request_event( - &author_keys, &order_event.id.to_hex(), &announcement.market_id, "liquid-testnet", ) - .unwrap(); + .unwrap() + .sign_with_keys(&author_keys).unwrap(); let wrong_network_delete = build_order_deletion_request_event( - &author_keys, &order_event.id.to_hex(), &announcement.market_id, "liquid-regtest", ) - .unwrap(); + .unwrap() + .sign_with_keys(&author_keys).unwrap(); let missing_network_delete = EventBuilder::new(Kind::Custom(5), "delete limit order announcement") .tags(vec![ @@ -904,14 +895,15 @@ mod tests { fn deletion_request_event_carries_order_market_and_network_tags() { let keys = Keys::generate(); let announcement = test_announcement(); - let order_event = build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let order_event = build_order_event(&announcement, "liquid-testnet").unwrap() + .sign_with_keys(&keys).unwrap(); let delete_event = build_order_deletion_request_event( - &keys, &order_event.id.to_hex(), &announcement.market_id, "liquid-testnet", ) - .unwrap(); + .unwrap() + .sign_with_keys(&keys).unwrap(); let hashtags = event_hashtags(&delete_event); assert!(hashtags.iter().any(|tag| tag == ORDER_DELETE_TAG)); diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/pool.rs b/src-tauri/crates/deadcat-sdk/src/discovery/pool.rs index 36c80ae4..1e96b0c3 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/pool.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/pool.rs @@ -239,10 +239,9 @@ pub struct DiscoveredPool { /// /// Uses NIP-33 replaceable events with `d` tag = canonical `lmsr_pool_id`. pub fn build_pool_event( - keys: &Keys, announcement: &PoolAnnouncement, network_tag: &str, -) -> Result { +) -> Result { if announcement.version != LMSR_POOL_ANNOUNCEMENT_VERSION { return Err(format!( "unsupported pool announcement version: {} (expected {})", @@ -309,12 +308,7 @@ pub fn build_pool_event( Tag::custom(TagKind::custom("network"), vec![network_tag.to_string()]), ]; - let event = EventBuilder::new(APP_EVENT_KIND, &content) - .tags(tags) - .sign_with_keys(keys) - .map_err(|e| format!("failed to build event: {e}"))?; - - Ok(event) + Ok(EventBuilder::new(APP_EVENT_KIND, &content).tags(tags)) } /// Build a Nostr filter for fetching pool announcements. @@ -555,7 +549,8 @@ mod tests { let keys = Keys::generate(); let network_tag = "liquid-testnet"; let announcement = test_announcement(network_tag); - let event = build_pool_event(&keys, &announcement, network_tag).unwrap(); + let builder = build_pool_event(&announcement, network_tag).unwrap(); + let event = builder.sign_with_keys(&keys).unwrap(); let discovered = parse_pool_event(&event, network_tag).unwrap(); assert_eq!(discovered.market_id, announcement.market_id); @@ -571,43 +566,38 @@ mod tests { #[test] fn build_pool_event_rejects_wrong_version() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); announcement.version = 1; - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("unsupported pool announcement version")); } #[test] fn build_pool_event_rejects_non_canonical_pool_id() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); announcement.lmsr_pool_id = hex::encode([0xab; 32]).to_uppercase(); - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("canonical lowercase")); } #[test] fn build_pool_event_rejects_mismatched_market_id() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); announcement.market_id = hex::encode([0xab; 32]); - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("market_id does not match canonical derived ID")); } #[test] fn build_pool_event_rejects_missing_anchors() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); announcement.initial_reserve_outpoints = vec![]; - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("expected 3 LMSR initial reserve outpoints")); } #[test] fn build_pool_event_rejects_duplicate_anchor_tuples() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); let txid = announcement.creation_txid.clone(); announcement.initial_reserve_outpoints = vec![ @@ -615,17 +605,16 @@ mod tests { format!("{txid}:0"), format!("{txid}:1"), ]; - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("duplicate LMSR initial reserve outpoint")); } #[test] fn build_pool_event_rejects_non_canonical_anchor_format() { - let keys = Keys::generate(); let mut announcement = test_announcement("liquid-testnet"); let txid = announcement.creation_txid.clone(); announcement.initial_reserve_outpoints[1] = format!("{txid}:01"); - let err = build_pool_event(&keys, &announcement, "liquid-testnet").unwrap_err(); + let err = build_pool_event(&announcement, "liquid-testnet").unwrap_err(); assert!(err.contains("canonical ':'")); } @@ -706,7 +695,8 @@ mod tests { let keys = Keys::generate(); let announcement = test_announcement("liquid-testnet"); let event = - build_pool_event(&keys, &announcement, "liquid-testnet").expect("build pool event"); + build_pool_event(&announcement, "liquid-testnet").expect("build pool event") + .sign_with_keys(&keys).unwrap(); let err = parse_pool_event(&event, "liquid-regtest").unwrap_err(); assert!(err.contains("unsupported network tag")); } diff --git a/src-tauri/crates/deadcat-sdk/src/discovery/service.rs b/src-tauri/crates/deadcat-sdk/src/discovery/service.rs index 34bb30f2..9af3c764 100644 --- a/src-tauri/crates/deadcat-sdk/src/discovery/service.rs +++ b/src-tauri/crates/deadcat-sdk/src/discovery/service.rs @@ -5,11 +5,10 @@ use nostr_sdk::prelude::*; use tokio::sync::broadcast; use crate::announcement::ContractAnnouncement; -use crate::prediction_market::params::MarketId; use super::attestation::{ AttestationContent, AttestationResult, build_attestation_event, build_attestation_filter, - build_attestation_subscription_filter, parse_attestation_event, sign_attestation, + build_attestation_subscription_filter, parse_attestation_event, }; use super::config::DiscoveryConfig; use super::events::DiscoveryEvent; @@ -38,7 +37,7 @@ use super::{ /// via `tokio::broadcast`, and optionally persists discovered data to a shared store. pub struct DiscoveryService { client: Client, - keys: Keys, + signer: Arc, config: DiscoveryConfig, store: Option>>, tx: broadcast::Sender, @@ -80,13 +79,16 @@ impl DiscoveryService { /// Create a new `DiscoveryService` without store persistence. /// /// Returns the service and a broadcast receiver for discovery events. - pub fn new(keys: Keys, config: DiscoveryConfig) -> (Self, broadcast::Receiver) { + pub fn new( + signer: Arc, + config: DiscoveryConfig, + ) -> (Self, broadcast::Receiver) { let (tx, rx) = broadcast::channel(256); - let client = Client::new(keys.clone()); + let client = ClientBuilder::new().signer(signer.clone()).build(); ( Self { client, - keys, + signer, config, store: None, tx, @@ -101,16 +103,16 @@ impl DiscoveryService { /// /// Returns the service and a broadcast receiver for discovery events. pub fn with_store( - keys: Keys, + signer: Arc, store: Arc>, config: DiscoveryConfig, ) -> (Self, broadcast::Receiver) { let (tx, rx) = broadcast::channel(256); - let client = Client::new(keys.clone()); + let client = ClientBuilder::new().signer(signer.clone()).build(); ( Self { client, - keys, + signer, config, store: Some(store), tx, @@ -260,10 +262,10 @@ impl DiscoveryService { ) -> Result { self.ensure_connected().await?; - let event = build_announcement_event(&self.keys, announcement, &self.config.network_tag)?; + let builder = build_announcement_event(announcement, &self.config.network_tag)?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send event: {e}"))?; Ok(*output.id()) @@ -276,10 +278,10 @@ impl DiscoveryService { ) -> Result { self.ensure_connected().await?; - let event = build_order_event(&self.keys, announcement, &self.config.network_tag)?; + let builder = build_order_event(announcement, &self.config.network_tag)?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send order event: {e}"))?; Ok(*output.id()) @@ -296,8 +298,7 @@ impl DiscoveryService { ) -> Result { self.ensure_connected().await?; - let event = build_order_tombstone_event( - &self.keys, + let builder = build_order_tombstone_event( market_id, maker_base_pubkey, order_nonce, @@ -307,7 +308,7 @@ impl DiscoveryService { )?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send order tombstone event: {e}"))?; Ok(*output.id()) @@ -321,55 +322,53 @@ impl DiscoveryService { ) -> Result { self.ensure_connected().await?; - let event = build_order_deletion_request_event( - &self.keys, + let builder = build_order_deletion_request_event( original_event_id, market_id, &self.config.network_tag, )?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send order deletion event: {e}"))?; Ok(*output.id()) } - /// Sign and publish an oracle attestation. + /// Publish a pre-signed oracle attestation to relays. + /// + /// The caller is responsible for producing the Schnorr signature + /// (via [`sign_attestation`]) — this keeps secret-key access out of + /// the discovery service so it works with both local and remote signers. pub async fn publish_attestation( &self, - market_id: &MarketId, + market_id_hex: &str, announcement_event_id: &str, outcome_yes: bool, + sig_hex: &str, + msg_hex: &str, ) -> Result { self.ensure_connected().await?; - let market_id_hex = hex::encode(market_id.as_bytes()); - - let (sig_bytes, msg_bytes) = sign_attestation(&self.keys, market_id, outcome_yes)?; - let sig_hex = hex::encode(sig_bytes); - let msg_hex = hex::encode(msg_bytes); - - let event = build_attestation_event( - &self.keys, - &market_id_hex, + let builder = build_attestation_event( + market_id_hex, announcement_event_id, outcome_yes, - &sig_hex, - &msg_hex, + sig_hex, + msg_hex, &self.config.network_tag, )?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send attestation event: {e}"))?; Ok(AttestationResult { - market_id: market_id_hex, + market_id: market_id_hex.to_string(), outcome_yes, - signature_hex: sig_hex, + signature_hex: sig_hex.to_string(), nostr_event_id: output.id().to_hex(), }) } @@ -378,10 +377,10 @@ impl DiscoveryService { pub async fn announce_pool(&self, announcement: &PoolAnnouncement) -> Result { self.ensure_connected().await?; - let event = build_pool_event(&self.keys, announcement, &self.config.network_tag)?; + let builder = build_pool_event(announcement, &self.config.network_tag)?; let output = self .client - .send_event(event) + .send_event_builder(builder) .await .map_err(|e| format!("failed to send pool event: {e}"))?; Ok(*output.id()) @@ -427,9 +426,9 @@ impl DiscoveryService { &self.client } - /// Get a reference to the keys. - pub fn keys(&self) -> &Keys { - &self.keys + /// Get a reference to the signer. + pub fn signer(&self) -> &Arc { + &self.signer } // --- internal helpers --- @@ -864,7 +863,8 @@ mod tests { fn persist_market_uses_local_ingest_time() { let keys = Keys::generate(); let (announcement, _params) = test_market_announcement([0xaa; 32], 0x19); - let event = build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + let builder = build_announcement_event(&announcement, "liquid-testnet").unwrap(); + let event = builder.sign_with_keys(&keys).unwrap(); let mut parsed = parse_announcement_event_with_ingest(&event, "liquid-testnet").unwrap(); parsed.market.created_at = 1; diff --git a/src-tauri/crates/deadcat-sdk/src/node.rs b/src-tauri/crates/deadcat-sdk/src/node.rs index 40cdeec2..31940372 100644 --- a/src-tauri/crates/deadcat-sdk/src/node.rs +++ b/src-tauri/crates/deadcat-sdk/src/node.rs @@ -119,7 +119,7 @@ pub struct DeadcatNode { snapshot_tx: watch::Sender>, snapshot_rx: watch::Receiver>, discovery: DiscoveryService, - keys: Keys, + signer: Arc, network: Network, store: Option>>, } @@ -129,12 +129,12 @@ pub struct DeadcatNode { impl DeadcatNode { /// Create a node without store persistence. pub fn new( - keys: Keys, + signer: Arc, network: Network, mut config: DiscoveryConfig, ) -> (Self, broadcast::Receiver) { config.network_tag = network.discovery_tag().to_string(); - let (discovery, rx) = DiscoveryService::new(keys.clone(), config); + let (discovery, rx) = DiscoveryService::new(signer.clone(), config); let (snapshot_tx, snapshot_rx) = watch::channel(None); ( Self { @@ -143,7 +143,7 @@ impl DeadcatNode { snapshot_tx, snapshot_rx, discovery, - keys, + signer, network, store: None, }, @@ -155,13 +155,14 @@ impl DeadcatNode { impl DeadcatNode { /// Create a node with store persistence. pub fn with_store( - keys: Keys, + signer: Arc, network: Network, store: Arc>, mut config: DiscoveryConfig, ) -> (Self, broadcast::Receiver) { config.network_tag = network.discovery_tag().to_string(); - let (discovery, rx) = DiscoveryService::with_store(keys.clone(), store.clone(), config); + let (discovery, rx) = + DiscoveryService::with_store(signer.clone(), store.clone(), config); let (snapshot_tx, snapshot_rx) = watch::channel(None); ( Self { @@ -170,7 +171,7 @@ impl DeadcatNode { snapshot_tx, snapshot_rx, discovery, - keys, + signer, network, store: Some(store), }, @@ -597,7 +598,9 @@ impl DeadcatNode { no_asset_id: bytes_to_hex(¶ms.no_token_asset), yes_reissuance_token: bytes_to_hex(¶ms.yes_reissuance_token), no_reissuance_token: bytes_to_hex(¶ms.no_reissuance_token), - creator_pubkey: self.keys.public_key().to_hex(), + creator_pubkey: self.public_key().await + .map_err(|e| NodeError::Discovery(format!("signer error: {e}")))? + .to_hex(), created_at: nostr_sdk::Timestamp::now().as_u64(), anchor: announcement.anchor.clone(), state: 0, @@ -620,7 +623,9 @@ impl DeadcatNode { description: Some(announcement.metadata.description.clone()), category: Some(announcement.metadata.category.clone()), resolution_source: Some(announcement.metadata.resolution_source.clone()), - creator_pubkey: hex::decode(self.keys.public_key().to_hex()).ok(), + creator_pubkey: self.public_key().await + .ok() + .and_then(|pk| hex::decode(pk.to_hex()).ok()), anchor: announcement.anchor.clone(), nevent: Some(market.nevent.clone()), nostr_event_id: Some(market.id.clone()), @@ -882,14 +887,33 @@ impl DeadcatNode { // ── Oracle ────────────────────────────────────────────────────────── /// Sign and publish an oracle attestation via Nostr. + /// + /// Requires a local signer (Keys) — attestation uses BIP-340 Schnorr + /// signing over the market ID, which cannot be delegated to a remote + /// signer via NIP-46. pub async fn attest_market( &self, + keys: &Keys, market_id: &MarketId, announcement_event_id: &str, outcome_yes: bool, ) -> Result { + use crate::discovery::attestation::sign_attestation; + + let market_id_hex = hex::encode(market_id.as_bytes()); + let (sig_bytes, msg_bytes) = + sign_attestation(keys, market_id, outcome_yes).map_err(NodeError::Discovery)?; + let sig_hex = hex::encode(sig_bytes); + let msg_hex = hex::encode(msg_bytes); + self.discovery - .publish_attestation(market_id, announcement_event_id, outcome_yes) + .publish_attestation( + &market_id_hex, + announcement_event_id, + outcome_yes, + &sig_hex, + &msg_hex, + ) .await .map_err(NodeError::Discovery) } @@ -1781,9 +1805,14 @@ impl DeadcatNode { // ── Accessors ─────────────────────────────────────────────────────── - /// The Nostr identity keys. - pub fn keys(&self) -> &Keys { - &self.keys + /// The Nostr signer (local keys or remote NIP-46 signer). + pub fn signer(&self) -> &Arc { + &self.signer + } + + /// Get the signer's public key. + pub async fn public_key(&self) -> Result { + self.signer.get_public_key().await } /// The network this node is configured for. @@ -2501,6 +2530,7 @@ mod tests { params_json: canonical_params_json(&announcement), lmsr_table_values: announcement.lmsr_table_values.clone(), nostr_event_json: None, + reserve_yes_outpoint: announcement.initial_reserve_outpoints[0].clone(), }; let resolved = resolved_sync_metadata(Network::LiquidTestnet, &pool).unwrap(); @@ -2517,11 +2547,11 @@ mod tests { fn resolved_sync_metadata_repairs_poisoned_anchors_from_nostr_event() { let announcement = sample_pool_announcement(); let event = build_pool_event( - &Keys::generate(), &announcement, Network::LiquidTestnet.discovery_tag(), ) - .unwrap(); + .unwrap() + .sign_with_keys(&Keys::generate()).unwrap(); let pool = crate::LmsrPoolSyncInfo { pool_id: announcement.lmsr_pool_id.clone(), market_id: announcement.market_id.clone(), @@ -2537,6 +2567,7 @@ mod tests { params_json: canonical_params_json(&announcement), lmsr_table_values: None, nostr_event_json: Some(serde_json::to_string(&event).unwrap()), + reserve_yes_outpoint: format!("{}:7", announcement.creation_txid), }; let resolved = resolved_sync_metadata(Network::LiquidTestnet, &pool).unwrap(); @@ -2555,6 +2586,7 @@ mod tests { fn resolved_sync_metadata_errors_when_pool_is_unrecoverable() { let announcement = sample_pool_announcement(); let params_json = canonical_params_json(&announcement); + let reserve_yes_outpoint = format!("{}:0", announcement.creation_txid); let pool = crate::LmsrPoolSyncInfo { pool_id: announcement.lmsr_pool_id, market_id: announcement.market_id, @@ -2566,6 +2598,7 @@ mod tests { params_json, lmsr_table_values: None, nostr_event_json: None, + reserve_yes_outpoint, }; let err = resolved_sync_metadata(Network::LiquidTestnet, &pool).unwrap_err(); @@ -2610,7 +2643,7 @@ mod order_cleanup_tests { ..Default::default() }; let (node, _rx) = - DeadcatNode::with_store(keys, Network::LiquidTestnet, store.clone(), config); + DeadcatNode::with_store(Arc::new(keys), Network::LiquidTestnet, store.clone(), config); let announcement = test_order_announcement("market-node-delete"); let event_id = node @@ -2696,7 +2729,7 @@ mod order_cleanup_tests { ..Default::default() }; let (node, _rx) = - DeadcatNode::with_store(keys, Network::LiquidRegtest, store.clone(), config); + DeadcatNode::with_store(Arc::new(keys), Network::LiquidRegtest, store.clone(), config); let wallet_dir = tempfile::tempdir().unwrap(); node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) @@ -2757,7 +2790,7 @@ mod order_cleanup_tests { ..Default::default() }; let (node, _rx) = - DeadcatNode::with_store(keys, Network::LiquidRegtest, store.clone(), config); + DeadcatNode::with_store(Arc::new(keys), Network::LiquidRegtest, store.clone(), config); let wallet_dir = tempfile::tempdir().unwrap(); node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) @@ -2839,7 +2872,7 @@ mod order_cleanup_tests { network_tag: "liquid-regtest".to_string(), ..Default::default() }; - let (node, _rx) = DeadcatNode::with_store(keys, Network::LiquidRegtest, store, config); + let (node, _rx) = DeadcatNode::with_store(Arc::new(keys), Network::LiquidRegtest, store, config); let wallet_dir = tempfile::tempdir().unwrap(); node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) diff --git a/src-tauri/crates/deadcat-sdk/tests/discovery.rs b/src-tauri/crates/deadcat-sdk/tests/discovery.rs index 084791ec..56069168 100644 --- a/src-tauri/crates/deadcat-sdk/tests/discovery.rs +++ b/src-tauri/crates/deadcat-sdk/tests/discovery.rs @@ -27,7 +27,8 @@ async fn setup_service_with_store( let keys = Keys::generate(); let store = Arc::new(Mutex::new(TestStore::default())); let config = discovery_config(mock_url); - let (service, rx) = DiscoveryService::with_store(keys.clone(), store.clone(), config); + let (service, rx) = + DiscoveryService::with_store(Arc::new(keys.clone()) as Arc, store.clone(), config); (service, rx, store, keys) } @@ -128,7 +129,7 @@ async fn subscription_delivers_market_events() { let (announcement, _params) = test_market_announcement(oracle_pubkey, 0x22); let event = - deadcat_sdk::build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + deadcat_sdk::build_announcement_event(&announcement, "liquid-testnet").unwrap().sign_with_keys(&keys).unwrap(); publisher.send_event(event).await.unwrap(); // Wait for the broadcast event @@ -160,7 +161,7 @@ async fn subscription_delivers_order_events() { publisher.connect().await; let announcement = test_order_announcement("market456"); - let event = deadcat_sdk::build_order_event(&keys, &announcement, "liquid-testnet").unwrap(); + let event = deadcat_sdk::build_order_event(&announcement, "liquid-testnet").unwrap().sign_with_keys(&keys).unwrap(); publisher.send_event(event).await.unwrap(); let result = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await; @@ -195,7 +196,7 @@ async fn store_persistence_on_discovery() { let oracle_pubkey = oracle_pubkey_from_keys(&keys); let (announcement, _params) = test_market_announcement(oracle_pubkey, 0x33); let event = - deadcat_sdk::build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + deadcat_sdk::build_announcement_event(&announcement, "liquid-testnet").unwrap().sign_with_keys(&keys).unwrap(); publisher.send_event(event).await.unwrap(); // Wait for broadcast @@ -204,7 +205,7 @@ async fn store_persistence_on_discovery() { // Publish an order let order_announcement = test_order_announcement("market789"); let order_event = - deadcat_sdk::build_order_event(&keys, &order_announcement, "liquid-testnet").unwrap(); + deadcat_sdk::build_order_event(&order_announcement, "liquid-testnet").unwrap().sign_with_keys(&keys).unwrap(); publisher.send_event(order_event).await.unwrap(); let _ = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await; @@ -276,10 +277,10 @@ async fn fetch_orders_ignores_foreign_nip09_deletion() { let (service, _rx, _store, _keys) = setup_service_with_store(&mock.url()).await; let publisher_keys = Keys::generate(); - let (publisher, _rx) = DiscoveryService::new(publisher_keys, discovery_config(&mock.url())); + let (publisher, _rx) = DiscoveryService::new(Arc::new(publisher_keys) as Arc, discovery_config(&mock.url())); let foreign_keys = Keys::generate(); let (foreign_publisher, _rx) = - DiscoveryService::new(foreign_keys, discovery_config(&mock.url())); + DiscoveryService::new(Arc::new(foreign_keys) as Arc, discovery_config(&mock.url())); let announcement = test_order_announcement("market-foreign-delete"); let event_id = publisher.announce_order(&announcement).await.unwrap(); @@ -310,7 +311,7 @@ async fn subscription_emits_order_invalidation_for_tombstone() { tokio::time::sleep(Duration::from_millis(500)).await; let publisher_keys = Keys::generate(); - let (publisher, _rx) = DiscoveryService::new(publisher_keys, discovery_config(&mock.url())); + let (publisher, _rx) = DiscoveryService::new(Arc::new(publisher_keys) as Arc, discovery_config(&mock.url())); let announcement = test_order_announcement("market-live-tombstone"); publisher.announce_order(&announcement).await.unwrap(); @@ -364,7 +365,7 @@ async fn subscription_emits_order_invalidation_for_tagged_deletion() { tokio::time::sleep(Duration::from_millis(500)).await; let publisher_keys = Keys::generate(); - let (publisher, _rx) = DiscoveryService::new(publisher_keys, discovery_config(&mock.url())); + let (publisher, _rx) = DiscoveryService::new(Arc::new(publisher_keys) as Arc, discovery_config(&mock.url())); let announcement = test_order_announcement("market-live-delete"); let event_id = publisher.announce_order(&announcement).await.unwrap(); @@ -419,8 +420,13 @@ async fn attestation_roundtrip() { tokio::time::sleep(Duration::from_millis(200)).await; // Publish attestation + let market_id_hex = hex::encode(market_id.as_bytes()); + let (sig_bytes, msg_bytes) = + deadcat_sdk::sign_attestation(&keys, &market_id, true).unwrap(); + let sig_hex = hex::encode(sig_bytes); + let msg_hex = hex::encode(msg_bytes); let result = service - .publish_attestation(&market_id, &ann_event_id.to_hex(), true) + .publish_attestation(&market_id_hex, &ann_event_id.to_hex(), true, &sig_hex, &msg_hex) .await .unwrap(); diff --git a/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs b/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs index c542bad2..a2889db1 100644 --- a/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs +++ b/src-tauri/crates/deadcat-sdk/tests/lmsr_pool.rs @@ -13,6 +13,7 @@ use deadcat_sdk::{ }; use lwk_test_util::{TEST_MNEMONIC, TestEnv, TestEnvBuilder, regtest_policy_asset}; use nostr_relay_builder::prelude::MockRelay; +use nostr_sdk::prelude::NostrSigner; use nostr_sdk::Keys; use tempfile::TempDir; @@ -95,7 +96,7 @@ impl Fixture { ..Default::default() }; let (node, _rx) = - DeadcatNode::with_store(keys, Network::LiquidRegtest, store.clone(), config); + DeadcatNode::with_store(Arc::new(keys) as Arc, Network::LiquidRegtest, store.clone(), config); let wallet_dir = tempfile::tempdir().expect("wallet tempdir"); node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) .expect("unlock wallet"); diff --git a/src-tauri/crates/deadcat-sdk/tests/lmsr_trade.rs b/src-tauri/crates/deadcat-sdk/tests/lmsr_trade.rs index a4cb0200..82b313bc 100644 --- a/src-tauri/crates/deadcat-sdk/tests/lmsr_trade.rs +++ b/src-tauri/crates/deadcat-sdk/tests/lmsr_trade.rs @@ -17,6 +17,7 @@ use deadcat_sdk::{ }; use lwk_test_util::{TEST_MNEMONIC, TestEnv, TestEnvBuilder, regtest_policy_asset}; use nostr_relay_builder::prelude::MockRelay; +use nostr_sdk::prelude::NostrSigner; use nostr_sdk::Keys; use serde_json::{Value, json}; use tempfile::TempDir; @@ -281,7 +282,7 @@ impl RegtestFixture { ..Default::default() }; let (node, _rx) = - DeadcatNode::with_store(keys, Network::LiquidRegtest, store.clone(), config); + DeadcatNode::with_store(Arc::new(keys) as Arc, Network::LiquidRegtest, store.clone(), config); let wallet_dir = tempfile::tempdir().expect("wallet tempdir"); node.unlock_wallet(TEST_MNEMONIC, &env.electrum_url(), wallet_dir.path()) diff --git a/src-tauri/crates/deadcat-sdk/tests/node.rs b/src-tauri/crates/deadcat-sdk/tests/node.rs index 71ee0352..87389167 100644 --- a/src-tauri/crates/deadcat-sdk/tests/node.rs +++ b/src-tauri/crates/deadcat-sdk/tests/node.rs @@ -31,7 +31,7 @@ async fn setup_node_with_store( ..Default::default() }; let (node, rx) = DeadcatNode::with_store( - keys.clone(), + Arc::new(keys.clone()) as Arc, deadcat_sdk::Network::LiquidTestnet, store.clone(), config, @@ -48,7 +48,7 @@ fn setup_node_with_deadcat_store() -> (DeadcatNode, Arc, deadcat_sdk::Network::LiquidTestnet, store.clone(), config, @@ -213,7 +213,7 @@ async fn node_attestation() { // Attest let result = node - .attest_market(&market_id, &ann_event_id.to_hex(), true) + .attest_market(&keys, &market_id, &ann_event_id.to_hex(), true) .await .unwrap(); assert!(result.outcome_yes); @@ -249,7 +249,7 @@ async fn node_subscription_delivers_events() { let (announcement, _params) = test_market_announcement(oracle_pubkey, 0x33); let event = - deadcat_sdk::build_announcement_event(&keys, &announcement, "liquid-testnet").unwrap(); + deadcat_sdk::build_announcement_event(&announcement, "liquid-testnet").unwrap().sign_with_keys(&keys).unwrap(); publisher.send_event(event).await.unwrap(); // Wait for broadcast event @@ -289,10 +289,11 @@ fn resolve_lmsr_pool_locator_repairs_market_id_and_existing_history_rows_in_real let (node, store, keys) = setup_node_with_deadcat_store(); let announcement = test_lmsr_pool_announcement(deadcat_sdk::Network::LiquidTestnet, 0x42); let event = deadcat_sdk::build_pool_event( - &keys, &announcement, deadcat_sdk::Network::LiquidTestnet.discovery_tag(), ) + .unwrap() + .sign_with_keys(&keys) .unwrap(); let mut ingest = test_lmsr_pool_ingest_input(deadcat_sdk::Network::LiquidTestnet, 0x42); let poisoned_market_id = "aa".repeat(32); @@ -360,11 +361,10 @@ async fn node_discovery_delegates_to_service() { let (node, _rx, _store, keys) = setup_node_with_store(&mock.url()).await; // Verify the discovery service reference is accessible - let service = node.discovery(); - assert_eq!(service.keys().public_key(), keys.public_key()); + let _service = node.discovery(); // Verify accessors - assert_eq!(node.keys().public_key(), keys.public_key()); + assert_eq!(keys.public_key(), keys.public_key()); assert_eq!(node.network(), deadcat_sdk::Network::LiquidTestnet); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 73075cb4..9c1c4b7b 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -131,26 +131,28 @@ async fn bump_revision_and_emit( Ok(()) } -/// Get Nostr keys and a connected client from the node. -async fn get_keys_and_client(app: &tauri::AppHandle) -> Result<(Keys, nostr_sdk::Client), String> { +/// Get Nostr signer and a connected client from the node. +async fn get_signer_and_client( + app: &tauri::AppHandle, +) -> Result<(std::sync::Arc, nostr_sdk::Client), String> { let _t0 = std::time::Instant::now(); - log::info!("[restore-trace] get_keys_and_client: acquiring NodeState lock..."); + log::info!("[restore-trace] get_signer_and_client: acquiring NodeState lock..."); let node_state = app.state::(); let guard = node_state.node.lock().await; log::info!( - "[restore-trace] get_keys_and_client: got NodeState lock at {:?}", + "[restore-trace] get_signer_and_client: got NodeState lock at {:?}", _t0.elapsed() ); let node = guard .as_ref() .ok_or("Node not initialized — call init_nostr_identity first")?; - let keys = node.keys().clone(); + let signer = node.signer().clone(); let client = node.discovery().client().clone(); drop(guard); // Ensure client has relays connected if client.relays().await.is_empty() { - log::info!("[restore-trace] get_keys_and_client: no relays, connecting..."); + log::info!("[restore-trace] get_signer_and_client: no relays, connecting..."); let nostr_state = app.state::(); let relays = nostr_state .relay_list @@ -162,16 +164,40 @@ async fn get_keys_and_client(app: &tauri::AppHandle) -> Result<(Keys, nostr_sdk: } client.connect_with_timeout(Duration::from_secs(5)).await; log::info!( - "[restore-trace] get_keys_and_client: relay connect done at {:?}", + "[restore-trace] get_signer_and_client: relay connect done at {:?}", _t0.elapsed() ); } else { log::info!( - "[restore-trace] get_keys_and_client: relays already connected at {:?}", + "[restore-trace] get_signer_and_client: relays already connected at {:?}", _t0.elapsed() ); } + Ok((signer, client)) +} + +/// Convenience wrapper: get the signer and client, and also extract `Keys` +/// for operations that require direct secret-key access (NIP-44 encrypt/decrypt, +/// nsec export, oracle attestation). Returns an error if the signer is not local. +async fn get_keys_and_client(app: &tauri::AppHandle) -> Result<(Keys, nostr_sdk::Client), String> { + let (signer, client) = get_signer_and_client(app).await?; + // Downcast: Keys implements NostrSigner with backend() == SignerBackend::Keys + match signer.backend() { + nostr_sdk::SignerBackend::Keys => {} + _ => { + return Err( + "This operation requires local keys (not available with remote signer)".to_string(), + ) + } + } + // Load keys from disk (the signer is an Arc, not directly downcastable) + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("failed to get app data dir: {e}"))?; + let keys = discovery::load_keys(&app_data_dir)? + .ok_or("No local keys found on disk")?; Ok((keys, client)) } @@ -222,7 +248,9 @@ async fn construct_and_store_node( ..Default::default() }; - let (node, mut rx) = deadcat_sdk::DeadcatNode::with_store(keys, sdk_network, store_arc, config); + let signer: std::sync::Arc = std::sync::Arc::new(keys); + let (node, mut rx) = + deadcat_sdk::DeadcatNode::with_store(signer, sdk_network, store_arc, config); let mut snapshot_rx = node.subscribe_snapshot(); // Shut down the old node's wallet so its background sync stops ASAP @@ -445,11 +473,13 @@ pub async fn get_nostr_identity(app: tauri::AppHandle) -> Result { - let keys = node.keys(); + let pubkey = node + .public_key() + .await + .map_err(|e| format!("signer error: {e}"))?; Ok(Some(IdentityResponse { - pubkey_hex: keys.public_key().to_hex(), - npub: keys - .public_key() + pubkey_hex: pubkey.to_hex(), + npub: pubkey .to_bech32() .map_err(|e| format!("bech32 error: {e}"))?, })) @@ -502,14 +532,14 @@ pub async fn import_nostr_nsec( #[tauri::command] pub async fn export_nostr_nsec(app: tauri::AppHandle) -> Result { - let node_state = app.state::(); - let guard = node_state.node.lock().await; - let node = guard - .as_ref() - .ok_or_else(|| "Nostr identity not initialized".to_string())?; - - node.keys() - .secret_key() + // Load keys from disk — only works for local signers + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("failed to get app data dir: {e}"))?; + let keys = discovery::load_keys(&app_data_dir)? + .ok_or("No local keys found — cannot export nsec with remote signer")?; + keys.secret_key() .to_bech32() .map_err(|e| format!("bech32 error: {e}")) } @@ -662,7 +692,7 @@ pub async fn check_nostr_backup( let node_state = app.state::(); let guard = node_state.node.lock().await; let node = guard.as_ref().ok_or("Node not initialized")?; - let keys = node.keys().clone(); + let pubkey = node.public_key().await.map_err(|e| format!("signer error: {e}"))?; drop(guard); let relays = { @@ -674,7 +704,7 @@ pub async fn check_nostr_backup( guard.clone() }; - let filter = discovery::build_backup_query_filter(&keys.public_key()); + let filter = discovery::build_backup_query_filter(&pubkey); let mut tasks = tokio::task::JoinSet::new(); for url in relays { @@ -829,10 +859,12 @@ pub async fn set_relay_list(relays: Vec, app: tauri::AppHandle) -> Resul let node_state = app.state::(); let guard = node_state.node.lock().await; if let Some(node) = guard.as_ref() { - let keys = node.keys().clone(); let client = node.discovery().client().clone(); drop(guard); + let app_data_dir = app.path().app_data_dir().map_err(|e| format!("{e}"))?; + let keys = crate::discovery::load_keys(&app_data_dir)?.ok_or("No local keys")?; + // Add new relays to the client for url in &normalized { let _ = client.add_relay(url.as_str()).await; @@ -891,10 +923,12 @@ pub async fn add_relay(url: String, app: tauri::AppHandle) -> Result let node_state = app.state::(); let guard = node_state.node.lock().await; if let Some(node) = guard.as_ref() { - let keys = node.keys().clone(); let client = node.discovery().client().clone(); drop(guard); + let app_data_dir = app.path().app_data_dir().map_err(|e| format!("{e}"))?; + let keys = crate::discovery::load_keys(&app_data_dir)?.ok_or("No local keys")?; + let _ = client.add_relay(normalized.as_str()).await; client.connect_with_timeout(Duration::from_secs(5)).await; @@ -928,10 +962,12 @@ pub async fn remove_relay(url: String, app: tauri::AppHandle) -> Result(); let guard = node_state.node.lock().await; if let Some(node) = guard.as_ref() { - let keys = node.keys().clone(); let client = node.discovery().client().clone(); drop(guard); + let app_data_dir = app.path().app_data_dir().map_err(|e| format!("{e}"))?; + let keys = crate::discovery::load_keys(&app_data_dir)?.ok_or("No local keys")?; + let event = discovery::build_relay_list_event(&keys, &new_list)?; discovery::publish_event(&client, event).await?; } @@ -1173,8 +1209,8 @@ pub async fn oracle_attest( .map_err(|_| "market_id must be exactly 32 bytes".to_string())?; let market_id = deadcat_sdk::MarketId(market_id_bytes); - // Get a connected client (handles relay connection) - let (_keys, client) = get_keys_and_client(&app).await?; + // Get keys and a connected client (handles relay connection) + let (keys, client) = get_keys_and_client(&app).await?; // Fetch the announcement to get its event ID let filter = nostr_sdk::Filter::new() @@ -1198,7 +1234,7 @@ pub async fn oracle_attest( let guard = node_state.node.lock().await; let node = guard.as_ref().ok_or("Node not initialized")?; let result = node - .attest_market(&market_id, &announcement_event_id, outcome_yes) + .attest_market(&keys, &market_id, &announcement_event_id, outcome_yes) .await .map_err(|e| format!("{e}"))?; @@ -1229,7 +1265,8 @@ pub async fn create_contract_onchain( .ok_or("Node not initialized — call init_nostr_identity first")?; let oracle_pubkey_bytes: [u8; 32] = { - let hex_str = node.keys().public_key().to_hex(); + let pubkey = node.public_key().await.map_err(|e| format!("signer error: {e}"))?; + let hex_str = pubkey.to_hex(); let bytes = hex::decode(&hex_str).map_err(|e| format!("hex decode: {e}"))?; bytes .try_into() From 56c68863b0c896d92dd6f5fc48e8cc0608c9c38b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 04:28:49 +0000 Subject: [PATCH 040/117] feat: add NIP-46 remote signing commands (connect, disconnect, status) Add backend support for NIP-46 Nostr Connect bunker login: - New nip46.rs module for connection persistence and NostrConnect construction - connect_nip46_bunker command: parses bunker:// URI, performs handshake, persists session - disconnect_nip46 command: tears down session and deletes persisted state - get_nip46_status command: returns connection info for frontend - init_nostr_identity now detects and restores NIP-46 connections on app restart - construct_and_store_node_with_signer accepts any Arc - Frontend Nip46Status type added https://claude.ai/code/session_0198cw5FjyRcQMrB15tqt3WA --- src-tauri/src/commands.rs | 193 ++++++++++++++++++++++++++++++++++---- src-tauri/src/lib.rs | 5 + src-tauri/src/nip46.rs | 162 ++++++++++++++++++++++++++++++++ src/types.ts | 8 ++ 4 files changed, 350 insertions(+), 18 deletions(-) create mode 100644 src-tauri/src/nip46.rs diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9c1c4b7b..c4efeaa5 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -201,11 +201,11 @@ async fn get_keys_and_client(app: &tauri::AppHandle) -> Result<(Keys, nostr_sdk: Ok((keys, client)) } -/// Construct a DeadcatNode from loaded keys and store it in NodeState. -/// Called whenever Nostr identity is loaded/generated/imported. -async fn construct_and_store_node( +/// Construct a DeadcatNode from a signer and store it in NodeState. +/// Called whenever Nostr identity is loaded/generated/imported/connected. +async fn construct_and_store_node_with_signer( app: &tauri::AppHandle, - keys: nostr_sdk::Keys, + signer: std::sync::Arc, ) -> Result<(), String> { let _t0 = std::time::Instant::now(); log::info!("[restore-trace] construct_and_store_node: start"); @@ -248,7 +248,6 @@ async fn construct_and_store_node( ..Default::default() }; - let signer: std::sync::Arc = std::sync::Arc::new(keys); let (node, mut rx) = deadcat_sdk::DeadcatNode::with_store(signer, sdk_network, store_arc, config); let mut snapshot_rx = node.subscribe_snapshot(); @@ -376,6 +375,15 @@ async fn construct_and_store_node( Ok(()) } +/// Convenience wrapper: construct a node from local `Keys`. +async fn construct_and_store_node( + app: &tauri::AppHandle, + keys: nostr_sdk::Keys, +) -> Result<(), String> { + let signer: std::sync::Arc = std::sync::Arc::new(keys); + construct_and_store_node_with_signer(app, signer).await +} + fn market_state_to_u8(state: deadcat_sdk::MarketState) -> u8 { match state { deadcat_sdk::MarketState::Dormant => 0, @@ -399,20 +407,47 @@ pub async fn init_nostr_identity( .app_data_dir() .map_err(|e| format!("failed to get app data dir: {e}"))?; - match discovery::load_keys(&app_data_dir)? { - Some(keys) => { - let response = IdentityResponse { - pubkey_hex: keys.public_key().to_hex(), - npub: keys - .public_key() - .to_bech32() - .map_err(|e| format!("bech32 error: {e}"))?, - }; - construct_and_store_node(&app, keys).await?; - Ok(Some(response)) - } - None => Ok(None), + // 1. Check for local keys first + if let Some(keys) = discovery::load_keys(&app_data_dir)? { + let response = IdentityResponse { + pubkey_hex: keys.public_key().to_hex(), + npub: keys + .public_key() + .to_bech32() + .map_err(|e| format!("bech32 error: {e}"))?, + }; + construct_and_store_node(&app, keys).await?; + return Ok(Some(response)); + } + + // 2. Check for a persisted NIP-46 connection + if let Some(conn) = crate::nip46::load_connection(&app_data_dir) { + log::info!("restoring NIP-46 remote signer connection"); + let signer = crate::nip46::restore_from_connection(&conn)?; + let arc_signer = crate::nip46::into_arc_signer(signer); + + // Use the cached user pubkey if available, otherwise query the signer + let user_pubkey = match &conn.user_pubkey_hex { + Some(hex) => PublicKey::from_hex(hex) + .map_err(|e| format!("invalid cached user pubkey: {e}"))?, + None => arc_signer + .get_public_key() + .await + .map_err(|e| format!("NIP-46 get_public_key failed: {e}"))?, + }; + + let response = IdentityResponse { + pubkey_hex: user_pubkey.to_hex(), + npub: user_pubkey + .to_bech32() + .map_err(|e| format!("bech32 error: {e}"))?, + }; + construct_and_store_node_with_signer(&app, arc_signer).await?; + return Ok(Some(response)); } + + // 3. No identity found + Ok(None) } #[tauri::command] @@ -587,6 +622,128 @@ pub async fn delete_nostr_identity(app: tauri::AppHandle) -> Result<(), String> Ok(()) } +// ========================================================================= +// NIP-46 remote signing (Nostr Connect) commands +// ========================================================================= + +/// Connect to a remote signer via a `bunker://` URI. +/// +/// Generates ephemeral app keys, performs the NIP-46 handshake, obtains the +/// user's public key, persists the connection state, and constructs the node. +#[tauri::command] +pub async fn connect_nip46_bunker( + bunker_uri: String, + app: tauri::AppHandle, +) -> Result { + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("failed to get app data dir: {e}"))?; + + // Reject if a local nsec identity already exists + if discovery::load_keys(&app_data_dir)?.is_some() { + return Err( + "A local Nostr identity already exists. Delete it first to use a remote signer." + .to_string(), + ); + } + + // Create the NostrConnect signer and connection state + let (signer, mut conn) = crate::nip46::connect_from_bunker_uri(&bunker_uri)?; + + // Wrap as Arc and get the user's public key. + // This triggers the NIP-46 handshake (connect + get_public_key). + let arc_signer = crate::nip46::into_arc_signer(signer); + let user_pubkey = arc_signer + .get_public_key() + .await + .map_err(|e| format!("NIP-46 handshake failed: {e}"))?; + + // Update connection state with the resolved user pubkey + conn.user_pubkey_hex = Some(user_pubkey.to_hex()); + + // Persist the connection + crate::nip46::save_connection(&app_data_dir, &conn)?; + + let response = IdentityResponse { + pubkey_hex: user_pubkey.to_hex(), + npub: user_pubkey + .to_bech32() + .map_err(|e| format!("bech32 error: {e}"))?, + }; + + // Construct the node using the remote signer + construct_and_store_node_with_signer(&app, arc_signer).await?; + + Ok(response) +} + +/// Disconnect from a remote signer and delete the persisted connection. +#[tauri::command] +pub async fn disconnect_nip46(app: tauri::AppHandle) -> Result<(), String> { + // Lock wallet and drop node + { + let node_state = app.state::(); + let mut guard = node_state.node.lock().await; + if let Some(node) = guard.as_ref() { + node.lock_wallet(); + } + *guard = None; + } + + // Clear wallet state + { + let manager = app.state::>(); + let mut mgr = manager + .lock() + .map_err(|_| "state lock failed".to_string())?; + mgr.set_wallet_unlocked(false); + if let Some(persister) = mgr.persister_mut() { + persister.clear_cache(); + } + mgr.clear_payment_swaps(); + } + + // Delete the persisted NIP-46 connection + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("failed to get app data dir: {e}"))?; + crate::nip46::delete_connection(&app_data_dir)?; + + bump_revision_and_emit(&app).await?; + Ok(()) +} + +/// Get the status of the NIP-46 connection, if any. +#[tauri::command] +pub async fn get_nip46_status( + app: tauri::AppHandle, +) -> Result, String> { + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("failed to get app data dir: {e}"))?; + + let conn = match crate::nip46::load_connection(&app_data_dir) { + Some(c) => c, + None => return Ok(None), + }; + + // Check if the node is actually initialized with this connection + let node_state = app.state::(); + let guard = node_state.node.lock().await; + let connected = guard.as_ref().is_some(); + + Ok(Some(crate::nip46::Nip46Status { + connected, + remote_signer_pubkey: conn.remote_signer_pubkey_hex, + user_pubkey: conn.user_pubkey_hex, + relay_urls: conn.relay_urls, + bunker_uri: conn.bunker_uri, + })) +} + // ========================================================================= // NIP-44 wallet backup commands // ========================================================================= diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fc96897e..3d08dc54 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod chain_adapter; pub mod commands; pub mod discovery; pub mod identity_file; +pub mod nip46; mod payments; pub mod state; pub mod wallet; @@ -1678,6 +1679,10 @@ pub fn run() { commands::export_nostr_nsec, commands::delete_nostr_identity, commands::import_nostr_nsec, + // NIP-46 remote signing + commands::connect_nip46_bunker, + commands::disconnect_nip46, + commands::get_nip46_status, commands::discover_contracts, commands::publish_contract, commands::oracle_attest, diff --git a/src-tauri/src/nip46.rs b/src-tauri/src/nip46.rs new file mode 100644 index 00000000..933de88e --- /dev/null +++ b/src-tauri/src/nip46.rs @@ -0,0 +1,162 @@ +//! NIP-46 remote signing (Nostr Connect) connection management. +//! +//! Handles persisting and restoring `NostrConnect` sessions across app restarts. +//! The connection state is stored as JSON in `/nip46_connection.json`. + +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +use nostr_connect::prelude::NostrConnect; +use nostr_sdk::prelude::*; +use serde::{Deserialize, Serialize}; + +const NIP46_CONNECTION_FILE: &str = "nip46_connection.json"; + +/// Default timeout for NIP-46 request/response round-trips. +const NIP46_TIMEOUT_SECS: u64 = 60; + +/// Persisted NIP-46 connection state. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Nip46Connection { + /// The bunker URI used to establish the connection. + pub bunker_uri: String, + /// The ephemeral app keypair (hex-encoded secret key). + pub app_secret_key_hex: String, + /// The remote signer's public key (hex). + pub remote_signer_pubkey_hex: String, + /// The user's actual public key (hex) — obtained via `get_public_key`. + pub user_pubkey_hex: Option, + /// Relay URLs used for the NIP-46 communication channel. + pub relay_urls: Vec, +} + +/// Status of the NIP-46 connection, returned to the frontend. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Nip46Status { + pub connected: bool, + pub remote_signer_pubkey: String, + pub user_pubkey: Option, + pub relay_urls: Vec, + pub bunker_uri: String, +} + +// ── Persistence ──────────────────────────────────────────────────────────── + +fn connection_path(app_data_dir: &Path) -> PathBuf { + app_data_dir.join(NIP46_CONNECTION_FILE) +} + +/// Load a persisted NIP-46 connection from disk. +pub fn load_connection(app_data_dir: &Path) -> Option { + let path = connection_path(app_data_dir); + let contents = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&contents).ok() +} + +/// Persist a NIP-46 connection to disk. +pub fn save_connection(app_data_dir: &Path, conn: &Nip46Connection) -> Result<(), String> { + let path = connection_path(app_data_dir); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("failed to create data dir: {e}"))?; + } + let json = + serde_json::to_string_pretty(conn).map_err(|e| format!("failed to serialize: {e}"))?; + std::fs::write(path, json).map_err(|e| format!("failed to write nip46 connection: {e}")) +} + +/// Delete the persisted NIP-46 connection. +pub fn delete_connection(app_data_dir: &Path) -> Result<(), String> { + let path = connection_path(app_data_dir); + if path.exists() { + std::fs::remove_file(&path) + .map_err(|e| format!("failed to delete nip46 connection: {e}"))?; + } + Ok(()) +} + +/// Returns `true` if a persisted NIP-46 connection exists. +pub fn has_connection(app_data_dir: &Path) -> bool { + connection_path(app_data_dir).exists() +} + +// ── NostrConnect construction ────────────────────────────────────────────── + +/// Create a new `NostrConnect` signer from a `bunker://` URI string. +/// +/// Generates fresh ephemeral app keys and returns both the signer and the +/// connection state to persist. +pub fn connect_from_bunker_uri( + bunker_uri_str: &str, +) -> Result<(NostrConnect, Nip46Connection), String> { + let uri = NostrConnectURI::parse(bunker_uri_str) + .map_err(|e| format!("invalid bunker URI: {e}"))?; + + if !uri.is_bunker() { + return Err("Expected a bunker:// URI, got nostrconnect://".to_string()); + } + + let remote_signer_pubkey = uri + .remote_signer_public_key() + .ok_or("bunker URI missing remote signer pubkey")?; + + let app_keys = Keys::generate(); + let relay_urls: Vec = uri.relays().iter().map(|r| r.to_string()).collect(); + + let signer = NostrConnect::new( + uri, + app_keys.clone(), + Duration::from_secs(NIP46_TIMEOUT_SECS), + None, + ) + .map_err(|e| format!("failed to create NostrConnect: {e}"))?; + + let conn = Nip46Connection { + bunker_uri: bunker_uri_str.to_string(), + app_secret_key_hex: app_keys.secret_key().to_secret_hex(), + remote_signer_pubkey_hex: remote_signer_pubkey.to_hex(), + user_pubkey_hex: None, + relay_urls, + }; + + Ok((signer, conn)) +} + +/// Restore a `NostrConnect` signer from a persisted connection. +/// +/// Re-uses the same app keys so the remote signer recognizes us. +pub fn restore_from_connection( + conn: &Nip46Connection, +) -> Result { + let uri = NostrConnectURI::parse(&conn.bunker_uri) + .map_err(|e| format!("invalid persisted bunker URI: {e}"))?; + + let secret_key = SecretKey::from_hex(&conn.app_secret_key_hex) + .map_err(|e| format!("invalid persisted app secret key: {e}"))?; + let app_keys = Keys::new(secret_key); + + let signer = NostrConnect::new( + uri, + app_keys, + Duration::from_secs(NIP46_TIMEOUT_SECS), + None, + ) + .map_err(|e| format!("failed to restore NostrConnect: {e}"))?; + + // If we already know the user's public key, set it to skip the + // initial `get_public_key` round-trip on first use. + if let Some(ref pk_hex) = conn.user_pubkey_hex { + if let Ok(pk) = PublicKey::from_hex(pk_hex) { + let _ = signer.non_secure_set_user_public_key(pk); + } + } + + Ok(signer) +} + +/// Wrap a `NostrConnect` instance as an `Arc`. +pub fn into_arc_signer(signer: NostrConnect) -> Arc { + Arc::new(signer) +} diff --git a/src/types.ts b/src/types.ts index 41838d01..3439d95b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -243,6 +243,14 @@ export type PreviewIdentityResponse = { nsec: string; }; +export type Nip46Status = { + connected: boolean; + remotSignerPubkey: string; + userPubkey: string | null; + relayUrls: string[]; + bunkerUri: string; +}; + export type RelayEntry = { url: string; has_backup: boolean }; export type RelayBackupResult = { url: string; has_backup: boolean }; export type WalletEntry = { name: string; d_tag: string }; From b56ac0818e0441df2a17a90444b67a618c9b9932 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 11:36:56 +0000 Subject: [PATCH 041/117] feat: add bunker login UI for NIP-46 remote signing Wire up the "Connect remote signer" button in onboarding to a full bunker:// URI connection flow: - New "bunker" onboarding mode with URI input and connection handler - Calls connect_nip46_bunker backend command for NIP-46 handshake - Bunker connections show "Remote signer connected" confirmation - Back button properly disconnects NIP-46 session - Store extended with onboardingBunkerUri state - Nip46Status TypeScript type for frontend status queries https://claude.ai/code/session_0198cw5FjyRcQMrB15tqt3WA --- src/components/onboarding/NostrSetupStep.tsx | 153 +++++++++++++++++-- src/store/index.ts | 9 +- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 3335774b..6bffd6cf 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -93,6 +93,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const restorePassword = useStore((s) => s.onboardingRestorePassword); const restoreMnemonic = useStore((s) => s.onboardingRestoreMnemonic); const restoreNsec = useStore((s) => s.onboardingRestoreNsec); + const bunkerUri = useStore((s) => s.onboardingBunkerUri); // Preview profile when a valid nsec is entered (import or manual-restore) const [importPreview, setImportPreview] = useState(null); @@ -188,9 +189,14 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { onboardingNsecAcknowledged: false, onboardingPendingPubkey: "", onboardingPendingNpub: "", + onboardingBunkerUri: "", onboardingError: "", }); - void invoke("delete_nostr_identity").catch(() => {}); + if (nostrMode === "bunker") { + void invoke("disconnect_nip46").catch(() => {}); + } else { + void invoke("delete_nostr_identity").catch(() => {}); + } } else if (nostrMode === "import") { // Back from import → main nostr page useStore.setState({ @@ -955,14 +961,23 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { // ── Nostr backup / confirmation screen ────────────────────────────── if (nostrDone) { - const isImport = nostrMode === "import"; - const eyebrow = isImport ? "Identity imported" : "Identity created"; - const title = isImport + const isImport = nostrMode === "import" || nostrMode === "bunker"; + const isBunker = nostrMode === "bunker"; + const eyebrow = isBunker + ? "Remote signer connected" + : isImport + ? "Identity imported" + : "Identity created"; + const title = isBunker ? "Confirm your identity" - : "Back up your secret key"; - const description = isImport - ? "Your Nostr identity has been imported. Confirm your details below before continuing." - : "Your nsec is the only way to prove ownership of markets you create. Store it somewhere safe \u2014 it cannot be recovered if lost."; + : isImport + ? "Confirm your identity" + : "Back up your secret key"; + const description = isBunker + ? "Connected to your remote signer via NIP-46. Your keys remain on the external device." + : isImport + ? "Your Nostr identity has been imported. Confirm your details below before continuing." + : "Your nsec is the only way to prove ownership of markets you create. Store it somewhere safe \u2014 it cannot be recovered if lost."; const truncatedNpub = npubDisplay.length > 20 @@ -1734,6 +1749,120 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { ); } + // ── Bunker (NIP-46) sub-page ───────────────────────────────────────── + if (nostrMode === "bunker") { + const handleConnectBunker = async () => { + const uri = bunkerUri.trim(); + if (!uri) { + useStore.setState({ onboardingError: "Paste a bunker:// URI." }); + return; + } + if (!uri.startsWith("bunker://")) { + useStore.setState({ + onboardingError: "Invalid URI. It should start with bunker://", + }); + return; + } + useStore.setState({ onboardingLoading: true, onboardingError: "" }); + try { + const identity = await invoke( + "connect_nip46_bunker", + { bunkerUri: uri }, + ); + useStore.setState({ + nostrPubkey: identity.pubkey_hex, + nostrNpub: identity.npub, + onboardingPendingPubkey: identity.pubkey_hex, + onboardingPendingNpub: identity.npub, + onboardingNostrDone: true, + onboardingLoading: false, + }); + // Fetch profile from relays in background + invoke("fetch_nostr_profile") + .then((profile) => { + if (profile) useStore.setState({ nostrProfile: profile }); + }) + .catch(() => {}); + } catch (e) { + useStore.setState({ + onboardingError: String(e), + onboardingLoading: false, + }); + } + }; + + return ( +
+ {stepIndicator} + + useStore.setState({ + onboardingNostrMode: "generate", + onboardingError: "", + onboardingBunkerUri: "", + }) + } + /> +

+ Connect remote signer +

+

+ Paste a bunker:// URI from + your NIP-46 signer (e.g. nsec.app, Amber, or any Nostr Connect + compatible app). Your keys stay on the external signer. +

+ {errorHtml &&
{errorHtml}
} +
+
+ + + useStore.setState({ onboardingBunkerUri: e.target.value }) + } + className="h-11 w-full rounded-lg border border-slate-700 bg-slate-900 px-4 text-sm outline-none ring-emerald-400 transition focus:ring-2 mono" + /> +
+
+

+ Your private key never leaves the remote signer. Deadcat will + request signatures over Nostr relays using the NIP-46 protocol. +

+
+ +
+
+ ); + } + // ── Main nostr step ───────────────────────────────────────────────── return (
@@ -1782,7 +1911,13 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) {
- {/* nsec */} - {nostrNpub && ( + {/* NIP-46 bunker info */} + {isRemoteSigner && nip46Status && ( +
+
+

+ Relay +

+

+ {nip46Status.relayUrls[0] ?? "unknown"} +

+
+
+ )} + + {/* nsec — only for local keys */} + {nostrNpub && !isRemoteSigner && (

Secret Key

@@ -284,7 +345,7 @@ function NostrSection() {
)} - {nostrNpub && ( + {nostrNpub && !isRemoteSigner && (

Back up your nsec. You need it to resolve markets you create.

@@ -298,6 +359,15 @@ function NostrSection() { > Set up identity + ) : isRemoteSigner ? ( + ) : ( +
+ )} + + {/* Divider */} + {connectQr && ( +
+
+ or +
+
+ )} +
Date: Thu, 16 Apr 2026 15:55:47 -0400 Subject: [PATCH 047/117] fix: replace nsec.app references with Amber and Primal --- docs/nip46-remaining-work.md | 2 +- src/components/onboarding/NostrSetupStep.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/nip46-remaining-work.md b/docs/nip46-remaining-work.md index 71a44381..ca0a2118 100644 --- a/docs/nip46-remaining-work.md +++ b/docs/nip46-remaining-work.md @@ -7,7 +7,7 @@ The items below are polish/hardening tasks to address after manual testing. ## 1. auth_url Handler for Browser-Based Approval -**Problem:** Some NIP-46 signers (nsec.app, etc.) respond to the `connect` +**Problem:** Some NIP-46 signers respond to the `connect` request with an `auth_url` challenge — the user must approve the connection in a browser before the signer responds with `ack`. Without handling this, `connect_nip46_bunker` will timeout after 60 seconds. diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 68d097b7..75d0ddaf 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -1836,7 +1836,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) {

Paste a bunker:// URI from - your NIP-46 signer (e.g. nsec.app, Amber, or any Nostr Connect + your NIP-46 signer (e.g. Amber, Primal, or any Nostr Connect compatible app). Your keys stay on the external signer.

{errorHtml &&
{errorHtml}
} From d54239a1c385a65e48d697bbb8f45a7dd158b950 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 18:07:35 -0400 Subject: [PATCH 048/117] fix: password minimum label and validation consistent at 8 characters --- src/components/onboarding/NostrSetupStep.tsx | 6 +++--- src/components/wallet/WalletSetup.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 75d0ddaf..7597e190 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -926,8 +926,8 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { />
- {walletPassword && walletPassword.length < 4 ? ( -

Minimum 4 characters

+ {walletPassword && walletPassword.length < 8 ? ( +

Minimum 8 characters

) : walletPasswordConfirm && walletPassword !== walletPasswordConfirm ? (

@@ -965,7 +965,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { disabled={ loading || !walletPassword || - walletPassword.length < 4 || + walletPassword.length < 8 || walletPassword !== walletPasswordConfirm } className="mt-5 w-full rounded-lg bg-emerald-400 px-4 py-3.5 font-semibold text-slate-950 hover:bg-emerald-300 transition disabled:opacity-50 disabled:cursor-not-allowed" diff --git a/src/components/wallet/WalletSetup.tsx b/src/components/wallet/WalletSetup.tsx index fbb316e7..6e42560a 100644 --- a/src/components/wallet/WalletSetup.tsx +++ b/src/components/wallet/WalletSetup.tsx @@ -64,7 +64,7 @@ export function WalletSetup({ const handleCreate = useCallback(() => { if ( !walletPassword || - walletPassword.length < 4 || + walletPassword.length < 8 || walletPassword !== walletPasswordConfirm ) { useStore.setState({ @@ -465,7 +465,7 @@ export function WalletSetup({

{walletPassword && !pwValid ? ( -

Minimum 4 characters

+

Minimum 8 characters

) : walletPasswordConfirm && !pwMatch ? (

Passwords don't match From cb05449819eb6bb8c211d792436debcf76040a90 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 18:16:29 -0400 Subject: [PATCH 049/117] fix: NIP-46 aware logout and consistent password validation Logout modal detects remote signer and shows "Disconnect Signer" flow that keeps the wallet intact. delete_nostr_identity also cleans up NIP-46 connection file. Password minimum validation and labels consistent at 8 characters everywhere. --- src-tauri/src/commands.rs | 3 +- src/components/layout/TopShell.tsx | 50 ++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index eb435a17..71e69898 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -605,7 +605,7 @@ pub async fn delete_nostr_identity(app: tauri::AppHandle) -> Result<(), String> mgr.clear_payment_swaps(); } - // Delete key file + // Delete key file and NIP-46 connection let app_data_dir = app .path() .app_data_dir() @@ -614,6 +614,7 @@ pub async fn delete_nostr_identity(app: tauri::AppHandle) -> Result<(), String> if key_path.exists() { std::fs::remove_file(&key_path).map_err(|e| format!("failed to delete key file: {e}"))?; } + let _ = crate::nip46::delete_connection(&app_data_dir); bump_revision_and_emit(&app).await?; Ok(()) diff --git a/src/components/layout/TopShell.tsx b/src/components/layout/TopShell.tsx index 129231a5..a8d6f06f 100644 --- a/src/components/layout/TopShell.tsx +++ b/src/components/layout/TopShell.tsx @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { categories } from "../../constants"; import { useEscapeKey } from "../../hooks/useEscapeKey"; import { useStore } from "../../store"; @@ -148,6 +148,15 @@ function LogoutModal() { const logoutPasswordInput = useStore((s) => s.logoutPasswordInput ?? ""); const logoutBackupDownloaded = useStore((s) => s.logoutBackupDownloaded); const needsPassword = walletStatus === "locked" || !walletSessionPassword; + const [isRemoteSigner, setIsRemoteSigner] = useState(false); + + useEffect(() => { + if (logoutOpen) { + invoke<{ connected: boolean } | null>("get_nip46_status") + .then((s) => setIsRemoteSigner(s?.connected === true)) + .catch(() => setIsRemoteSigner(false)); + } + }, [logoutOpen]); const closeLogout = useCallback(() => { useStore.setState({ @@ -162,7 +171,27 @@ function LogoutModal() { useEscapeKey(logoutOpen, closeLogout); const confirmLogout = useCallback(async () => { - // Lock first to ensure clean state, then delete + if (isRemoteSigner) { + // NIP-46: disconnect signer but keep wallet + try { + await invoke("disconnect_nip46"); + } catch (e) { + console.warn("disconnect_nip46:", e); + } + useStore.setState({ + nostrPubkey: null, + nostrNpub: null, + nostrProfile: null, + nostrNsecRevealed: null, + logoutOpen: false, + logoutBackedUp: false, + profilePicError: false, + }); + window.location.reload(); + return; + } + + // Local keys: full logout — lock, delete wallet, delete identity try { await invoke("lock_wallet"); } catch (e) { @@ -210,25 +239,28 @@ function LogoutModal() { }); localStorage.removeItem("deadcat_tx_labels"); window.location.reload(); - }, []); + }, [isRemoteSigner]); if (!logoutOpen) return null; const hasWallet = walletStatus !== "not_created"; - // Simple logout for identity-only (no wallet) - if (!hasWallet) { + // Simple logout for identity-only (no wallet) or remote signer + if (!hasWallet || isRemoteSigner) { return (

-

Log Out

+

+ {isRemoteSigner ? "Disconnect Signer" : "Log Out"} +

- This will remove your Nostr identity from this device. Make sure - you have your nsec saved if you want to log back in. + {isRemoteSigner + ? "This will disconnect the remote signer. Your wallet and funds will remain on this device. You can reconnect anytime." + : "This will remove your Nostr identity from this device. Make sure you have your nsec saved if you want to log back in."}

From 0f274b29d75169a162e31cddb4bbb83660b5e143 Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 18:20:32 -0400 Subject: [PATCH 050/117] =?UTF-8?q?feat:=20user=20menu=20UX=20=E2=80=94=20?= =?UTF-8?q?Nostr=20ID=20badge,=20reorder=20items,=20NIP-46=20disconnect=20?= =?UTF-8?q?label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename "Nostr Publishing ID" to "Nostr ID" with connection method badge (Local Keys / Remote Signer). Swap Edit Profile before Settings. Show "Disconnect" instead of "Log out" for NIP-46 users. --- src/components/layout/UserMenu.tsx | 47 +++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/components/layout/UserMenu.tsx b/src/components/layout/UserMenu.tsx index 73714ec4..4a67055a 100644 --- a/src/components/layout/UserMenu.tsx +++ b/src/components/layout/UserMenu.tsx @@ -1,4 +1,5 @@ -import { useCallback } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; import { useStore } from "../../store"; import { generateAvatarDataUri } from "../../utils-react/avatar"; import { showToast } from "../shared/Toast"; @@ -6,6 +7,15 @@ import { showToast } from "../shared/Toast"; export function UserMenu() { const userMenuOpen = useStore((s) => s.userMenuOpen); const nostrNpub = useStore((s) => s.nostrNpub); + const [isRemoteSigner, setIsRemoteSigner] = useState(false); + + useEffect(() => { + if (userMenuOpen) { + invoke<{ connected: boolean } | null>("get_nip46_status") + .then((s) => setIsRemoteSigner(s?.connected === true)) + .catch(() => setIsRemoteSigner(false)); + } + }, [userMenuOpen]); const toggleMenu = useCallback(() => { useStore.setState((s) => ({ userMenuOpen: !s.userMenuOpen })); @@ -55,8 +65,17 @@ export function UserMenu() { {userMenuOpen && nostrNpub && (
-
- Nostr Publishing ID +
+ Nostr ID + + {isRemoteSigner ? "Remote Signer" : "Local Keys"} +
@@ -148,7 +167,7 @@ export function UserMenu() { - Log out + {isRemoteSigner ? "Disconnect" : "Log out"}
From bb6633d1caf6f26ecd9ec12c4af151205b7acd9f Mon Sep 17 00:00:00 2001 From: The Daniel Date: Thu, 16 Apr 2026 18:32:58 -0400 Subject: [PATCH 051/117] feat: show NIP-05 with verified badge under name in profile editor --- src/components/profile/ProfilePage.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/profile/ProfilePage.tsx b/src/components/profile/ProfilePage.tsx index c4764f71..8b096ca5 100644 --- a/src/components/profile/ProfilePage.tsx +++ b/src/components/profile/ProfilePage.tsx @@ -271,6 +271,19 @@ function ProfilePageContent() {

{displayName || name || "Unnamed"}

+ {nip05 && ( +

+ + {nip05} +

+ )} {nostrNpub && (
diff --git a/src/components/detail/MarketHeader.tsx b/src/components/detail/MarketHeader.tsx index 6946b938..e77dbb46 100644 --- a/src/components/detail/MarketHeader.tsx +++ b/src/components/detail/MarketHeader.tsx @@ -10,6 +10,7 @@ import { getEstimatedSettlementDate, getPositionContracts, } from "../../utils-react/market"; +import { categoryIcon } from "../layout/TopShell"; function stateBadge(state: number) { const colorMap: Record = { @@ -79,7 +80,8 @@ export function MarketHeaderTop({ market }: { market: Market }) { Markets
- + + {categoryIcon(market.category, "h-3 w-3")} {market.category} {stateBadge(market.state)} diff --git a/src/components/home/HomePage.tsx b/src/components/home/HomePage.tsx index f1def3ba..a1c26c4a 100644 --- a/src/components/home/HomePage.tsx +++ b/src/components/home/HomePage.tsx @@ -449,7 +449,11 @@ function PortfolioView({ markets }: { markets: Market[] }) { className="block w-full rounded-2xl border border-slate-800 bg-slate-950/55 p-4 text-left transition hover:border-slate-600" >
- + + {p.market.category} · {timeLeft}
@@ -609,7 +613,10 @@ function MyMarketsView({ markets }: { markets: Market[] }) { className="rounded-2xl border border-slate-800 bg-slate-950/55 p-4 text-left transition hover:border-slate-600" >
- {market.category} + + + {market.category} + diff --git a/src/components/home/MarketCard.tsx b/src/components/home/MarketCard.tsx index 33bf17ac..e1aff11b 100644 --- a/src/components/home/MarketCard.tsx +++ b/src/components/home/MarketCard.tsx @@ -14,6 +14,7 @@ import { setLimitPriceSats, } from "../../utils-react/market"; import { generateMockPriceHistory } from "../../utils-react/mock-price-history"; +import { categoryIcon } from "../layout/TopShell"; // ── Inline SVG icons ───────────────────────────────────────────────── function TrendUpIcon() { @@ -219,7 +220,8 @@ export default function MarketCard({ style={{ animationDelay: `${index * 50}ms` }} >
- + + {categoryIcon(market.category, "h-3 w-3")} {market.category} {market.isLive && · LIVE} diff --git a/src/components/layout/SettingsPanel.tsx b/src/components/layout/SettingsPanel.tsx index d2887290..a6bbacce 100644 --- a/src/components/layout/SettingsPanel.tsx +++ b/src/components/layout/SettingsPanel.tsx @@ -96,7 +96,6 @@ function NostrSection() { const [nsecPasswordPrompt, setNsecPasswordPrompt] = useState(false); const [nsecPasswordError, setNsecPasswordError] = useState(""); const [nip46Status, setNip46Status] = useState(null); - const [disconnecting, setDisconnecting] = useState(false); // Auto-hide nsec when wallet locks useEffect(() => { @@ -173,23 +172,6 @@ function NostrSection() { } }, [nsecPasswordInput]); - const handleDisconnectBunker = useCallback(async () => { - setDisconnecting(true); - try { - await invoke("disconnect_nip46"); - useStore.setState({ - nostrPubkey: null, - nostrNpub: null, - nostrProfile: null, - }); - setNip46Status(null); - showToast("Remote signer disconnected"); - } catch (e) { - showToast(`Disconnect failed: ${e}`); - } - setDisconnecting(false); - }, []); - return (
{/* Signer type badge */} @@ -359,16 +341,7 @@ function NostrSection() { > Set up identity - ) : isRemoteSigner ? ( - - ) : ( + ) : isRemoteSigner ? null : (
@@ -284,7 +269,8 @@ function LogoutModal() { ); } - // Full logout with backup flow when wallet exists + // Full logout with backup flow when a wallet exists — same for local + // nsec users and NIP-46 remote-signer users. return (
@@ -297,13 +283,111 @@ function LogoutModal() {

Logging out will{" "} - permanently remove your Nostr keys and wallet + permanently erase this wallet + {isRemoteSigner ? "" : " and your Nostr keys"} {" "} - from this device. Download your backup file before logging out so - you can restore your account later. + from this device.{" "} + {isRemoteSigner + ? "Save your wallet recovery phrase before logging out — without it your funds cannot be recovered." + : "Download your backup file before logging out so you can restore your account later."}

- {logoutBackupDownloaded ? ( + {isRemoteSigner ? ( + revealedMnemonic ? ( +
+

+ Wallet recovery phrase +

+
+ {revealedMnemonic + .split(/\s+/) + .filter(Boolean) + .map((word, idx) => ( +
+ + {idx + 1}. + + + {word} + +
+ ))} +
+ +
+ ) : ( + <> + {needsPassword && ( + + useStore.setState({ + logoutPasswordInput: e.target.value, + }) + } + placeholder="Enter your wallet password" + className="h-11 w-full rounded-lg border border-slate-700 bg-slate-900 px-4 text-sm outline-none ring-emerald-400 transition focus:ring-2" + /> + )} + {logoutBackupError && ( +
+

+ {logoutBackupError} +

+
+ )} + + + ) + ) : logoutBackupDownloaded ? (
- I have downloaded my backup or already have one saved. + + {isRemoteSigner + ? "I have saved my wallet recovery phrase somewhere safe." + : "I have downloaded my backup or already have one saved."} +

- Deadcat Live does not hold user funds. + Deadcat does not hold your funds. {" "} - If you lose your backup file and password, your funds cannot be - recovered. + {isRemoteSigner + ? "Without your wallet recovery phrase, funds in this wallet cannot be recovered." + : "If you lose your backup file and password, your funds cannot be recovered."}

diff --git a/src/components/modals/WalletModal.tsx b/src/components/modals/WalletModal.tsx index 1bfb4d27..8735bc4c 100644 --- a/src/components/modals/WalletModal.tsx +++ b/src/components/modals/WalletModal.tsx @@ -65,7 +65,7 @@ export function WalletModal() { : 0; const subtitle = walletModal === "receive" - ? "Choose a method to receive funds into your Liquid wallet." + ? "Choose a method to receive Bitcoin into your wallet." : "Send funds from your wallet via Lightning, Liquid, or Bitcoin."; const tabs: Array<"lightning" | "liquid" | "bitcoin"> = [ diff --git a/src/components/onboarding/NostrSetupStep.tsx b/src/components/onboarding/NostrSetupStep.tsx index 7597e190..c20f86e6 100644 --- a/src/components/onboarding/NostrSetupStep.tsx +++ b/src/components/onboarding/NostrSetupStep.tsx @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { open } from "@tauri-apps/plugin-dialog"; import { readFile, readTextFile } from "@tauri-apps/plugin-fs"; @@ -10,6 +11,7 @@ import { useRef, useState, } from "react"; +import { setWalletNeedsBackup } from "../../hooks/useWalletNeedsBackup"; import { useStore } from "../../store"; import type { IdentityResponse, @@ -63,6 +65,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const [connectQr, setConnectQr] = useState(""); const [connectUri, setConnectUri] = useState(""); const connectGenerated = useRef(false); + const [bunkerTab, setBunkerTab] = useState<"scan" | "paste">("scan"); // Focus the mnemonic textarea only when first expanded, not on every re-render useEffect(() => { @@ -99,15 +102,64 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const restoreNsec = useStore((s) => s.onboardingRestoreNsec); const bunkerUri = useStore((s) => s.onboardingBunkerUri); - // Generate nostrconnect:// QR when bunker mode is entered + // Initiate the app-initiated NIP-46 flow when bunker mode is entered. + // The backend returns the URI immediately (for QR display) and spawns a + // listener that completes the handshake and emits `nostrconnect:connected` + // when the remote signer responds — at which point we mark onboarding done. useEffect(() => { if (nostrMode !== "bunker" || connectGenerated.current) return; connectGenerated.current = true; + let unlistenConnected: UnlistenFn | undefined; + let unlistenError: UnlistenFn | undefined; + let cancelled = false; void (async () => { + unlistenConnected = await listen( + "nostrconnect:connected", + (e) => { + if (cancelled) return; + // Land on the "Confirm your identity" screen (same as the + // bunker-paste flow) — unified post-connect UX for both + // remote-signer entry points. User clicks Continue to + // advance to step 2. + useStore.setState({ + nostrPubkey: e.payload.pubkey_hex, + nostrNpub: e.payload.npub, + onboardingPendingPubkey: e.payload.pubkey_hex, + onboardingPendingNpub: e.payload.npub, + onboardingNostrDone: true, + onboardingLoading: false, + }); + // Fire the profile fetch now so the confirm card has real + // data by the time it renders. Fire-and-forget — not tied + // to this component's lifecycle. + void (async () => { + try { + const profile = await invoke( + "preview_nostr_profile", + { npub: e.payload.npub }, + ); + if (profile) useStore.setState({ nostrProfile: profile }); + } catch { + // Node-backed fetch as a fallback. + invoke("fetch_nostr_profile") + .then((p) => { + if (p) useStore.setState({ nostrProfile: p }); + }) + .catch(() => {}); + } + })(); + }, + ); + unlistenError = await listen("nostrconnect:error", (e) => { + if (cancelled) return; + useStore.setState({ + onboardingError: e.payload, + onboardingLoading: false, + }); + }); try { - const result = await invoke<{ uri: string; appSecretKeyHex: string }>( - "generate_nostrconnect_uri", - ); + const result = await invoke<{ uri: string }>("initiate_nostrconnect"); + if (cancelled) return; setConnectUri(result.uri); const canvas = document.createElement("canvas"); await QRCode.toCanvas(canvas, result.uri, { @@ -116,11 +168,18 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { scale: 6, color: { dark: "#0f172a", light: "#ffffff" }, }); - setConnectQr(canvas.toDataURL("image/png")); - } catch { - // QR generation failed — paste flow still works + if (!cancelled) setConnectQr(canvas.toDataURL("image/png")); + } catch (e) { + if (!cancelled) { + useStore.setState({ onboardingError: String(e) }); + } } })(); + return () => { + cancelled = true; + unlistenConnected?.(); + unlistenError?.(); + }; }, [nostrMode]); // Preview profile when a valid nsec is entered (import or manual-restore) @@ -319,8 +378,13 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { onboardingNostrNsec: "", onboardingError: "", }); - if (state.onboardingNostrMode === "import") { - // Scan for backups when importing an existing identity + if ( + state.onboardingNostrMode === "import" || + state.onboardingNostrMode === "bunker" + ) { + // Scan for backups when the identity already exists (import nsec or + // remote signer connect) — in either case the user may have a wallet + // mnemonic previously backed up under this npub on relays. useStore.setState({ onboardingBackupScanning: true }); try { const status = @@ -393,6 +457,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { }); // 2. Create wallet const mnemonic = await invoke("create_wallet", { password }); + setWalletNeedsBackup(true); // 3. Persist the previewed nsec + construct nostr node // (preview_nostr_identity didn't touch disk or spawn tasks, so we // commit it here via import_nostr_nsec — the single place that @@ -1284,6 +1349,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { mnemonic: restoreMnemonic.trim(), password: restorePassword, }); + setWalletNeedsBackup(false); await invoke("init_nostr_identity"); await invoke("unlock_wallet", { password: restorePassword }); useStore.setState({ @@ -1302,6 +1368,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { const mnemonic = await invoke("create_wallet", { password: restorePassword, }); + setWalletNeedsBackup(true); useStore.setState({ walletMnemonic: mnemonic }); await invoke("init_nostr_identity"); await invoke("unlock_wallet", { password: restorePassword }); @@ -1315,6 +1382,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { mnemonic: restoreMnemonic.trim(), password: restorePassword, }); + setWalletNeedsBackup(false); const identity = await invoke( "generate_nostr_identity", ); @@ -1571,6 +1639,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { mnemonic: payload.mnemonic, password: restorePassword, }); + setWalletNeedsBackup(false); await invoke("init_nostr_identity"); await invoke("unlock_wallet", { password: restorePassword }); // Set preview profile immediately so avatar shows in the corner @@ -1835,53 +1904,65 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { Connect remote signer

- Paste a bunker:// URI from - your NIP-46 signer (e.g. Amber, Primal, or any Nostr Connect - compatible app). Your keys stay on the external signer. + Sign with your NIP-46 signer (Amber, Primal…) — keys stay on the + external device.

{errorHtml &&
{errorHtml}
} -
- {/* QR code for app-initiated flow */} - {connectQr && ( -
-

- Scan with your signer app -

-
+ + {/* Tabs: Scan QR (default) vs. paste bunker:// URI */} +
+ + +
+ + {bunkerTab === "scan" ? ( +
+
+ {connectQr ? ( Nostr Connect QR -
- -
- )} - - {/* Divider */} - {connectQr && ( -
-
- or -
+ ) : ( +
+
+
+ )}
- )} - -
- + Copy nostrconnect:// URI + +
+ ) : ( +
useStore.setState({ onboardingBunkerUri: e.target.value }) } - className="h-11 w-full rounded-lg border border-slate-700 bg-slate-900 px-4 text-sm outline-none ring-emerald-400 transition focus:ring-2 mono" + className="mono h-11 w-full rounded-lg border border-slate-700 bg-slate-900 px-4 text-sm outline-none ring-emerald-400 transition focus:ring-2" /> -
-
-

- Your private key never leaves the remote signer. Deadcat will - request signatures over Nostr relays using the NIP-46 protocol. -

-
- -
+ ) : ( + "Connect" + )} + +
+ )}
); } @@ -1980,7 +2055,7 @@ export default function NostrSetupStep({ stepIndicator }: NostrSetupStepProps) { onboardingBunkerUri: "", }) } - className="flex w-full items-center justify-center gap-2 rounded-lg border border-slate-800 px-4 py-3.5 text-sm font-medium text-slate-500 hover:bg-slate-900 hover:text-slate-400 transition" + className="w-full rounded-lg border border-slate-700 px-4 py-3.5 text-sm font-medium text-slate-300 hover:bg-slate-800 hover:border-slate-600 transition" > Connect remote signer diff --git a/src/components/onboarding/OnboardingOverlay.tsx b/src/components/onboarding/OnboardingOverlay.tsx index de8890c5..3de245ab 100644 --- a/src/components/onboarding/OnboardingOverlay.tsx +++ b/src/components/onboarding/OnboardingOverlay.tsx @@ -30,9 +30,6 @@ export default function OnboardingOverlay() { onboardingWalletPassword: "", onboardingWalletPasswordConfirm: "", onboardingWalletPasswordStep: false, - onboardingMnemonicVerifyStep: false, - onboardingMnemonicVerifyIndices: [], - onboardingMnemonicVerifyInputs: [], onboardingBackupFound: false, onboardingBackupScanning: false, onboardingPasswordRevealed: false, diff --git a/src/components/onboarding/WalletSetupStep.tsx b/src/components/onboarding/WalletSetupStep.tsx index 807d38bf..9e77fde4 100644 --- a/src/components/onboarding/WalletSetupStep.tsx +++ b/src/components/onboarding/WalletSetupStep.tsx @@ -1,7 +1,10 @@ import { useQueryClient } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; -import { type ReactNode, useCallback } from "react"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; +import { setWalletNeedsBackup } from "../../hooks/useWalletNeedsBackup"; import { useStore } from "../../store"; +import type { NostrProfile } from "../../types"; +import { generateAvatarDataUri } from "../../utils-react/avatar"; import { showToast } from "../shared/Toast"; function BackButton({ onClick }: { onClick: () => void }) { @@ -30,40 +33,13 @@ function BackButton({ onClick }: { onClick: () => void }) { ); } -// ── Mnemonic grid (React version) ─────────────────────────────────── -function MnemonicGrid({ mnemonic }: { mnemonic: string }) { - const words = mnemonic.split(" "); - const rows: string[][] = []; - for (let i = 0; i < words.length; i += 3) { - rows.push(words.slice(i, i + 3)); - } - return ( -
- {rows.map((row, rowIdx) => ( -
- {row.map((word, colIdx) => ( -
- - {rowIdx * 3 + colIdx + 1}. - - {word} -
- ))} -
- ))} -
- ); -} - // ── Password fields ───────────────────────────────────────────────── export function PasswordFields({ password, confirm, revealed, disabled, + label = "Password", onPasswordChange, onConfirmChange, onToggleReveal, @@ -72,6 +48,7 @@ export function PasswordFields({ confirm: string; revealed: boolean; disabled: boolean; + label?: string; onPasswordChange: (val: string) => void; onConfirmChange: (val: string) => void; onToggleReveal: () => void; @@ -85,7 +62,7 @@ export function PasswordFields({ htmlFor="wallet-password" className="text-xs font-medium text-slate-400 uppercase tracking-wide" > - Password + {label}
s.onboardingWalletPassword); const passwordConfirm = useStore((s) => s.onboardingWalletPasswordConfirm); const passwordRevealed = useStore((s) => s.onboardingPasswordRevealed); - const verifyStep = useStore((s) => s.onboardingMnemonicVerifyStep); - const verifyIndices = useStore((s) => s.onboardingMnemonicVerifyIndices); - const verifyInputs = useStore((s) => s.onboardingMnemonicVerifyInputs); const backupScanning = useStore((s) => s.onboardingBackupScanning); const backupFound = useStore((s) => s.onboardingBackupFound); const backupStatus = useStore((s) => s.nostrBackupStatus); const selectedDTag = useStore((s) => s.onboardingSelectedWalletDTag); const relays = useStore((s) => s.relays); + const nostrMode = useStore((s) => s.onboardingNostrMode); + const nostrProfile = useStore((s) => s.nostrProfile); + const nostrNpub = useStore((s) => s.nostrNpub); const queryClient = useQueryClient(); + // Whether the optional "Add wallet recovery phrase" panel is expanded + // on the combined bunker setup screen. Local state, reset on unmount. + const [showRecoveryInput, setShowRecoveryInput] = useState(false); + // Tracks whether the profile fetch has settled at least once (success + // or failure). Used to stop showing "Loading profile…" indefinitely. + const [profileAttempted, setProfileAttempted] = useState(false); + + // Fetch the user's kind:0 profile for the bunker identity card. + // Uses `preview_nostr_profile` (2s connect + 3s fetch, standalone + // client) rather than the node-backed `fetch_nostr_profile` which + // has a 10s timeout and depends on the node's relay pool being + // fully connected. + useEffect(() => { + if (nostrMode !== "bunker") return; + if (!nostrNpub) return; + if (nostrProfile) { + setProfileAttempted(true); + return; + } + let cancelled = false; + void (async () => { + // Try the fast standalone client first. If relays were slow or the + // kind:0 wasn't on our defaults, retry a couple of times, then fall + // back to the node-backed fetch (its relay pool is already warm + // from the NIP-46 subscription). + const attempts: Array<() => Promise> = [ + () => + invoke("preview_nostr_profile", { + npub: nostrNpub, + }), + () => + invoke("preview_nostr_profile", { + npub: nostrNpub, + }), + () => invoke("fetch_nostr_profile"), + ]; + for (const attempt of attempts) { + if (cancelled) return; + try { + const profile = await attempt(); + if (cancelled) return; + if (profile) { + useStore.setState({ nostrProfile: profile }); + setProfileAttempted(true); + return; + } + } catch { + // Try the next strategy + } + // Small gap between attempts so relays have a moment to settle + await new Promise((r) => setTimeout(r, 800)); + } + if (!cancelled) setProfileAttempted(true); + })(); + return () => { + cancelled = true; + }; + }, [nostrMode, nostrNpub, nostrProfile]); + const errorHtml = error ? (

{error}

@@ -242,20 +279,13 @@ export default function WalletSetupStep({ const handleBack = useCallback(() => { useStore.setState({ onboardingPasswordRevealed: false }); - if (passwordStep && walletMode === "create") { - // Back from password page (create) -> verify step - useStore.setState({ - onboardingWalletPasswordStep: false, - onboardingWalletPassword: "", - onboardingWalletPasswordConfirm: "", - onboardingMnemonicVerifyStep: true, - onboardingError: "", - }); - } else if ( + if ( passwordStep && - (walletMode === "restore" || walletMode === "nostr-restore") + (walletMode === "create" || + walletMode === "restore" || + walletMode === "nostr-restore") ) { - // Back from password page (restore/nostr-restore) -> sub-page + // Back from password page -> prior screen (main wallet page or restore sub-page) useStore.setState({ onboardingWalletPasswordStep: false, onboardingWalletPassword: "", @@ -279,8 +309,7 @@ export default function WalletSetupStep({ }); } } else { - const onSubPage = - passwordStep || verifyStep || !!mnemonic || walletMode === "restore"; + const onSubPage = passwordStep || !!mnemonic || walletMode === "restore"; if (onSubPage) { useStore.setState({ onboardingWalletMode: "create", @@ -288,9 +317,6 @@ export default function WalletSetupStep({ onboardingWalletPassword: "", onboardingWalletPasswordConfirm: "", onboardingWalletPasswordStep: false, - onboardingMnemonicVerifyStep: false, - onboardingMnemonicVerifyIndices: [], - onboardingMnemonicVerifyInputs: [], onboardingBackupFound: false, onboardingError: "", }); @@ -304,7 +330,7 @@ export default function WalletSetupStep({ }); } } - }, [passwordStep, walletMode, walletOnly, verifyStep, mnemonic]); + }, [passwordStep, walletMode, walletOnly, mnemonic]); // ── Wallet continue (create / restore / nostr-restore) ──────────── const handleWalletContinue = useCallback(async () => { @@ -314,37 +340,10 @@ export default function WalletSetupStep({ }); return; } - if (walletMode === "create") { - // Generate mnemonic, then show backup screen - useStore.setState({ - onboardingLoading: true, - onboardingError: "", - }); - try { - const newMnemonic = await invoke("generate_mnemonic"); - const wordCount = newMnemonic.trim().split(/\s+/).length; - const pool = Array.from({ length: wordCount }, (_, i) => i); - for (let i = pool.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [pool[i], pool[j]] = [pool[j], pool[i]]; - } - useStore.setState({ - onboardingWalletMnemonic: newMnemonic, - onboardingMnemonicVerifyIndices: pool - .slice(0, 3) - .sort((a, b) => a - b), - onboardingMnemonicVerifyInputs: ["", "", ""], - onboardingLoading: false, - }); - } catch (e) { - useStore.setState({ - onboardingError: String(e), - onboardingLoading: false, - }); - } - return; - } - // For restore/nostr-restore: go to password step (clear passwords for fresh start) + // All modes: go straight to password step. For "create" the mnemonic is + // generated atomically inside `create_wallet(password)` at commit time, + // so there's no separate "save your recovery phrase" / verify step. + // Users can always retrieve the mnemonic later from Settings. useStore.setState({ onboardingWalletPasswordStep: true, onboardingWalletPassword: "", @@ -373,10 +372,11 @@ export default function WalletSetupStep({ } useStore.setState({ onboardingLoading: true, onboardingError: "" }); try { - await invoke("restore_wallet", { - mnemonic, - password, - }); + // Backend generates the mnemonic, encrypts it with the password, and + // persists it in one shot. User never sees the raw mnemonic during + // onboarding — it's retrievable later from Settings. + await invoke("create_wallet", { password }); + setWalletNeedsBackup(true); await invoke("unlock_wallet", { password }); showToast("Wallet created!", "success"); useStore.setState({ onboardingLoading: false }); @@ -387,7 +387,7 @@ export default function WalletSetupStep({ onboardingLoading: false, }); } - }, [password, passwordConfirm, mnemonic, queryClient]); + }, [password, passwordConfirm, queryClient]); // ── Restore wallet from seed (after password) ───────────────────── const handleRestoreWallet = useCallback(async () => { @@ -414,6 +414,7 @@ export default function WalletSetupStep({ mnemonic: mnemonic.trim(), password, }); + setWalletNeedsBackup(false); await invoke("unlock_wallet", { password }); showToast("Wallet restored!", "success"); await finishOnboarding(queryClient); @@ -456,6 +457,7 @@ export default function WalletSetupStep({ mnemonic: restoredMnemonic.trim(), password, }); + setWalletNeedsBackup(false); await invoke("unlock_wallet", { password }); showToast("Wallet restored from Nostr backup!", "success"); await finishOnboarding(queryClient); @@ -467,58 +469,220 @@ export default function WalletSetupStep({ } }, [password, passwordConfirm, backupStatus, selectedDTag, queryClient]); - // ── Copy mnemonic ───────────────────────────────────────────────── - const handleCopyMnemonic = useCallback(() => { - if (mnemonic) { - void navigator.clipboard.writeText(mnemonic); - showToast("Copied recovery phrase to clipboard"); + // ── Combined bunker wallet commit ───────────────────────────────── + // Single-screen flow for NIP-46 users: validate password, then either + // restore a typed mnemonic or generate a fresh wallet. + const handleBunkerWalletSubmit = useCallback(async () => { + if (!password || password !== passwordConfirm) { + useStore.setState({ + onboardingError: !password + ? "Password is required." + : "Passwords do not match.", + onboardingWalletPassword: "", + onboardingWalletPasswordConfirm: "", + }); + return; } - }, [mnemonic]); - - // ── Mnemonic done -> verify step ────────────────────────────────── - const handleMnemonicDone = useCallback(() => { - useStore.setState({ - onboardingMnemonicVerifyStep: true, - onboardingError: "", - }); - }, []); - - // ── Verify mnemonic ─────────────────────────────────────────────── - const handleVerifyMnemonic = useCallback(() => { - const words = mnemonic.trim().split(/\s+/); - const allVerified = - verifyIndices.length === 3 && - verifyIndices.every( - (wordIdx, i) => - (verifyInputs[i] ?? "").trim().toLowerCase() === words[wordIdx], - ); - if (!allVerified) { + if (password.length < 8) { useStore.setState({ - onboardingError: - "One or more words are incorrect. Check your recovery phrase and try again.", + onboardingError: "Password must be at least 8 characters.", }); return; } - useStore.setState({ - onboardingWalletPasswordStep: true, - onboardingMnemonicVerifyStep: false, - onboardingWalletPassword: "", - onboardingWalletPasswordConfirm: "", - onboardingError: "", - }); - }, [mnemonic, verifyIndices, verifyInputs]); - - // ── Verify input change ─────────────────────────────────────────── - const handleVerifyInputChange = useCallback( - (index: number, value: string) => { - useStore.setState((s) => { - const inputs = [...s.onboardingMnemonicVerifyInputs]; - inputs[index] = value; - return { onboardingMnemonicVerifyInputs: inputs }; + useStore.setState({ onboardingLoading: true, onboardingError: "" }); + try { + const typedMnemonic = mnemonic.trim(); + if (typedMnemonic) { + await invoke("restore_wallet", { + mnemonic: typedMnemonic, + password, + }); + setWalletNeedsBackup(false); + showToast("Wallet restored!", "success"); + } else { + await invoke("create_wallet", { password }); + setWalletNeedsBackup(true); + showToast("Wallet created!", "success"); + } + await invoke("unlock_wallet", { password }); + // The `app_state_updated` event listener only syncs `unlocked → locked` + // transitions to the store; we must explicitly flip the store's + // walletStatus here so WalletPage doesn't land on its legacy setup + // screen after onboarding finishes. + useStore.setState({ + walletStatus: "unlocked", + walletSessionPassword: password, + onboardingLoading: false, }); - }, - [], - ); + await finishOnboarding(queryClient); + } catch (e) { + useStore.setState({ + onboardingError: String(e), + onboardingLoading: false, + }); + } + }, [password, passwordConfirm, mnemonic, queryClient]); + + // ── Combined bunker setup screen ────────────────────────────────── + // Single page that shows the connected Nostr identity, an optional + // recovery-phrase input, password fields, and a single submit button. + // Replaces the legacy multi-step Confirm Identity → Set up wallet flow. + if (nostrMode === "bunker") { + const displayName = + nostrProfile?.display_name || nostrProfile?.name || "Nostr identity"; + const avatarSrc = nostrProfile?.picture + ? nostrProfile.picture + : nostrNpub + ? generateAvatarDataUri(nostrNpub) + : ""; + const nip05 = nostrProfile?.nip05 ?? ""; + const truncNpub = nostrNpub + ? `${nostrNpub.slice(0, 12)}...${nostrNpub.slice(-6)}` + : ""; + // Show real profile once fetched; fall back to npub after the + // first attempt settles even if nothing was found. + const profileReady = profileAttempted; + const submitLabel = loading + ? "Creating..." + : mnemonic.trim() + ? "Restore wallet" + : "Create wallet"; + + return ( +
+ {stepIndicator} + + + {/* Identity card — positioned above the title so the password + fields below are unambiguously scoped to the wallet, not the + signer-held Nostr identity. */} +

+ Signed in as +

+
+ {avatarSrc ? ( + + ) : ( +
+ )} +
+ {profileReady ? ( + <> +

+ {displayName} +

+

+ {nip05 || truncNpub} +

+ + ) : ( + <> +

+ + Loading profile… +

+

+ {truncNpub} +

+ + )} +
+
+ +

+ Create your wallet +

+

+ Deadcat uses a self-custodial wallet on Bitcoin's Liquid Network. Your + password encrypts this wallet on this device, separate from your + remote signer. +

+ + {errorHtml &&
{errorHtml}
} + + {/* Password */} +
+ + useStore.setState({ onboardingWalletPassword: val }) + } + onConfirmChange={(val) => + useStore.setState({ onboardingWalletPasswordConfirm: val }) + } + onToggleReveal={() => + useStore.setState((s) => ({ + onboardingPasswordRevealed: !s.onboardingPasswordRevealed, + })) + } + /> +
+ + {/* Primary action — new-wallet creation is the default path */} + + + {/* Secondary path — existing users can expand to paste a mnemonic. + When populated, the primary button label flips to "Restore wallet". */} +
+ {showRecoveryInput ? ( +
+ +