From 006a8d70f904005928ad36868b8a6271a4c36c73 Mon Sep 17 00:00:00 2001 From: grunch Date: Fri, 24 Jul 2026 23:05:54 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(cashu):=20C2=20=E2=80=94=20embedded=20?= =?UTF-8?q?Cashu=20wallet=20over=20cdk=20(native),=20typed=20stub=20on=20w?= =?UTF-8?q?eb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C2 of docs/cashu/README.md. The seller needs ecash at the node's mint before an escrow can be locked and the buyer needs somewhere for redeemed ecash to land, so this is a prerequisite for every later phase rather than a feature on its own. No UI yet. Inert on Lightning: every entry point in `api/cashu.rs` returns `CashuNotEnabled` unless `escrow_mode::is_cashu_mode()` is true, which needs the active node to have advertised Cashu *and* a usable mint. Nothing connects at startup — a Lightning user never opens a proof store or contacts a mint. Wallet (`rust/src/cashu/`) - `CashuWallet::connect` binds to one mint (the node pins it; a second mint would have no counterparty) and verifies NUT-07, NUT-11, NUT-12 and a `sat` keyset up front. Checking at connect rather than at first use matters: a seller who funds a wallet at a mint that cannot do NUT-11 would otherwise find out when the escrow lock fails, with the sats already there. - `balance`, `receive_token` (swap-in, DLEQ-verified), `create_token`, `check_proofs_state` (NUT-07 reconciliation of interrupted sends). - Seed is the identity's BIP-39 seed, so the ecash is recoverable from the words the user already backed up — one secret to protect, not two. - Proofs live in `cashu.sqlite`, a sibling of the app DB derived from the path Dart already passes to `init_db`. cdk owns that file's schema; mixing it into the app's would put two migration systems on one file. - Web gets a typed stub (`CashuUnsupportedOnWeb`), mirroring `nwc::client`. The gap is storage only: cdk itself compiles to wasm, cdk-sqlite does not. Bridge (`rust/src/api/cashu.rs`) - `cashu_connect`, `cashu_status`, `cashu_get_balance`, `cashu_receive_token`, `cashu_create_token`, `cashu_check_proofs_state`, `cashu_disconnect` and an `on_cashu_wallet_changed` stream. Errors are stable markers; Dart localizes. - `cashu_status` is deliberately ungated — the UI asks before it knows what the node runs, and "not connected" is truthful on every node. Dependencies pinned `=0.17.3` with default features off. docs/cashu/cdk-spike.md records what was verified against that version: NUT-11 with custom tags is fully exposed (so C4 needs no hand-built secrets), the wallet database trait is wasm-shaped but has 50 methods and no IndexedDB backend (so C9 stays a phase), and cdk compiles to wasm32 today. Tests: capability gating and its markers, every bridge entry point shut on a Lightning node, idempotent disconnect, proof-store path refusal without a DB. Integration tests against a local nutshell are `#[ignore]` behind MOSTRO_TEST_MINT_URL — a mock would have to fake blind signatures and DLEQ, and a wallet that passes against faked cryptography has proven nothing. --- docs/cashu/README.md | 4 +- docs/cashu/cdk-spike.md | 105 +++ rust/Cargo.lock | 1398 ++++++++++++++++++++++++++++++++++--- rust/Cargo.toml | 9 + rust/src/api/cashu.rs | 331 +++++++++ rust/src/api/identity.rs | 19 + rust/src/api/mod.rs | 1 + rust/src/api/types.rs | 20 + rust/src/cashu/mod.rs | 84 +++ rust/src/cashu/wallet.rs | 395 +++++++++++ rust/src/crypto/keys.rs | 13 + rust/src/db/app_db.rs | 16 + rust/src/frb_generated.rs | 688 +++++++++++++++--- rust/src/lib.rs | 1 + 14 files changed, 2905 insertions(+), 179 deletions(-) create mode 100644 docs/cashu/cdk-spike.md create mode 100644 rust/src/api/cashu.rs create mode 100644 rust/src/cashu/mod.rs create mode 100644 rust/src/cashu/wallet.rs diff --git a/docs/cashu/README.md b/docs/cashu/README.md index aedce43d..01484d5b 100644 --- a/docs/cashu/README.md +++ b/docs/cashu/README.md @@ -383,7 +383,9 @@ buyer needs somewhere for redeemed ecash to land — so a minimal embedded walle hard prerequisite, not a nice-to-have. - Add `cdk` + `cdk-sqlite` deps (native); wasm gets a typed "not supported yet" stub - (NWC-client pattern, `rust/src/nwc/client.rs`). + (NWC-client pattern, `rust/src/nwc/client.rs`). Pinned at `=0.17.3` — what was + verified against that version, and how to upgrade, is in + [`cdk-spike.md`](cdk-spike.md). - `rust/src/cashu/mod.rs` — `CashuWallet`: - `connect(mint_url)` — reachability + **required NUTs 07/11/12** + `sat` keyset (mirror of daemon `CashuClient::connect`); diff --git a/docs/cashu/cdk-spike.md b/docs/cashu/cdk-spike.md new file mode 100644 index 00000000..1b5227ef --- /dev/null +++ b/docs/cashu/cdk-spike.md @@ -0,0 +1,105 @@ +# cdk spike — what was verified before C2 + +Findings from a throwaway crate built against **cdk 0.17.3** before writing the +wallet phase. The spike itself is not in the repo; this is what it established, +and it is the justification for the version pin in `rust/Cargo.toml`. + +Everything below was *run*, not read off documentation. + +## 1. NUT-11 with custom tags — yes, fully + +This was the question that could have doubled the size of C4. It does not. + +`cashu` (re-exported through `cdk::nuts`) exposes the whole NUT-11 surface: + +```rust +Conditions::new( + Some(locktime), // unix seconds + Some(vec![buyer_pk, seller_pk]), // additional pubkeys + Some(vec![seller_pk]), // refund keys + Some(2), // n_sigs + Some(SigFlag::SigInputs), // sigflag + Some(1), // n_sigs_refund +)?; + +SpendingConditions::P2PKConditions { data: mostro_pk, conditions: Some(conditions) } +``` + +Verified in the spike: + +- the built conditions serialise into a NUT-10 secret containing `P2PK`, + `n_sigs`, `SIG_INPUTS`, `locktime`, `refund` and `n_sigs_refund`; +- `SpendingConditions::try_from(&Secret)` round-trips back to an equal value, so + the client can *read* an escrow secret it did not build — which is what the + buyer must do to verify the seller locked correctly; +- `Proof::sign_p2pk(secret_key)` attaches a per-proof witness signature; +- `SendOptions` carries `conditions`, `p2pk_signing_keys` and + `p2pk_locked_proof_send_mode`, so locking on send **and** spending a locked + input are both first-class. + +**Consequence for C4:** no hand-built secrets, no hand-rolled witness encoding. +C4 builds `Conditions`, hands them to the wallet, and signs. + +## 2. `WalletDatabase` over IndexedDB — possible, but its own phase + +`cdk_common::database::wallet::Database` is declared + +```rust +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +``` + +so the trait is *designed* for a non-`Send` wasm implementation. There is no +ready-made backend, though: `cdk-rexie` and `cdk-indexeddb` do not exist at +0.17.3, and `cdk-sqlite` is `rusqlite` — native only, no wasm feature. + +The trait has **50 async methods** (mints, keysets, quotes, proofs, +transactions, keys). That is a phase, not a detail. + +**Consequence:** C9 stays a real phase. C2 ships native-only with a typed stub +on web, the same split `crate::nwc::client` already uses. Note this compounds +with #233 — the app's *own* IndexedDB backend is a stub too, so "web +persistence" is one problem, not two. + +## 3. wasm32 compilation — confirmed + +``` +cargo add cdk@0.17.3 --no-default-features --features wallet +cargo check --target wasm32-unknown-unknown # clean +``` + +`cdk`'s manifest carries explicit `cfg(target_arch = "wasm32")` dependency +blocks (getrandom with `wasm32_unknown_unknown_js`, gloo-timers, a sync-only +tokio). So the web gap really is storage alone — the protocol half would build +today. + +## 4. The pin: `=0.17.3`, wallet feature only + +```toml +cdk = { version = "=0.17.3", default-features = false, features = ["wallet"] } +cdk-sqlite = { version = "=0.17.3", default-features = false, features = ["wallet"] } +``` + +Exact-pinned, not caret. cdk is pre-1.0: minor releases move the wallet API +(`prepare_send`/`confirm` replaced a direct `send` in this line's history), and +a silent bump would break the escrow construction with no test to catch it +until a trade failed. + +Default features are off deliberately. The default set pulls `mint`, +`cdk-signatory`, `nostr` and `bip353` and, transitively, an HTTP/TLS/tor stack +the app does not use — `wallet` alone is what a client needs. + +### Upgrade procedure + +When moving off 0.17.3: + +1. Re-run the checks above — especially §1, since the escrow secret is the part + with no compile-time guarantee of *semantic* stability. +2. `cargo check --target wasm32-unknown-unknown` (the stub must still build even + though it does not use cdk). +3. Run the `#[ignore]`d integration tests against a local nutshell: + `MOSTRO_TEST_MINT_URL=http://localhost:3338 cargo test -- --ignored`. + They are the only thing that exercises real blind signatures and DLEQ; a unit + test cannot substitute for them. +4. Check `SendOptions` and `ReceiveOptions` field by field — they are built with + `..Default::default()`, so a new field lands silently. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d630339a..e65057d1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -50,6 +50,18 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -70,6 +82,21 @@ dependencies = [ "backtrace", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -113,18 +140,100 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "async-utility" version = "0.3.1" @@ -220,6 +329,12 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -277,6 +392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", + "base64 0.21.7", "bech32", "bitcoin-internals", "bitcoin-io", @@ -285,6 +401,7 @@ dependencies = [ "hex-conservative", "hex_lit", "secp256k1 0.29.1", + "serde", ] [[package]] @@ -292,6 +409,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -299,6 +419,17 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +[[package]] +name = "bitcoin-payment-instructions" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f5b43b0e8338cd36ce7940bd6b8a007d54f1cbe25314ec3fbba566f870b085" +dependencies = [ + "bitcoin", + "lightning", + "lightning-invoice", +] + [[package]] name = "bitcoin-units" version = "0.1.2" @@ -306,6 +437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", + "serde", ] [[package]] @@ -346,6 +478,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.5.1" @@ -353,6 +506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ "sha2", + "tinyvec", ] [[package]] @@ -385,6 +539,32 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cashu" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e035322b7e279fc37a110e5fffdcf987fa5414caea1d0576cedb3b3611cbbb" +dependencies = [ + "bitcoin", + "cbor-diag", + "ciborium", + "lightning", + "lightning-invoice", + "once_cell", + "serde", + "serde_json", + "serde_with", + "strum", + "strum_macros", + "thiserror 2.0.18", + "tracing", + "unicode-normalization", + "url", + "uuid", + "web-time", + "zeroize", +] + [[package]] name = "cbc" version = "0.1.2" @@ -394,6 +574,25 @@ dependencies = [ "cipher", ] +[[package]] +name = "cbor-diag" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc245b6ecd09b23901a4fbad1ad975701fd5061ceaef6afa93a2d70605a64429" +dependencies = [ + "bs58", + "chrono", + "data-encoding", + "half", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "separator", + "url", + "uuid", +] + [[package]] name = "cc" version = "1.2.58" @@ -401,9 +600,166 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cdk" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e19bcd47376859753f86adf4c2f661419ce619c777f92ac0a4e7c0594d4901" +dependencies = [ + "anyhow", + "arc-swap", + "async-trait", + "bitcoin", + "bitcoin-payment-instructions", + "cbor-diag", + "cdk-common", + "cdk-signatory", + "ciborium", + "futures", + "getrandom 0.2.17", + "gloo-timers", + "jsonwebtoken", + "lightning", + "lightning-invoice", + "regex", + "ring", + "rustls", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "url", + "uuid", + "web-time", + "zeroize", +] + +[[package]] +name = "cdk-common" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb4f7239c3ed8ce35d8928324cf73147c802eb4f7cc42ef71827954cda96ac3" +dependencies = [ + "anyhow", + "async-trait", + "bitcoin", + "cashu", + "cbor-diag", + "cdk-http-client", + "ciborium", + "futures", + "getrandom 0.2.17", + "jsonwebtoken", + "lightning", + "lightning-invoice", + "parking_lot", + "paste", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", + "tokio", + "tonic", + "tracing", + "url", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "cdk-http-client" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6089ad04be877f3642729ab01a97b62e90a1ebafaac19583444fd72ffb2d25f" +dependencies = [ + "futures", + "futures-channel", + "js-sys", + "regex", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio-tungstenite", + "tracing", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "cdk-signatory" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bed36ee68cff3260fca23fa485b2c9b31f2a15f6c045f62588c5db006c8964a" +dependencies = [ + "anyhow", + "async-trait", + "bip39", + "bitcoin", + "cdk-common", + "clap", + "getrandom 0.2.17", + "home", + "rustls", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "cdk-sql-common" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed515d34378fa51a5531882ea7c774f1f7af69bd4f50958c120c96f9159ac2f" +dependencies = [ + "async-trait", + "bitcoin", + "cdk-common", + "lightning-invoice", + "once_cell", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "cdk-sqlite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f96798d170b82f860daa83b2fc8f997151e6b77eb9f37c2ff4a92a7b5d650033" +dependencies = [ + "async-trait", + "bitcoin", + "cdk-common", + "cdk-sql-common", + "lightning-invoice", + "paste", + "rusqlite", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -460,10 +816,38 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -475,6 +859,72 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -500,6 +950,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -539,6 +999,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -555,26 +1024,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crypto-bigint" -version = "0.5.5" +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "darling_macro" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", + "darling_core", + "quote", + "syn 2.0.117", ] [[package]] @@ -613,7 +1122,7 @@ checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -625,7 +1134,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -639,6 +1148,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + [[package]] name = "digest" version = "0.10.7" @@ -659,7 +1177,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "dnssec-prover" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4f825369fc7134da70ca4040fddc8e03b80a46d249ae38d9c1c39b7b4476bf" +dependencies = [ + "bitcoin_hashes", ] [[package]] @@ -668,6 +1195,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -727,6 +1260,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -749,6 +1292,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy_constructor" version = "1.3.0" @@ -758,9 +1313,15 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "ff" version = "0.13.1" @@ -777,6 +1338,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -827,7 +1398,7 @@ dependencies = [ "md-5", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -912,7 +1483,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1025,11 +1596,38 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -1037,8 +1635,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -1050,11 +1646,11 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.14.5", ] [[package]] @@ -1187,6 +1783,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1200,7 +1797,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1328,6 +1925,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1366,6 +1969,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1416,12 +2030,28 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.92" @@ -1434,6 +2064,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -1489,15 +2134,70 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "lightning" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1ab99a13ab3343a9b4d1d25c455bf72322819fc5a5f05c5182e704528a583" +dependencies = [ + "bech32", + "bitcoin", + "dnssec-prover", + "hashbrown 0.13.2", + "libm", + "lightning-invoice", + "lightning-macros", + "lightning-types", + "possiblyrandom", +] + +[[package]] +name = "lightning-invoice" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d83bd798e04ab9eecc8bbef1fa17d3808859bcdc0406bd16c55d51c8834444" +dependencies = [ + "bech32", + "bitcoin", + "lightning-types", + "serde", +] + +[[package]] +name = "lightning-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c717494cdc2c8bb85bee7113031248f5f6c64f8802b33c1c9e2d98e594aa71" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "lightning-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c211dfcff95ca308247da8b1e0e81604bc9e568239967cd2c34572558511e869" +dependencies = [ + "bitcoin", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -1551,7 +2251,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1562,7 +2262,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1575,7 +2275,16 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", ] [[package]] @@ -1594,6 +2303,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1601,6 +2316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1635,6 +2351,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nostr" version = "0.44.2" @@ -1642,7 +2368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ "aes", - "base64", + "base64 0.22.1", "bech32", "bip39", "bitcoin_hashes", @@ -1713,6 +2439,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1729,6 +2474,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -1749,6 +2500,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1784,12 +2546,24 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "oslog" version = "0.2.0" @@ -1841,6 +2615,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1851,6 +2631,16 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1866,6 +2656,26 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1928,6 +2738,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "possiblyrandom" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c564dbf654befd49035528299f1208a40508f6e07efb11c163444e304e4484f" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1937,6 +2756,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1953,7 +2778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -2135,6 +2960,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "regex" version = "1.12.3" @@ -2170,7 +3015,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -2185,6 +3030,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -2255,15 +3101,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust" version = "0.1.0" dependencies = [ "android_logger 0.14.1", "anyhow", - "base64", + "base64 0.22.1", "bip32", "bip39", + "cdk", + "cdk-sqlite", "chacha20poly1305", "flutter_rust_bridge", "hex", @@ -2297,6 +3159,19 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.37" @@ -2311,6 +3186,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2353,6 +3240,39 @@ dependencies = [ "cipher", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2424,12 +3344,41 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "separator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" + [[package]] name = "serde" version = "1.0.228" @@ -2457,7 +3406,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2485,6 +3434,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2507,12 +3488,31 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2523,6 +3523,24 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -2567,11 +3585,21 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + [[package]] name = "sqlx" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2582,32 +3610,37 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" dependencies = [ - "base64", + "atoi", + "byteorder", "bytes", "crc", "crossbeam-queue", "either", "event-listener", + "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.14.5", "hashlink", - "indexmap", + "hex", + "indexmap 2.13.0", "log", "memchr", "once_cell", + "paste", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "sqlformat", + "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", @@ -2616,22 +3649,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.117", ] [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" dependencies = [ "dotenvy", "either", @@ -2647,19 +3680,20 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.117", + "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -2689,19 +3723,19 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 1.0.69", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -2709,6 +3743,7 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", + "futures-io", "futures-util", "hex", "hkdf", @@ -2726,16 +3761,16 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 1.0.69", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" dependencies = [ "atoi", "flume", @@ -2750,7 +3785,6 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", "tracing", "url", ] @@ -2772,6 +3806,30 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2789,6 +3847,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2806,7 +3875,20 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2835,7 +3917,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2846,7 +3928,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", ] [[package]] @@ -2858,6 +3949,36 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2892,7 +4013,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2906,7 +4029,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2951,6 +4074,7 @@ dependencies = [ "futures-util", "log", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2958,6 +4082,39 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "base64 0.22.1", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -2979,13 +4136,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -3023,7 +4185,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3033,6 +4195,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -3099,6 +4291,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3140,6 +4338,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.0" @@ -3153,6 +4357,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3246,7 +4456,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3276,7 +4486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -3289,7 +4499,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "semver", ] @@ -3376,7 +4586,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3387,7 +4597,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3664,9 +4874,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.13.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3682,7 +4892,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3695,7 +4905,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -3714,7 +4924,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.0", "log", "semver", "serde", @@ -3749,7 +4959,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3770,7 +4980,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3790,7 +5000,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3830,7 +5040,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3838,3 +5048,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f08040e5..6eae2c38 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -57,6 +57,15 @@ bip39 = { version = "2", features = ["all-languages", "rand"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros", "migrate"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +# Cashu wallet (phase C2 of docs/cashu/README.md). Exact-pinned: cdk is pre-1.0 +# and its wallet API moves between minor releases — docs/cashu/cdk-spike.md +# records what was verified against this version and how to upgrade. +# `wallet` only: the mint, signatory, nostr and tor features are server-side or +# unused here, and each drags in a large dependency subtree. +cdk = { version = "=0.17.3", default-features = false, features = ["wallet"] } +# Proof storage. Native by construction (rusqlite), which is why the web build +# gets a typed stub instead — see rust/src/cashu/mod.rs. +cdk-sqlite = { version = "=0.17.3", default-features = false, features = ["wallet"] } # Android logging backend [target.'cfg(target_os = "android")'.dependencies] diff --git a/rust/src/api/cashu.rs b/rust/src/api/cashu.rs new file mode 100644 index 00000000..c152688f --- /dev/null +++ b/rust/src/api/cashu.rs @@ -0,0 +1,331 @@ +//! Cashu wallet surface for the UI — phase C2 of `docs/cashu/README.md`. +//! +//! Holds the single process-wide wallet, gates every entry point on the escrow +//! mode, and broadcasts changes so the UI never polls. +//! +//! **Nothing here runs on a Lightning node.** Every function returns +//! `CashuNotEnabled` unless [`crate::mostro::escrow_mode::is_cashu_mode`] is +//! true, which requires the active node to have advertised Cashu *and* a usable +//! mint. That gate is the whole reason this module is inert by default. +//! +//! Errors are stable markers (`CashuNotEnabled`, `CashuNotConnected`, +//! `CashuMintUnreachable`, …); Dart maps them to localized strings. + +use anyhow::{bail, Result}; +use std::sync::OnceLock; +use tokio::sync::broadcast::error::RecvError; +use tokio::sync::{broadcast, RwLock}; + +use crate::api::types::CashuWalletStatus; +use crate::cashu::CashuWallet; +use crate::mostro::escrow_mode; + +// ── Global wallet ───────────────────────────────────────────────────────────── + +fn wallet_lock() -> &'static RwLock> { + static WALLET: OnceLock>> = OnceLock::new(); + WALLET.get_or_init(|| RwLock::new(None)) +} + +fn changes() -> &'static broadcast::Sender { + static CHANGES: OnceLock> = OnceLock::new(); + CHANGES.get_or_init(|| broadcast::channel(32).0) +} + +/// Where the proof store lives: a sibling of the app database, never inside it. +/// +/// `cdk` owns that file's schema and migrations; mixing it into the app's would +/// put two migration systems on one file. +fn proof_store_path() -> Result { + let app_db = crate::db::app_db::app_db_path() + .ok_or_else(|| anyhow::anyhow!("CashuStoreUnavailable: database not initialised"))?; + + let path = std::path::Path::new(app_db); + let dir = path.parent().unwrap_or_else(|| std::path::Path::new(".")); + Ok(dir.join("cashu.sqlite").to_string_lossy().into_owned()) +} + +/// Fail closed unless the active node was positively identified as Cashu. +fn ensure_enabled() -> Result<()> { + if !escrow_mode::is_cashu_mode() { + bail!("CashuNotEnabled"); + } + Ok(()) +} + +async fn snapshot() -> CashuWalletStatus { + let guard = wallet_lock().read().await; + match guard.as_ref() { + Some(wallet) => CashuWalletStatus { + connected: true, + mint_url: Some(wallet.mint_url().to_string()), + // A failed balance read reports zero rather than tearing down the + // status: the wallet is still connected, and the next event will + // carry the real figure. + balance_sats: wallet.balance().await.unwrap_or_else(|e| { + log::warn!("[cashu] balance read failed: {e}"); + 0 + }), + missing_capabilities: wallet + .capabilities() + .missing() + .into_iter() + .map(str::to_string) + .collect(), + }, + None => CashuWalletStatus { + connected: false, + mint_url: None, + balance_sats: 0, + missing_capabilities: Vec::new(), + }, + } +} + +async fn notify() { + let _ = changes().send(snapshot().await); +} + +// ── Public API ──────────────────────────────────────────────────────────────── + +/// Connect the wallet to the mint the active node pins, unless already connected. +/// +/// Lazy by design: nothing connects at startup, so a Lightning user never opens +/// a proof store or contacts a mint. Repeat calls are cheap — an already +/// connected wallet is returned as is rather than reconnected. +/// +/// **Errors**: `CashuNotEnabled` when the node is not a usable Cashu node, +/// `NoIdentity` before an identity is loaded, plus the markers from +/// [`CashuWallet::connect`]. +pub async fn cashu_connect() -> Result { + ensure_enabled()?; + + { + let guard = wallet_lock().read().await; + if guard.is_some() { + drop(guard); + return Ok(snapshot().await); + } + } + + // The gate above implies a mint URL, but a concurrent node switch could + // have cleared it — handled rather than unwrapped. + let mint_url = escrow_mode::get_resolved() + .config + .mint_url + .ok_or_else(|| anyhow::anyhow!("CashuNotEnabled"))?; + + let seed = crate::api::identity::current_bip39_seed() + .await + .ok_or_else(|| anyhow::anyhow!("NoIdentity"))?; + + let db_path = proof_store_path()?; + let wallet = CashuWallet::connect(&mint_url, seed, &db_path).await?; + + { + let mut guard = wallet_lock().write().await; + // Another task may have connected while this one awaited the mint; + // keep the live wallet rather than replacing it. + if guard.is_none() { + *guard = Some(wallet); + } + } + + notify().await; + Ok(snapshot().await) +} + +/// Current wallet status. Safe to call on any node — a Lightning node simply +/// reports "not connected". +pub async fn cashu_status() -> Result { + Ok(snapshot().await) +} + +/// Spendable balance in satoshis. +/// +/// **Errors**: `CashuNotEnabled`, `CashuNotConnected`. +pub async fn cashu_get_balance() -> Result { + ensure_enabled()?; + let guard = wallet_lock().read().await; + let wallet = guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("CashuNotConnected"))?; + wallet.balance().await +} + +/// Redeem an encoded Cashu token into the wallet, returning the amount received. +/// +/// **Errors**: `CashuNotEnabled`, `CashuNotConnected`, `CashuReceiveFailed` +/// (wrong mint, already spent, malformed). +pub async fn cashu_receive_token(encoded: String) -> Result { + ensure_enabled()?; + let amount = { + let guard = wallet_lock().read().await; + let wallet = guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("CashuNotConnected"))?; + wallet.receive_token(&encoded).await? + }; + notify().await; + Ok(amount) +} + +/// Export `amount_sats` from the wallet as an encoded token. +/// +/// **Errors**: `CashuNotEnabled`, `CashuNotConnected`, `CashuAmountZero`, +/// `CashuSendFailed` (insufficient funds included). +pub async fn cashu_create_token(amount_sats: u64) -> Result { + ensure_enabled()?; + let token = { + let guard = wallet_lock().read().await; + let wallet = guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("CashuNotConnected"))?; + wallet.create_token(amount_sats).await? + }; + notify().await; + Ok(token) +} + +/// Reconcile pending proofs with the mint, returning the amount reclaimed. +/// +/// **Errors**: `CashuNotEnabled`, `CashuNotConnected`. +pub async fn cashu_check_proofs_state() -> Result { + ensure_enabled()?; + let reclaimed = { + let guard = wallet_lock().read().await; + let wallet = guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("CashuNotConnected"))?; + wallet.check_proofs_state().await? + }; + if reclaimed > 0 { + notify().await; + } + Ok(reclaimed) +} + +/// Drop the in-memory wallet. Proofs stay on disk — this is a disconnect, not a +/// wipe. Called when the active node changes, so a wallet bound to one node's +/// mint never serves another's. +pub async fn cashu_disconnect() -> Result<()> { + { + let mut guard = wallet_lock().write().await; + *guard = None; + } + notify().await; + Ok(()) +} + +// ── Stream ──────────────────────────────────────────────────────────────────── + +/// Emits the wallet status whenever it changes: connect, receive, send, reclaim +/// or disconnect. +pub struct CashuWalletStream { + rx: broadcast::Receiver, +} + +impl CashuWalletStream { + /// Poll for the next wallet-changed event. + /// + /// A lagged receiver skips dropped snapshots: the value is current state, + /// so only the newest one matters. + pub async fn next(&mut self) -> Result { + loop { + match self.rx.recv().await { + Ok(status) => return Ok(status), + Err(RecvError::Lagged(_)) => continue, + Err(RecvError::Closed) => bail!("CashuWalletStream closed"), + } + } + } +} + +/// Subscribe to wallet changes. +pub fn on_cashu_wallet_changed() -> CashuWalletStream { + CashuWalletStream { + rx: changes().subscribe(), + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + /// The escrow globals are process-wide; serialize the tests that read them + /// and start from a node that has advertised nothing. + fn escrow_lock() -> std::sync::MutexGuard<'static, ()> { + static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + let guard = LOCK.lock().unwrap_or_else(|e| e.into_inner()); + escrow_mode::clear(); + guard + } + + #[tokio::test] + async fn every_entry_point_is_shut_on_a_lightning_node() { + // Arrange — the default state: nothing fetched, so not Cashu. + let _g = escrow_lock(); + + // Act / Assert — the gate is the whole safety story, so check every + // door rather than trusting one of them. + for err in [ + cashu_connect().await.unwrap_err(), + cashu_get_balance().await.unwrap_err(), + cashu_receive_token("cashuBanything".to_string()) + .await + .unwrap_err(), + cashu_create_token(1).await.unwrap_err(), + cashu_check_proofs_state().await.unwrap_err(), + ] { + assert!( + err.to_string().contains("CashuNotEnabled"), + "expected the gate to close, got {err}" + ); + } + } + + #[tokio::test] + async fn status_is_answerable_on_any_node_and_reports_disconnected() { + // Arrange + let _g = escrow_lock(); + + // Act — status is deliberately ungated: the UI asks before it knows + // anything, and "not connected" is truthful everywhere. + let status = cashu_status().await.unwrap(); + + // Assert + assert!(!status.connected); + assert_eq!(status.balance_sats, 0); + assert_eq!(status.mint_url, None); + } + + #[tokio::test] + async fn disconnect_is_idempotent_and_notifies() { + // Arrange + let _g = escrow_lock(); + let mut stream = on_cashu_wallet_changed(); + + // Act — disconnecting a wallet that never existed must not error: this + // runs on every node switch. + cashu_disconnect().await.unwrap(); + + // Assert + let status = stream.next().await.unwrap(); + assert!(!status.connected); + } + + #[test] + fn the_proof_store_needs_an_initialised_database() { + // Arrange / Act — with no app DB there is nowhere to put the store, + // and guessing a path would create one the user never sees. + let err = proof_store_path().unwrap_err(); + + // Assert + assert!( + err.to_string().contains("CashuStoreUnavailable"), + "got {err}" + ); + } +} diff --git a/rust/src/api/identity.rs b/rust/src/api/identity.rs index 0cde9ecc..0b455c0a 100644 --- a/rust/src/api/identity.rs +++ b/rust/src/api/identity.rs @@ -200,6 +200,25 @@ pub async fn get_identity() -> Result> { Ok(guard.as_ref().map(|s| s.identity_info.clone())) } +/// The BIP-39 seed of the loaded identity, or `None` when none is loaded. +/// +/// Crate-internal on purpose — it is *not* part of the bridge surface, and FRB +/// skips it because it is not `pub`. The Cashu wallet (phase C2) needs a +/// 64-byte seed to derive its blinding secrets, and reusing this one is what +/// makes the ecash recoverable from the words the user already backed up. +pub(crate) async fn current_bip39_seed() -> Option<[u8; 64]> { + let guard = identity_lock().read().await; + let state = guard.as_ref()?; + match key_ops::derive_bip39_seed(&state.mnemonic_words) { + Ok(seed) => Some(seed), + Err(e) => { + // Unreachable in practice: the mnemonic was validated on the way in. + log::error!("[identity] seed derivation failed for a loaded identity: {e}"); + None + } + } +} + /// Delete the in-memory identity state. Flutter must also clear /// `flutter_secure_storage` after calling this. pub async fn delete_identity() -> Result<()> { diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index c3f6a65a..d0963a2d 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -1,4 +1,5 @@ pub mod bond; +pub mod cashu; pub mod disputes; pub mod identity; pub mod logging; diff --git a/rust/src/api/types.rs b/rust/src/api/types.rs index cf91ec40..cb68028a 100644 --- a/rust/src/api/types.rs +++ b/rust/src/api/types.rs @@ -503,6 +503,26 @@ pub struct Dispute { pub is_read: bool, } +/// State of the embedded Cashu wallet — phase C2 of `docs/cashu/README.md`. +/// +/// Reported for every node, including Lightning ones, where it is simply +/// "not connected": the UI asks before it knows what the node runs. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CashuWalletStatus { + /// Whether a wallet is bound to a mint right now. False on any Lightning + /// node, and before the first successful connect on a Cashu one. + pub connected: bool, + /// The mint the wallet is bound to, when connected. + pub mint_url: Option, + /// Spendable balance in satoshis. Zero when not connected. + pub balance_sats: u64, + /// Stable markers for anything the mint failed to advertise (`"nut07"`, + /// `"nut11"`, `"nut12"`, `"sat_keyset"`). Empty on a healthy connection — + /// a mint missing any of them is refused at connect, so a non-empty list + /// here means the wallet is bound to a mint that has since changed. + pub missing_capabilities: Vec, +} + /// Aggregated user-facing application settings. /// /// `privacy_mode` is a read-only mirror of `IdentityInfo.privacy_mode` — diff --git a/rust/src/cashu/mod.rs b/rust/src/cashu/mod.rs new file mode 100644 index 00000000..328c5c50 --- /dev/null +++ b/rust/src/cashu/mod.rs @@ -0,0 +1,84 @@ +//! Embedded Cashu wallet — phase C2 of `docs/cashu/README.md`. +//! +//! The seller needs ecash **at the node's mint** before they can lock an +//! escrow, and the buyer needs somewhere for redeemed ecash to land, so a +//! minimal wallet is a prerequisite for every later Cashu phase rather than a +//! nice-to-have. +//! +//! Scope, deliberately small: connect to one mint, hold `sat` proofs, receive a +//! token, export a token, and re-check proof state. Melting to Lightning, +//! multi-mint support and NUT-11 locking are later phases. +//! +//! **Native only.** Proof storage is `cdk-sqlite`, which is `rusqlite` and does +//! not build for `wasm32`. The web target gets the typed stub at the bottom of +//! this file — same shape, every call fails with a stable marker — mirroring +//! how `crate::nwc::client` handles the same split. `cdk` itself *does* compile +//! to wasm (verified; see `docs/cashu/cdk-spike.md`), so the web gap is storage +//! alone, and closing it belongs with the rest of IndexedDB in #233. + +#[cfg(not(target_arch = "wasm32"))] +mod wallet; + +#[cfg(not(target_arch = "wasm32"))] +pub use wallet::{CashuWallet, MintCapabilities}; + +// ── WASM stub ──────────────────────────────────────────────────────────────── + +/// What a mint advertises that this client needs. See the native +/// [`wallet::MintCapabilities`] for the real definition. +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone, Default)] +pub struct MintCapabilities { + pub nut07_state_check: bool, + pub nut11_p2pk: bool, + pub nut12_dleq: bool, + pub has_sat_keyset: bool, +} + +#[cfg(target_arch = "wasm32")] +impl MintCapabilities { + pub fn is_usable(&self) -> bool { + false + } + + pub fn missing(&self) -> Vec<&'static str> { + vec!["storage"] + } +} + +/// Web build: no proof storage, so no wallet. Every call fails with the +/// `CashuUnsupportedOnWeb` marker, which Dart maps to a localized string — +/// Rust does not translate. +#[cfg(target_arch = "wasm32")] +pub struct CashuWallet; + +#[cfg(target_arch = "wasm32")] +impl CashuWallet { + pub async fn connect(_mint_url: &str, _seed: [u8; 64], _db_path: &str) -> anyhow::Result { + anyhow::bail!("CashuUnsupportedOnWeb") + } + + pub fn mint_url(&self) -> &str { + "" + } + + pub fn capabilities(&self) -> &MintCapabilities { + unreachable!("no wasm wallet can be constructed") + } + + pub async fn balance(&self) -> anyhow::Result { + anyhow::bail!("CashuUnsupportedOnWeb") + } + + pub async fn receive_token(&self, _encoded: &str) -> anyhow::Result { + anyhow::bail!("CashuUnsupportedOnWeb") + } + + pub async fn create_token(&self, _amount_sats: u64) -> anyhow::Result { + anyhow::bail!("CashuUnsupportedOnWeb") + } + + pub async fn check_proofs_state(&self) -> anyhow::Result { + anyhow::bail!("CashuUnsupportedOnWeb") + } +} diff --git a/rust/src/cashu/wallet.rs b/rust/src/cashu/wallet.rs new file mode 100644 index 00000000..2df8b063 --- /dev/null +++ b/rust/src/cashu/wallet.rs @@ -0,0 +1,395 @@ +//! Native Cashu wallet over `cdk` — see [`super`] for scope and the wasm split. + +use std::sync::Arc; + +use anyhow::{anyhow, bail, Result}; +use cdk::amount::SplitTarget; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions, Wallet}; +use cdk::Amount; +use cdk_sqlite::WalletSqliteDatabase; + +/// Everything the escrow flow needs a mint to support. +/// +/// Checked once at connect rather than at the first failing operation: a seller +/// who funds a wallet at a mint that cannot do NUT-11 would only find out when +/// the escrow lock fails, with their sats already sitting at that mint. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MintCapabilities { + /// NUT-07 — proof state check. Without it the wallet cannot tell a spent + /// proof from a live one after an interrupted trade. + pub nut07_state_check: bool, + /// NUT-11 — P2PK spending conditions. The escrow lock *is* a P2PK secret. + pub nut11_p2pk: bool, + /// NUT-12 — DLEQ proofs, so a received token can be verified as genuinely + /// issued by this mint rather than taken on trust. + pub nut12_dleq: bool, + /// A keyset denominated in `sat`. Mostro amounts are satoshis. + pub has_sat_keyset: bool, +} + +impl MintCapabilities { + /// All four, or the mint is not one we can trade against. + pub fn is_usable(&self) -> bool { + self.nut07_state_check && self.nut11_p2pk && self.nut12_dleq && self.has_sat_keyset + } + + /// Stable markers for what is missing. Rust does not translate; Dart maps + /// these to localized strings. + pub fn missing(&self) -> Vec<&'static str> { + let mut missing = Vec::new(); + if !self.nut07_state_check { + missing.push("nut07"); + } + if !self.nut11_p2pk { + missing.push("nut11"); + } + if !self.nut12_dleq { + missing.push("nut12"); + } + if !self.has_sat_keyset { + missing.push("sat_keyset"); + } + missing + } +} + +/// A wallet bound to exactly one mint. +/// +/// One mint is not a simplification to revisit later: the Mostro node pins the +/// mint for every escrow (§2 of the Cashu doc), so ecash at any other mint has +/// no counterparty to trade with. +pub struct CashuWallet { + inner: Wallet, + mint_url: String, + capabilities: MintCapabilities, +} + +/// Hand-written so a failed `connect` can be unwrapped in tests and logged +/// without pulling `cdk::Wallet` into the output — it holds the seed, and a +/// derived `Debug` would risk printing key material into a log. +impl std::fmt::Debug for CashuWallet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CashuWallet") + .field("mint_url", &self.mint_url) + .field("capabilities", &self.capabilities) + .finish_non_exhaustive() + } +} + +impl CashuWallet { + /// Open the local proof store, bind to `mint_url` and verify it is usable. + /// + /// `seed` is the identity's BIP-39 seed, so the ecash is recoverable from + /// the words the user already backed up. `db_path` is the proof store's own + /// SQLite file — never the app's, whose schema is unrelated and whose + /// migrations must not run against `cdk`'s tables. + /// + /// **Errors** (stable markers): `CashuMintUnreachable` when the mint does + /// not answer, `CashuMintUnusable` when it answers but lacks a required NUT + /// or the `sat` keyset. + pub async fn connect(mint_url: &str, seed: [u8; 64], db_path: &str) -> Result { + let localstore = WalletSqliteDatabase::new(db_path) + .await + .map_err(|e| anyhow!("CashuStoreUnavailable: {e}"))?; + + let inner = Wallet::new( + mint_url, + CurrencyUnit::Sat, + Arc::new(localstore), + seed, + // cdk's own default: keep roughly this many proofs per denomination + // so an ordinary send rarely needs an extra swap round trip. + None, + ) + .map_err(|e| anyhow!("CashuWalletInit: {e}"))?; + + let capabilities = Self::probe(&inner).await?; + if !capabilities.is_usable() { + bail!("CashuMintUnusable: {}", capabilities.missing().join(",")); + } + + log::info!("[cashu] connected to {mint_url}"); + Ok(Self { + inner, + mint_url: mint_url.to_string(), + capabilities, + }) + } + + /// Ask the mint what it supports. Reachability is proven by the same call, + /// so there is no separate ping. + async fn probe(wallet: &Wallet) -> Result { + let info = wallet + .fetch_mint_info() + .await + .map_err(|e| anyhow!("CashuMintUnreachable: {e}"))? + .ok_or_else(|| anyhow!("CashuMintUnreachable: mint published no info"))?; + + let keysets = wallet + .load_mint_keysets() + .await + .map_err(|e| anyhow!("CashuMintUnreachable: {e}"))?; + + Ok(MintCapabilities { + nut07_state_check: info.nuts.nut07.supported, + nut11_p2pk: info.nuts.nut11.supported, + nut12_dleq: info.nuts.nut12.supported, + has_sat_keyset: keysets.iter().any(|k| k.unit == CurrencyUnit::Sat), + }) + } + + /// The mint this wallet is bound to. + pub fn mint_url(&self) -> &str { + &self.mint_url + } + + /// What the mint advertised at connect time. + pub fn capabilities(&self) -> &MintCapabilities { + &self.capabilities + } + + /// Spendable balance in satoshis. + pub async fn balance(&self) -> Result { + let amount = self + .inner + .total_balance() + .await + .map_err(|e| anyhow!("CashuBalance: {e}"))?; + Ok(u64::from(amount)) + } + + /// Swap an encoded token into this wallet, returning the amount received. + /// + /// `cdk` swaps the incoming proofs for fresh ones at the mint, so a token + /// that was also copied elsewhere cannot be spent twice against us, and it + /// verifies the mint's DLEQ proofs on the way in (NUT-12, required at + /// connect). A token from another mint is rejected rather than ignored. + pub async fn receive_token(&self, encoded: &str) -> Result { + let amount = self + .inner + .receive(encoded.trim(), ReceiveOptions::default()) + .await + .map_err(|e| anyhow!("CashuReceiveFailed: {e}"))?; + log::info!("[cashu] received {amount} sat"); + Ok(u64::from(amount)) + } + + /// Export `amount_sats` as an encoded token. + /// + /// The proofs are reserved before the token is handed out and only settle + /// once the recipient redeems them, so an abandoned token can be reclaimed + /// by [`Self::check_proofs_state`] instead of being lost. + pub async fn create_token(&self, amount_sats: u64) -> Result { + if amount_sats == 0 { + bail!("CashuAmountZero"); + } + + let prepared = self + .inner + .prepare_send( + Amount::from(amount_sats), + SendOptions { + amount_split_target: SplitTarget::default(), + ..Default::default() + }, + ) + .await + .map_err(|e| anyhow!("CashuSendFailed: {e}"))?; + + let token = prepared + .confirm(None::) + .await + .map_err(|e| anyhow!("CashuSendFailed: {e}"))?; + + Ok(token.to_string()) + } + + /// Reconcile pending proofs with the mint (NUT-07), returning the amount + /// reclaimed as spendable. + /// + /// Run after any interrupted send: proofs reserved for a token that was + /// never redeemed stay reserved otherwise, and the balance would understate + /// what the user actually has. + pub async fn check_proofs_state(&self) -> Result { + let reclaimed = self + .inner + .check_all_pending_proofs() + .await + .map_err(|e| anyhow!("CashuStateCheckFailed: {e}"))?; + if u64::from(reclaimed) > 0 { + log::info!("[cashu] reclaimed {reclaimed} sat from unredeemed proofs"); + } + Ok(u64::from(reclaimed)) + } +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + /// Integration tests need a real mint. Point this at a local + /// [nutshell](https://github.com/cashubtc/nutshell): + /// + /// ```text + /// docker run -p 3338:3338 cashubtc/nutshell:latest poetry run mint + /// MOSTRO_TEST_MINT_URL=http://localhost:3338 cargo test -- --ignored + /// ``` + /// + /// They are `#[ignore]` so CI stays green without one. A mock is not an + /// option here: it would have to fake blind signatures and DLEQ proofs, and + /// a wallet that passes against faked cryptography has proven nothing. + fn test_mint_url() -> String { + std::env::var("MOSTRO_TEST_MINT_URL") + .expect("set MOSTRO_TEST_MINT_URL to run the Cashu integration tests") + } + + fn temp_db_path() -> std::path::PathBuf { + use std::sync::atomic::{AtomicU32, Ordering}; + static COUNTER: AtomicU32 = AtomicU32::new(0); + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + std::env::temp_dir().join(format!("mostro_cashu_test_{}_{n}.db", std::process::id())) + } + + #[test] + fn a_mint_missing_any_requirement_is_unusable() { + // Arrange — everything present. + let all = MintCapabilities { + nut07_state_check: true, + nut11_p2pk: true, + nut12_dleq: true, + has_sat_keyset: true, + }; + + // Assert + assert!(all.is_usable()); + assert!(all.missing().is_empty()); + + // Act / Assert — dropping any single requirement closes the door, and + // the marker says which. There is no partial "works for now" state. + for (dropped, marker) in [ + ( + MintCapabilities { + nut07_state_check: false, + ..all.clone() + }, + "nut07", + ), + ( + MintCapabilities { + nut11_p2pk: false, + ..all.clone() + }, + "nut11", + ), + ( + MintCapabilities { + nut12_dleq: false, + ..all.clone() + }, + "nut12", + ), + ( + MintCapabilities { + has_sat_keyset: false, + ..all.clone() + }, + "sat_keyset", + ), + ] { + assert!(!dropped.is_usable(), "{marker} must be required"); + assert_eq!(dropped.missing(), vec![marker]); + } + } + + #[test] + fn a_fresh_capability_set_is_unusable() { + // Assert — the default must fail closed, so a probe that never ran + // cannot be mistaken for a usable mint. + let unknown = MintCapabilities::default(); + assert!(!unknown.is_usable()); + assert_eq!(unknown.missing().len(), 4); + } + + #[tokio::test] + #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] + async fn connects_to_a_real_mint_and_starts_empty() { + // Arrange / Act + let path = temp_db_path(); + let wallet = CashuWallet::connect(&test_mint_url(), [7u8; 64], path.to_str().unwrap()) + .await + .expect("nutshell must be reachable"); + + // Assert + assert!(wallet.capabilities().is_usable()); + assert_eq!(wallet.balance().await.unwrap(), 0); + + let _ = std::fs::remove_file(&path); + } + + #[tokio::test] + #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] + async fn an_unreachable_mint_fails_with_a_marker() { + // Arrange — a port nothing listens on. + let path = temp_db_path(); + + // Act + let err = CashuWallet::connect("http://127.0.0.1:1", [7u8; 64], path.to_str().unwrap()) + .await + .unwrap_err(); + + // Assert — the marker is what Dart localizes; the cause is for the log. + assert!(err.to_string().contains("CashuMintUnreachable"), "got {err}"); + + let _ = std::fs::remove_file(&path); + } + + #[tokio::test] + #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] + async fn a_token_round_trips_between_two_wallets() { + // Arrange — two wallets at the same mint. The sender must already hold + // at least 8 sat; funding is out of band (nutshell fakewallet), which + // is why this stays behind --ignored. + let sender_path = temp_db_path(); + let receiver_path = temp_db_path(); + let mint = test_mint_url(); + + let sender = CashuWallet::connect(&mint, [1u8; 64], sender_path.to_str().unwrap()) + .await + .unwrap(); + let receiver = CashuWallet::connect(&mint, [2u8; 64], receiver_path.to_str().unwrap()) + .await + .unwrap(); + + let funded = sender.balance().await.unwrap(); + assert!(funded >= 8, "fund the sender wallet first (has {funded} sat)"); + + // Act + let token = sender.create_token(8).await.unwrap(); + let received = receiver.receive_token(&token).await.unwrap(); + + // Assert + assert_eq!(received, 8); + assert_eq!(receiver.balance().await.unwrap(), 8); + assert_eq!(sender.balance().await.unwrap(), funded - 8); + + let _ = std::fs::remove_file(&sender_path); + let _ = std::fs::remove_file(&receiver_path); + } + + #[tokio::test] + #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] + async fn creating_a_zero_token_is_rejected_before_touching_the_mint() { + let path = temp_db_path(); + let wallet = CashuWallet::connect(&test_mint_url(), [3u8; 64], path.to_str().unwrap()) + .await + .unwrap(); + + let err = wallet.create_token(0).await.unwrap_err(); + assert!(err.to_string().contains("CashuAmountZero"), "got {err}"); + + let _ = std::fs::remove_file(&path); + } +} diff --git a/rust/src/crypto/keys.rs b/rust/src/crypto/keys.rs index 8ea10432..db9b86f5 100644 --- a/rust/src/crypto/keys.rs +++ b/rust/src/crypto/keys.rs @@ -44,6 +44,19 @@ pub fn derive_trade_key(mnemonic_words: &[String], index: u32) -> Result { derive_at_index(mnemonic_words, index) } +/// Derive the raw BIP-39 seed from a mnemonic. +/// +/// This is the same 64-byte seed [`derive_master_key`] feeds into BIP-32, with +/// the same empty passphrase. The Cashu wallet (phase C2) uses it directly: +/// `cdk` derives its blinding secrets from a 64-byte seed, so the wallet is +/// recoverable from the very words the user already backed up — one secret to +/// protect, not two. +pub fn derive_bip39_seed(mnemonic_words: &[String]) -> Result<[u8; 64]> { + let phrase = mnemonic_words.join(" "); + let mnemonic = Mnemonic::parse(&phrase).map_err(|e| anyhow!("invalid mnemonic: {e}"))?; + Ok(mnemonic.to_seed("")) +} + // ── Internal ───────────────────────────────────────────────────────────────── fn derive_at_index(mnemonic_words: &[String], index: u32) -> Result { diff --git a/rust/src/db/app_db.rs b/rust/src/db/app_db.rs index 70e18c14..fdb88ee4 100644 --- a/rust/src/db/app_db.rs +++ b/rust/src/db/app_db.rs @@ -15,6 +15,11 @@ use crate::db::indexeddb::IndexedDbStorage as AppStorage; static APP_DB: OnceCell = OnceCell::const_new(); +/// The `path` [`init_db`] was called with, kept so other stores can place their +/// own files next to it without a second call from Dart. Device paths are +/// Dart's job (repo architecture rule); deriving a sibling name from one is not. +static APP_DB_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); + /// Initialise the persistent store with the given `path`. /// /// On native platforms `path` is the absolute path to the SQLite file (e.g. @@ -26,10 +31,21 @@ pub async fn init_db(path: &str) -> Result<()> { APP_DB .get_or_try_init(|| async { AppStorage::open(path).await }) .await?; + // Recorded only after the store opened, so a failed init leaves no path + // behind for a sibling store to build on. + let _ = APP_DB_PATH.set(path.to_string()); log::info!("[db] persistent store ready (path={})", path); Ok(()) } +/// The path [`init_db`] was given, or `None` before it succeeded. +/// +/// Callers that need their own file (e.g. the Cashu proof store) derive a +/// sibling name from it rather than asking Dart for a second path. +pub(crate) fn app_db_path() -> Option<&'static str> { + APP_DB_PATH.get().map(String::as_str) +} + /// Return a reference to the initialised storage, or `None` if [`init_db`] /// has not been called yet. pub(crate) fn db() -> Option<&'static AppStorage> { diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index fbe1b115..65237187 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -26,6 +26,7 @@ // Section: imports use crate::api::bond::*; +use crate::api::cashu::*; use crate::api::disputes::*; use crate::api::logging::*; use crate::api::messages::*; @@ -46,7 +47,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1575145418; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 414549179; // Section: executor @@ -170,6 +171,63 @@ fn wire__crate__api__bond__BondSlashedStream_next_impl( }, ) } +fn wire__crate__api__cashu__CashuWalletStream_next_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CashuWalletStream_next", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref_mut().await) + } + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::api::cashu::CashuWalletStream::next(&mut *api_that_guard) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__nostr__ConnectionStateStream_next_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1305,6 +1363,254 @@ fn wire__crate__api__orders__cancel_order_impl( }, ) } +fn wire__crate__api__cashu__cashu_check_proofs_state_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_check_proofs_state", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_check_proofs_state().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_connect_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_connect", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_connect().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_create_token_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_create_token", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_amount_sats = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::cashu::cashu_create_token(api_amount_sats).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_disconnect_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_disconnect", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_disconnect().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_get_balance_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_get_balance", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_get_balance().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_receive_token_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_receive_token", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_encoded = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_receive_token(api_encoded).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__cashu__cashu_status_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "cashu_status", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::cashu::cashu_status().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__nwc__connect_wallet_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2894,6 +3200,39 @@ fn wire__crate__api__bond__on_bond_slashed_impl( }, ) } +fn wire__crate__api__cashu__on_cashu_wallet_changed_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "on_cashu_wallet_changed", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::cashu::on_cashu_wallet_changed())?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__nostr__on_connection_state_changed_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -4068,6 +4407,9 @@ flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); @@ -4132,6 +4474,16 @@ impl SseDecode for BondSlashedStream { } } +impl SseDecode for CashuWalletStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode for ConnectionStateStream { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4264,6 +4616,16 @@ impl SseDecode } } +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for RustOpaqueMoi< flutter_rust_bridge::for_generated::RustAutoOpaqueInner, @@ -4469,6 +4831,22 @@ impl SseDecode for crate::api::types::BuyerStep { } } +impl SseDecode for crate::api::types::CashuWalletStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_connected = ::sse_decode(deserializer); + let mut var_mintUrl = >::sse_decode(deserializer); + let mut var_balanceSats = ::sse_decode(deserializer); + let mut var_missingCapabilities = >::sse_decode(deserializer); + return crate::api::types::CashuWalletStatus { + connected: var_connected, + mint_url: var_mintUrl, + balance_sats: var_balanceSats, + missing_capabilities: var_missingCapabilities, + }; + } +} + impl SseDecode for crate::api::types::ChatMessage { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5537,238 +5915,256 @@ fn pde_ffi_dispatcher_primary_impl( data_len, ), 2 => wire__crate__api__bond__BondSlashedStream_next_impl(port, ptr, rust_vec_len, data_len), - 3 => wire__crate__api__nostr__ConnectionStateStream_next_impl( + 3 => { + wire__crate__api__cashu__CashuWalletStream_next_impl(port, ptr, rust_vec_len, data_len) + } + 4 => wire__crate__api__nostr__ConnectionStateStream_next_impl( port, ptr, rust_vec_len, data_len, ), - 4 => wire__crate__api__disputes__DisputeStream_next_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__logging__LogEntryStream_next_impl(port, ptr, rust_vec_len, data_len), - 6 => wire__crate__api__messages__MessageStream_next_impl(port, ptr, rust_vec_len, data_len), - 7 => wire__crate__api__orders__OrderBook_clear_impl(port, ptr, rust_vec_len, data_len), - 8 => wire__crate__api__orders__OrderBook_default_impl(port, ptr, rust_vec_len, data_len), - 9 => wire__crate__api__orders__OrderBook_get_order_impl(port, ptr, rust_vec_len, data_len), - 10 => { + 5 => wire__crate__api__disputes__DisputeStream_next_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__logging__LogEntryStream_next_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__messages__MessageStream_next_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__orders__OrderBook_clear_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__orders__OrderBook_default_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__orders__OrderBook_get_order_impl(port, ptr, rust_vec_len, data_len), + 11 => { wire__crate__api__orders__OrderBook_get_orders_impl(port, ptr, rust_vec_len, data_len) } - 11 => wire__crate__api__orders__OrderBook_new_impl(port, ptr, rust_vec_len, data_len), - 12 => { + 12 => wire__crate__api__orders__OrderBook_new_impl(port, ptr, rust_vec_len, data_len), + 13 => { wire__crate__api__orders__OrderBook_remove_order_impl(port, ptr, rust_vec_len, data_len) } - 13 => { + 14 => { wire__crate__api__orders__OrderBook_set_orders_impl(port, ptr, rust_vec_len, data_len) } - 14 => wire__crate__api__orders__OrderBook_update_order_status_impl( + 15 => wire__crate__api__orders__OrderBook_update_order_status_impl( port, ptr, rust_vec_len, data_len, ), - 15 => { + 16 => { wire__crate__api__orders__OrderBook_upsert_order_impl(port, ptr, rust_vec_len, data_len) } - 16 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), - 17 => { + 17 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), + 18 => { wire__crate__api__reputation__RatingStream_next_impl(port, ptr, rust_vec_len, data_len) } - 18 => { + 19 => { wire__crate__api__nostr__RelayStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 19 => { + 20 => { wire__crate__api__settings__SettingsStream_next_impl(port, ptr, rust_vec_len, data_len) } - 20 => wire__crate__api__messages__UnreadCountStream_next_impl( + 21 => wire__crate__api__messages__UnreadCountStream_next_impl( port, ptr, rust_vec_len, data_len, ), - 21 => { + 22 => { wire__crate__api__nwc__WalletStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 22 => wire__crate__api__nostr__add_relay_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__orders__cancel_order_impl(port, ptr, rust_vec_len, data_len), - 24 => wire__crate__api__nwc__connect_wallet_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__identity__create_identity_impl(port, ptr, rust_vec_len, data_len), - 26 => wire__crate__api__orders__create_order_impl(port, ptr, rust_vec_len, data_len), - 27 => wire__crate__api__identity__delete_identity_impl(port, ptr, rust_vec_len, data_len), - 28 => wire__crate__api__identity__derive_trade_key_impl(port, ptr, rust_vec_len, data_len), - 29 => wire__crate__api__nwc__disconnect_wallet_impl(port, ptr, rust_vec_len, data_len), - 30 => { + 23 => wire__crate__api__nostr__add_relay_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__orders__cancel_order_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__cashu__cashu_check_proofs_state_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 26 => wire__crate__api__cashu__cashu_connect_impl(port, ptr, rust_vec_len, data_len), + 27 => wire__crate__api__cashu__cashu_create_token_impl(port, ptr, rust_vec_len, data_len), + 28 => wire__crate__api__cashu__cashu_disconnect_impl(port, ptr, rust_vec_len, data_len), + 29 => wire__crate__api__cashu__cashu_get_balance_impl(port, ptr, rust_vec_len, data_len), + 30 => wire__crate__api__cashu__cashu_receive_token_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__cashu__cashu_status_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__nwc__connect_wallet_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__identity__create_identity_impl(port, ptr, rust_vec_len, data_len), + 34 => wire__crate__api__orders__create_order_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__identity__delete_identity_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__crate__api__identity__derive_trade_key_impl(port, ptr, rust_vec_len, data_len), + 37 => wire__crate__api__nwc__disconnect_wallet_impl(port, ptr, rust_vec_len, data_len), + 38 => { wire__crate__api__messages__download_attachment_impl(port, ptr, rust_vec_len, data_len) } - 31 => wire__crate__api__identity__export_encrypted_backup_impl( + 39 => wire__crate__api__identity__export_encrypted_backup_impl( port, ptr, rust_vec_len, data_len, ), - 32 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( + 40 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( port, ptr, rust_vec_len, data_len, ), - 33 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), - 34 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), - 35 => wire__crate__api__messages__get_attachment_status_impl( + 41 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), + 43 => wire__crate__api__messages__get_attachment_status_impl( port, ptr, rust_vec_len, data_len, ), - 36 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), - 37 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), - 40 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), - 41 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), - 45 => { + 44 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), + 51 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), + 52 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), + 53 => { wire__crate__api__reputation__get_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 46 => wire__crate__api__reputation__get_rating_for_trade_impl( + 54 => wire__crate__api__reputation__get_rating_for_trade_impl( port, ptr, rust_vec_len, data_len, ), - 47 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), - 49 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), - 50 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), - 51 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), - 52 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), - 53 => wire__crate__api__disputes__handle_admin_canceled_impl( + 55 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), + 56 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), + 57 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), + 58 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), + 59 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), + 60 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), + 61 => wire__crate__api__disputes__handle_admin_canceled_impl( port, ptr, rust_vec_len, data_len, ), - 54 => { + 62 => { wire__crate__api__disputes__handle_admin_settled_impl(port, ptr, rust_vec_len, data_len) } - 55 => wire__crate__api__disputes__handle_admin_took_dispute_impl( + 63 => wire__crate__api__disputes__handle_admin_took_dispute_impl( port, ptr, rust_vec_len, data_len, ), - 56 => wire__crate__api__reputation__handle_rating_received_impl( + 64 => wire__crate__api__reputation__handle_rating_received_impl( port, ptr, rust_vec_len, data_len, ), - 57 => { + 65 => { wire__crate__api__identity__import_from_mnemonic_impl(port, ptr, rust_vec_len, data_len) } - 58 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), - 59 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), - 60 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), - 61 => wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len), - 62 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), - 63 => wire__crate__api__identity__load_identity_from_mnemonic_impl( + 66 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), + 67 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), + 68 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), + 69 => wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len), + 70 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), + 71 => wire__crate__api__identity__load_identity_from_mnemonic_impl( port, ptr, rust_vec_len, data_len, ), - 64 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), - 65 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), - 66 => wire__crate__api__messages__on_attachment_progress_impl( + 72 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), + 73 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), + 74 => wire__crate__api__messages__on_attachment_progress_impl( port, ptr, rust_vec_len, data_len, ), - 67 => wire__crate__api__bond__on_bond_slashed_impl(port, ptr, rust_vec_len, data_len), - 68 => wire__crate__api__nostr__on_connection_state_changed_impl( + 75 => wire__crate__api__bond__on_bond_slashed_impl(port, ptr, rust_vec_len, data_len), + 76 => { + wire__crate__api__cashu__on_cashu_wallet_changed_impl(port, ptr, rust_vec_len, data_len) + } + 77 => wire__crate__api__nostr__on_connection_state_changed_impl( port, ptr, rust_vec_len, data_len, ), - 69 => { + 78 => { wire__crate__api__disputes__on_dispute_updated_impl(port, ptr, rust_vec_len, data_len) } - 70 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), - 71 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), - 72 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), - 73 => { + 79 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), + 81 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), + 82 => { wire__crate__api__reputation__on_rating_received_impl(port, ptr, rust_vec_len, data_len) } - 74 => { + 83 => { wire__crate__api__nostr__on_relay_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 75 => { + 84 => { wire__crate__api__settings__on_settings_changed_impl(port, ptr, rust_vec_len, data_len) } - 76 => wire__crate__api__messages__on_unread_count_changed_impl( + 85 => wire__crate__api__messages__on_unread_count_changed_impl( port, ptr, rust_vec_len, data_len, ), - 77 => { + 86 => { wire__crate__api__nwc__on_wallet_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 78 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), - 79 => { + 87 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), + 88 => { wire__crate__api__orders__order_filters_default_impl(port, ptr, rust_vec_len, data_len) } - 80 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), - 81 => wire__crate__api__settings__rehydrate_active_mostro_node_impl( + 89 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), + 90 => wire__crate__api__settings__rehydrate_active_mostro_node_impl( port, ptr, rust_vec_len, data_len, ), - 82 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), - 83 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), - 84 => wire__crate__api__orders__restart_orders_subscription_impl( + 91 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), + 92 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), + 93 => wire__crate__api__orders__restart_orders_subscription_impl( port, ptr, rust_vec_len, data_len, ), - 85 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), - 86 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), - 87 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), - 88 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), - 89 => wire__crate__api__settings__set_active_mostro_node_impl( + 94 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), + 95 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), + 96 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), + 97 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), + 98 => wire__crate__api__settings__set_active_mostro_node_impl( port, ptr, rust_vec_len, data_len, ), - 90 => wire__crate__api__settings__set_default_fiat_code_impl( + 99 => wire__crate__api__settings__set_default_fiat_code_impl( port, ptr, rust_vec_len, data_len, ), - 91 => wire__crate__api__settings__set_default_lightning_address_impl( + 100 => wire__crate__api__settings__set_default_lightning_address_impl( port, ptr, rust_vec_len, data_len, ), - 92 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), - 93 => { + 101 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), + 102 => { wire__crate__api__settings__set_logging_enabled_impl(port, ptr, rust_vec_len, data_len) } - 94 => { + 103 => { wire__crate__api__reputation__set_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 95 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), - 96 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), - 97 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), - 98 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), - 99 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), + 104 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), + 105 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), + 106 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), + 107 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), + 108 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -5822,6 +6218,21 @@ impl flutter_rust_bridge::IntoIntoDart> for BondSl } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for CashuWalletStream { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -6094,6 +6505,29 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::types::CashuWalletStatus { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.connected.into_into_dart().into_dart(), + self.mint_url.into_into_dart().into_dart(), + self.balance_sats.into_into_dart().into_dart(), + self.missing_capabilities.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::types::CashuWalletStatus +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::types::CashuWalletStatus +{ + fn into_into_dart(self) -> crate::api::types::CashuWalletStatus { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::types::ChatMessage { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -6944,6 +7378,13 @@ impl SseEncode for BondSlashedStream { } } +impl SseEncode for CashuWalletStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for ConnectionStateStream { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7050,6 +7491,17 @@ impl SseEncode } } +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for RustOpaqueMoi< flutter_rust_bridge::for_generated::RustAutoOpaqueInner, @@ -7244,6 +7696,16 @@ impl SseEncode for crate::api::types::BuyerStep { } } +impl SseEncode for crate::api::types::CashuWalletStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.connected, serializer); + >::sse_encode(self.mint_url, serializer); + ::sse_encode(self.balance_sats, serializer); + >::sse_encode(self.missing_capabilities, serializer); + } +} + impl SseEncode for crate::api::types::ChatMessage { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -8191,6 +8653,7 @@ mod io { use super::*; use crate::api::bond::*; + use crate::api::cashu::*; use crate::api::disputes::*; use crate::api::logging::*; use crate::api::messages::*; @@ -8237,6 +8700,20 @@ mod io { MoiArc::>::decrement_strong_count(ptr as _); } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_mostro_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCashuWalletStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_mostro_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCashuWalletStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_mostro_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerConnectionStateStream( ptr: *const std::ffi::c_void, @@ -8404,6 +8881,7 @@ mod web { use super::*; use crate::api::bond::*; + use crate::api::cashu::*; use crate::api::disputes::*; use crate::api::logging::*; use crate::api::messages::*; @@ -8452,6 +8930,20 @@ mod web { MoiArc::>::decrement_strong_count(ptr as _); } + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCashuWalletStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCashuWalletStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[wasm_bindgen] pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerConnectionStateStream( ptr: *const std::ffi::c_void, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f054b4e9..b10a36bc 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -5,6 +5,7 @@ mod frb_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be // Zero crypto in Dart — this is the only source of trust. pub mod api; +pub mod cashu; pub mod config; pub mod crypto; pub mod db; From 47269fec2825505595fd345fe5be7d3c26bb0f0a Mon Sep 17 00:00:00 2001 From: grunch Date: Sat, 25 Jul 2026 09:03:07 -0300 Subject: [PATCH 2/4] =?UTF-8?q?fix(cashu):=20C2=20review=20round=201=20?= =?UTF-8?q?=E2=80=94=20unknown=20balance,=20reserved=20proofs,=20cashu:=20?= =?UTF-8?q?URIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the strict review on #235. Major - `CashuWalletStatus.balance_sats` is now `Option`. It used to report a failed read as `0`, which for bearer money is the worst possible lie: a user opening the wallet during a transient store error was shown "0 Satoshis" and no indication anything had failed. `None` and `Some(0)` are different facts and only one of them is alarming. - A failed `confirm` left the prepared send's proofs reserved with no owner — `confirm` consumes the handle, so nothing could cancel them — and the balance silently dropped until the user happened to run a proof-state check. The failure path now reclaims and reports how much came back. - `receive_token` rejected `cashu:` and `cashu://` URIs, which is what QR payloads normally carry. The user got "may be from another mint, or already spent" for a token that was neither. Minor - `proof_store_path` joined onto `Path::parent()`, which is `Some("")` for the IndexedDB *database name* the web build passes to `init_db` — silently producing a relative file next to the process cwd. The two cases are now separated explicitly. - The BIP-39 seed is wrapped in `Zeroizing` from derivation through to the cdk boundary, so the copy made to seed the wallet is wiped on drop. - Concurrent `cashu_connect` calls opened the proof store twice and made two mint round trips, discarding one wallet. Serialised behind a connect mutex. - The wasm stub's `capabilities()` panicked via `unreachable!()` while its neighbours returned empty values. It now answers, so "this cannot happen" has one meaning in that file. Tests: `a_scanned_token_uri_is_accepted` over the five shapes a scanner produces, and `normalisation_leaves_a_token_body_alone` so the strip cannot eat a payload that merely contains the scheme. --- rust/Cargo.lock | 15 ++++++++ rust/Cargo.toml | 4 ++ rust/src/api/cashu.rs | 56 ++++++++++++++++++++-------- rust/src/api/identity.rs | 2 +- rust/src/api/types.rs | 9 ++++- rust/src/cashu/mod.rs | 17 ++++++++- rust/src/cashu/wallet.rs | 78 +++++++++++++++++++++++++++++++++------ rust/src/crypto/keys.rs | 8 +++- rust/src/frb_generated.rs | 4 +- 9 files changed, 157 insertions(+), 36 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e65057d1..ef3c83f7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3145,6 +3145,7 @@ dependencies = [ "uuid", "wasm-bindgen-futures", "wasmtimer", + "zeroize", ] [[package]] @@ -5009,6 +5010,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6eae2c38..433153bf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -45,6 +45,10 @@ rand = { version = "0.8", features = ["getrandom"] } # Hashing (nym derivation) sha2 = "0.10" +# Wipes key material on drop. The BIP-39 seed is copied out of the identity +# state to seed the Cashu wallet; without this it lingers in freed memory. +zeroize = { version = "1", features = ["zeroize_derive"] } + # secp256k1 scalar math (ECDH shared-key derivation for P2P chat) k256 = { version = "0.13", features = ["ecdh", "arithmetic"] } diff --git a/rust/src/api/cashu.rs b/rust/src/api/cashu.rs index c152688f..4e6b1e86 100644 --- a/rust/src/api/cashu.rs +++ b/rust/src/api/cashu.rs @@ -40,9 +40,18 @@ fn proof_store_path() -> Result { let app_db = crate::db::app_db::app_db_path() .ok_or_else(|| anyhow::anyhow!("CashuStoreUnavailable: database not initialised"))?; - let path = std::path::Path::new(app_db); - let dir = path.parent().unwrap_or_else(|| std::path::Path::new(".")); - Ok(dir.join("cashu.sqlite").to_string_lossy().into_owned()) + // `init_db`'s argument is a filesystem path on native and an IndexedDB + // *database name* on web. A name has no parent, and joining onto `""` would + // silently produce a relative file next to the process's cwd — so the two + // cases are separated rather than left to `Path` semantics. + let parent = std::path::Path::new(app_db) + .parent() + .filter(|p| !p.as_os_str().is_empty()); + + Ok(match parent { + Some(dir) => dir.join("cashu.sqlite").to_string_lossy().into_owned(), + None => "cashu.sqlite".to_string(), + }) } /// Fail closed unless the active node was positively identified as Cashu. @@ -59,13 +68,17 @@ async fn snapshot() -> CashuWalletStatus { Some(wallet) => CashuWalletStatus { connected: true, mint_url: Some(wallet.mint_url().to_string()), - // A failed balance read reports zero rather than tearing down the - // status: the wallet is still connected, and the next event will - // carry the real figure. - balance_sats: wallet.balance().await.unwrap_or_else(|e| { - log::warn!("[cashu] balance read failed: {e}"); - 0 - }), + // A failed read reports `None`, never zero. The wallet is still + // connected and the next event will carry the real figure, but in + // the meantime the UI must say "unknown" rather than name a number + // that would read as "your money is gone". + balance_sats: match wallet.balance().await { + Ok(balance) => Some(balance), + Err(e) => { + log::warn!("[cashu] balance read failed: {e}"); + None + } + }, missing_capabilities: wallet .capabilities() .missing() @@ -76,7 +89,9 @@ async fn snapshot() -> CashuWalletStatus { None => CashuWalletStatus { connected: false, mint_url: None, - balance_sats: 0, + // Not connected is a known state, and a wallet with no binding + // genuinely holds nothing spendable here. + balance_sats: Some(0), missing_capabilities: Vec::new(), }, } @@ -100,6 +115,15 @@ async fn notify() { pub async fn cashu_connect() -> Result { ensure_enabled()?; + // One connect at a time. Without this, two callers both miss the check + // below, both open the proof store on the same file and both make the mint + // round trip, and one of the wallets is then thrown away. + static CONNECTING: OnceLock> = OnceLock::new(); + let _connecting = CONNECTING + .get_or_init(|| tokio::sync::Mutex::new(())) + .lock() + .await; + { let guard = wallet_lock().read().await; if guard.is_some() { @@ -124,8 +148,9 @@ pub async fn cashu_connect() -> Result { { let mut guard = wallet_lock().write().await; - // Another task may have connected while this one awaited the mint; - // keep the live wallet rather than replacing it. + // The connect mutex above makes this the only writer, but a + // `cashu_disconnect` could have run in between; treat a live wallet as + // authoritative rather than replacing it. if guard.is_none() { *guard = Some(wallet); } @@ -295,9 +320,10 @@ mod tests { // anything, and "not connected" is truthful everywhere. let status = cashu_status().await.unwrap(); - // Assert + // Assert — a disconnected wallet holds nothing, and that is a *known* + // zero rather than an unreadable balance. assert!(!status.connected); - assert_eq!(status.balance_sats, 0); + assert_eq!(status.balance_sats, Some(0)); assert_eq!(status.mint_url, None); } diff --git a/rust/src/api/identity.rs b/rust/src/api/identity.rs index 0b455c0a..a7f6d4ef 100644 --- a/rust/src/api/identity.rs +++ b/rust/src/api/identity.rs @@ -206,7 +206,7 @@ pub async fn get_identity() -> Result> { /// skips it because it is not `pub`. The Cashu wallet (phase C2) needs a /// 64-byte seed to derive its blinding secrets, and reusing this one is what /// makes the ecash recoverable from the words the user already backed up. -pub(crate) async fn current_bip39_seed() -> Option<[u8; 64]> { +pub(crate) async fn current_bip39_seed() -> Option> { let guard = identity_lock().read().await; let state = guard.as_ref()?; match key_ops::derive_bip39_seed(&state.mnemonic_words) { diff --git a/rust/src/api/types.rs b/rust/src/api/types.rs index cb68028a..3bda3658 100644 --- a/rust/src/api/types.rs +++ b/rust/src/api/types.rs @@ -514,8 +514,13 @@ pub struct CashuWalletStatus { pub connected: bool, /// The mint the wallet is bound to, when connected. pub mint_url: Option, - /// Spendable balance in satoshis. Zero when not connected. - pub balance_sats: u64, + /// Spendable balance in satoshis, or `None` when it could not be read. + /// + /// `None` and `Some(0)` are different facts and only one of them is + /// alarming: ecash is bearer money, and showing a user "0 sat" because a + /// store read failed invites exactly the wrong reaction. The UI must render + /// the unknown case as unknown. + pub balance_sats: Option, /// Stable markers for anything the mint failed to advertise (`"nut07"`, /// `"nut11"`, `"nut12"`, `"sat_keyset"`). Empty on a healthy connection — /// a mint missing any of them is refused at connect, so a non-empty list diff --git a/rust/src/cashu/mod.rs b/rust/src/cashu/mod.rs index 328c5c50..39684fed 100644 --- a/rust/src/cashu/mod.rs +++ b/rust/src/cashu/mod.rs @@ -54,7 +54,11 @@ pub struct CashuWallet; #[cfg(target_arch = "wasm32")] impl CashuWallet { - pub async fn connect(_mint_url: &str, _seed: [u8; 64], _db_path: &str) -> anyhow::Result { + pub async fn connect( + _mint_url: &str, + _seed: zeroize::Zeroizing<[u8; 64]>, + _db_path: &str, + ) -> anyhow::Result { anyhow::bail!("CashuUnsupportedOnWeb") } @@ -62,8 +66,17 @@ impl CashuWallet { "" } + /// Never called — `connect` cannot succeed here — but it answers instead of + /// panicking, so "this cannot happen" has one meaning in this file rather + /// than two. pub fn capabilities(&self) -> &MintCapabilities { - unreachable!("no wasm wallet can be constructed") + const NONE: &MintCapabilities = &MintCapabilities { + nut07_state_check: false, + nut11_p2pk: false, + nut12_dleq: false, + has_sat_keyset: false, + }; + NONE } pub async fn balance(&self) -> anyhow::Result { diff --git a/rust/src/cashu/wallet.rs b/rust/src/cashu/wallet.rs index 2df8b063..638b85e5 100644 --- a/rust/src/cashu/wallet.rs +++ b/rust/src/cashu/wallet.rs @@ -88,7 +88,11 @@ impl CashuWallet { /// **Errors** (stable markers): `CashuMintUnreachable` when the mint does /// not answer, `CashuMintUnusable` when it answers but lacks a required NUT /// or the `sat` keyset. - pub async fn connect(mint_url: &str, seed: [u8; 64], db_path: &str) -> Result { + pub async fn connect( + mint_url: &str, + seed: zeroize::Zeroizing<[u8; 64]>, + db_path: &str, + ) -> Result { let localstore = WalletSqliteDatabase::new(db_path) .await .map_err(|e| anyhow!("CashuStoreUnavailable: {e}"))?; @@ -97,7 +101,8 @@ impl CashuWallet { mint_url, CurrencyUnit::Sat, Arc::new(localstore), - seed, + // cdk takes the seed by value; our copy is wiped on drop. + *seed, // cdk's own default: keep roughly this many proofs per denomination // so an ordinary send rarely needs an extra swap round trip. None, @@ -168,7 +173,7 @@ impl CashuWallet { pub async fn receive_token(&self, encoded: &str) -> Result { let amount = self .inner - .receive(encoded.trim(), ReceiveOptions::default()) + .receive(&normalize_token(encoded), ReceiveOptions::default()) .await .map_err(|e| anyhow!("CashuReceiveFailed: {e}"))?; log::info!("[cashu] received {amount} sat"); @@ -197,10 +202,17 @@ impl CashuWallet { .await .map_err(|e| anyhow!("CashuSendFailed: {e}"))?; - let token = prepared - .confirm(None::) - .await - .map_err(|e| anyhow!("CashuSendFailed: {e}"))?; + let token = match prepared.confirm(None::).await { + Ok(token) => token, + Err(e) => { + // `prepare_send` reserved the proofs and `confirm` consumed the + // handle, so they are now reserved with no owner. Left alone, + // the balance silently drops by the send amount until the user + // happens to run a proof-state check. Reclaim here instead. + let reclaimed = self.check_proofs_state().await.unwrap_or(0); + bail!("CashuSendFailed: {e} (reclaimed {reclaimed} sat)"); + } + }; Ok(token.to_string()) } @@ -224,6 +236,21 @@ impl CashuWallet { } } +/// Strip whitespace and the `cashu:` URI scheme. +/// +/// QR payloads routinely carry `cashu:cashuB…`, and a wallet that rejects them +/// tells the user their token is from another mint or already spent — three +/// wrong explanations for one missing prefix strip. +pub(crate) fn normalize_token(encoded: &str) -> String { + let trimmed = encoded.trim(); + trimmed + .strip_prefix("cashu://") + .or_else(|| trimmed.strip_prefix("cashu:")) + .unwrap_or(trimmed) + .trim() + .to_string() +} + // ── Tests ──────────────────────────────────────────────────────────────────── #[cfg(test)] @@ -246,6 +273,10 @@ mod tests { .expect("set MOSTRO_TEST_MINT_URL to run the Cashu integration tests") } + fn seed(byte: u8) -> zeroize::Zeroizing<[u8; 64]> { + zeroize::Zeroizing::new([byte; 64]) + } + fn temp_db_path() -> std::path::PathBuf { use std::sync::atomic::{AtomicU32, Ordering}; static COUNTER: AtomicU32 = AtomicU32::new(0); @@ -304,6 +335,29 @@ mod tests { } } + #[test] + fn a_scanned_token_uri_is_accepted() { + // Arrange — the shapes a QR scanner actually hands over. Rejecting any + // of these tells the user their token is from another mint or already + // spent, which is three wrong explanations for a missing prefix strip. + for input in [ + "cashuBtoken", + " cashuBtoken\n", + "cashu:cashuBtoken", + "cashu://cashuBtoken", + " cashu: cashuBtoken ", + ] { + assert_eq!(normalize_token(input), "cashuBtoken", "input: {input:?}"); + } + } + + #[test] + fn normalisation_leaves_a_token_body_alone() { + // Assert — only the scheme is stripped, never a prefix that happens to + // look like one inside the payload. + assert_eq!(normalize_token("cashuBcashu:inner"), "cashuBcashu:inner"); + } + #[test] fn a_fresh_capability_set_is_unusable() { // Assert — the default must fail closed, so a probe that never ran @@ -318,7 +372,7 @@ mod tests { async fn connects_to_a_real_mint_and_starts_empty() { // Arrange / Act let path = temp_db_path(); - let wallet = CashuWallet::connect(&test_mint_url(), [7u8; 64], path.to_str().unwrap()) + let wallet = CashuWallet::connect(&test_mint_url(), seed(7), path.to_str().unwrap()) .await .expect("nutshell must be reachable"); @@ -336,7 +390,7 @@ mod tests { let path = temp_db_path(); // Act - let err = CashuWallet::connect("http://127.0.0.1:1", [7u8; 64], path.to_str().unwrap()) + let err = CashuWallet::connect("http://127.0.0.1:1", seed(7), path.to_str().unwrap()) .await .unwrap_err(); @@ -356,10 +410,10 @@ mod tests { let receiver_path = temp_db_path(); let mint = test_mint_url(); - let sender = CashuWallet::connect(&mint, [1u8; 64], sender_path.to_str().unwrap()) + let sender = CashuWallet::connect(&mint, seed(1), sender_path.to_str().unwrap()) .await .unwrap(); - let receiver = CashuWallet::connect(&mint, [2u8; 64], receiver_path.to_str().unwrap()) + let receiver = CashuWallet::connect(&mint, seed(2), receiver_path.to_str().unwrap()) .await .unwrap(); @@ -383,7 +437,7 @@ mod tests { #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] async fn creating_a_zero_token_is_rejected_before_touching_the_mint() { let path = temp_db_path(); - let wallet = CashuWallet::connect(&test_mint_url(), [3u8; 64], path.to_str().unwrap()) + let wallet = CashuWallet::connect(&test_mint_url(), seed(3), path.to_str().unwrap()) .await .unwrap(); diff --git a/rust/src/crypto/keys.rs b/rust/src/crypto/keys.rs index db9b86f5..fe0d106f 100644 --- a/rust/src/crypto/keys.rs +++ b/rust/src/crypto/keys.rs @@ -13,6 +13,7 @@ use anyhow::{anyhow, Result}; use bip32::{DerivationPath, XPrv}; use bip39::Mnemonic; use nostr_sdk::prelude::{Keys, SecretKey}; +use zeroize::Zeroizing; const DERIVATION_PREFIX: &str = "m/44'/1237'/38383'/0"; @@ -51,10 +52,13 @@ pub fn derive_trade_key(mnemonic_words: &[String], index: u32) -> Result { /// `cdk` derives its blinding secrets from a 64-byte seed, so the wallet is /// recoverable from the very words the user already backed up — one secret to /// protect, not two. -pub fn derive_bip39_seed(mnemonic_words: &[String]) -> Result<[u8; 64]> { +/// Returned in a [`Zeroizing`] wrapper so the copy is wiped when the caller +/// drops it — the seed is the whole account, and it is about to cross into a +/// third-party crate. +pub fn derive_bip39_seed(mnemonic_words: &[String]) -> Result> { let phrase = mnemonic_words.join(" "); let mnemonic = Mnemonic::parse(&phrase).map_err(|e| anyhow!("invalid mnemonic: {e}"))?; - Ok(mnemonic.to_seed("")) + Ok(Zeroizing::new(mnemonic.to_seed(""))) } // ── Internal ───────────────────────────────────────────────────────────────── diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 65237187..f9712dfa 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -4836,7 +4836,7 @@ impl SseDecode for crate::api::types::CashuWalletStatus { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_connected = ::sse_decode(deserializer); let mut var_mintUrl = >::sse_decode(deserializer); - let mut var_balanceSats = ::sse_decode(deserializer); + let mut var_balanceSats = >::sse_decode(deserializer); let mut var_missingCapabilities = >::sse_decode(deserializer); return crate::api::types::CashuWalletStatus { connected: var_connected, @@ -7701,7 +7701,7 @@ impl SseEncode for crate::api::types::CashuWalletStatus { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.connected, serializer); >::sse_encode(self.mint_url, serializer); - ::sse_encode(self.balance_sats, serializer); + >::sse_encode(self.balance_sats, serializer); >::sse_encode(self.missing_capabilities, serializer); } } From 1d39d71e459b9d0897339d5a8a948057f789bd13 Mon Sep 17 00:00:00 2001 From: grunch Date: Sat, 25 Jul 2026 12:11:06 -0300 Subject: [PATCH 3/4] test(cashu): make the C2 integration tests runnable against a mint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the `#[ignore]`d suite against a real nutshell for the first time showed it could not pass at all, and then that it could not pass twice. - `a_token_round_trips_between_two_wallets` asserted "fund the sender wallet first" and returned. Nothing a reviewer could do satisfied that, so the test was documentation rather than verification. `mint_for_test` mints from the mint itself, which settles instantly against a FakeWallet backend. - A fixed seed plus a fresh DB replays the same NUT-13 blinding secrets, and the mint answers "Blinded Message is already signed" on the second run. Seeds are now unique per process and per call. - The round trip pinned the received amount at 8. nutshell's default keyset charges a swap fee, so an 8 sat token redeems for 7. The face value is what the sender parts with and what a validator checks; the assertion bounds the redeemed amount instead of pinning it. Verified 4/4 against nutshell 0.20.3, twice in a row. The mint rate limits by default — running back to back needs MINT_RATE_LIMIT=FALSE. --- rust/src/cashu/wallet.rs | 85 +++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/rust/src/cashu/wallet.rs b/rust/src/cashu/wallet.rs index 638b85e5..1569ce5e 100644 --- a/rust/src/cashu/wallet.rs +++ b/rust/src/cashu/wallet.rs @@ -217,6 +217,37 @@ impl CashuWallet { Ok(token.to_string()) } + /// Mint `amount_sats` straight from the mint, for tests only. + /// + /// Against a `FakeWallet` backend (which is what a local nutshell runs) the + /// quote settles itself, so this funds a wallet with no Lightning node and + /// no manual step. Without it the integration tests below assert + /// "fund the wallet first" and can never pass, which makes them + /// documentation rather than verification. + #[cfg(test)] + pub(crate) async fn mint_for_test(&self, amount_sats: u64) -> Result { + use cdk::nuts::PaymentMethod; + + let quote = self + .inner + .mint_quote( + PaymentMethod::BOLT11, + Some(Amount::from(amount_sats)), + None, + None, + ) + .await + .map_err(|e| anyhow!("CashuMintQuoteFailed: {e}"))?; + + let proofs = self + .inner + .mint("e.id, SplitTarget::default(), None) + .await + .map_err(|e| anyhow!("CashuMintFailed: {e} (is the mint in FakeWallet mode?)"))?; + + Ok(proofs.iter().map(|p| u64::from(p.amount)).sum()) + } + /// Reconcile pending proofs with the mint (NUT-07), returning the amount /// reclaimed as spendable. /// @@ -273,8 +304,30 @@ mod tests { .expect("set MOSTRO_TEST_MINT_URL to run the Cashu integration tests") } - fn seed(byte: u8) -> zeroize::Zeroizing<[u8; 64]> { - zeroize::Zeroizing::new([byte; 64]) + /// A seed unique to this process *and* this call. + /// + /// cdk derives blinding secrets deterministically from the seed and a + /// counter kept in the wallet DB (NUT-13). These tests create a fresh DB + /// each time, so a fixed seed replays the same blinded messages and the + /// mint answers "already signed" on the second run. Unique seeds make the + /// suite re-runnable, which is the whole point of a reviewer being able to + /// run it. + fn unique_seed() -> zeroize::Zeroizing<[u8; 64]> { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let mut seed = [0u8; 64]; + let pid = std::process::id() as u64; + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + seed[..8].copy_from_slice(&pid.to_le_bytes()); + seed[8..16].copy_from_slice(&n.to_le_bytes()); + seed[16..24].copy_from_slice( + &std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) + .to_le_bytes(), + ); + zeroize::Zeroizing::new(seed) } fn temp_db_path() -> std::path::PathBuf { @@ -372,7 +425,7 @@ mod tests { async fn connects_to_a_real_mint_and_starts_empty() { // Arrange / Act let path = temp_db_path(); - let wallet = CashuWallet::connect(&test_mint_url(), seed(7), path.to_str().unwrap()) + let wallet = CashuWallet::connect(&test_mint_url(), unique_seed(), path.to_str().unwrap()) .await .expect("nutshell must be reachable"); @@ -390,7 +443,7 @@ mod tests { let path = temp_db_path(); // Act - let err = CashuWallet::connect("http://127.0.0.1:1", seed(7), path.to_str().unwrap()) + let err = CashuWallet::connect("http://127.0.0.1:1", unique_seed(), path.to_str().unwrap()) .await .unwrap_err(); @@ -403,30 +456,34 @@ mod tests { #[tokio::test] #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] async fn a_token_round_trips_between_two_wallets() { - // Arrange — two wallets at the same mint. The sender must already hold - // at least 8 sat; funding is out of band (nutshell fakewallet), which - // is why this stays behind --ignored. + // Arrange — two wallets at the same mint, funded from the mint itself. let sender_path = temp_db_path(); let receiver_path = temp_db_path(); let mint = test_mint_url(); - let sender = CashuWallet::connect(&mint, seed(1), sender_path.to_str().unwrap()) + let sender = CashuWallet::connect(&mint, unique_seed(), sender_path.to_str().unwrap()) .await .unwrap(); - let receiver = CashuWallet::connect(&mint, seed(2), receiver_path.to_str().unwrap()) + let receiver = CashuWallet::connect(&mint, unique_seed(), receiver_path.to_str().unwrap()) .await .unwrap(); + sender.mint_for_test(64).await.expect("mint must fund the wallet"); let funded = sender.balance().await.unwrap(); - assert!(funded >= 8, "fund the sender wallet first (has {funded} sat)"); + assert!(funded >= 8, "minting should have funded the wallet"); // Act let token = sender.create_token(8).await.unwrap(); let received = receiver.receive_token(&token).await.unwrap(); - // Assert - assert_eq!(received, 8); - assert_eq!(receiver.balance().await.unwrap(), 8); + // Assert — the sender parted with the token's face value; the receiver + // gets that minus whatever the mint charges to swap (nutshell's default + // keyset has a non-zero input fee, and a real mint may too). + assert!( + received > 0 && received <= 8, + "received {received} sat for an 8 sat token" + ); + assert_eq!(receiver.balance().await.unwrap(), received); assert_eq!(sender.balance().await.unwrap(), funded - 8); let _ = std::fs::remove_file(&sender_path); @@ -437,7 +494,7 @@ mod tests { #[ignore = "requires a local nutshell mint (MOSTRO_TEST_MINT_URL)"] async fn creating_a_zero_token_is_rejected_before_touching_the_mint() { let path = temp_db_path(); - let wallet = CashuWallet::connect(&test_mint_url(), seed(3), path.to_str().unwrap()) + let wallet = CashuWallet::connect(&test_mint_url(), unique_seed(), path.to_str().unwrap()) .await .unwrap(); From 529a5a72788a2eecee2235d19cc417abd820cc9c Mon Sep 17 00:00:00 2001 From: grunch Date: Sat, 25 Jul 2026 18:14:39 -0300 Subject: [PATCH 4/4] =?UTF-8?q?fix(cashu):=20CodeRabbit=20round=20on=20#23?= =?UTF-8?q?5=20=E2=80=94=20connect=20could=20revive=20a=20dropped=20wallet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings, all valid. Major - A `cashu_disconnect` running while `CashuWallet::connect` awaited the mint cleared the slot, and the connect then filled it back in because it saw `None` — rebinding the wallet the caller had just dropped, and (on a node switch, which is where disconnect is called from) leaving it pointing at the *previous* node's mint. Connect and disconnect now share one lifecycle lock, and connect re-checks that the active node still resolves to the mint it connected to before installing. The old comment even said "treat a live wallet as authoritative", which is exactly backwards for this case. Minor - `CashuStoreUnavailable: database not initialised` and `CashuWalletStream closed: channel sender dropped` carried English prose past the bridge. Rust returns markers and Dart localizes them (repo rule), so both are now bare markers; the second was not even a marker Dart could match, so it always fell through to the generic message. - A failed `check_proofs_state` during send recovery was reported as "reclaimed 0 sat", making an unknown outcome look like a confirmed zero — the same trap this PR fixed for `balance_sats`. A reclaim that fails now says so, and points at the proof-state check. Test: `a_wallet_is_only_installed_while_its_mint_is_still_the_active_one` pins the install guard, including that a trailing slash is a formatting difference rather than a different mint. The race itself is not reproducible in a unit test, so the decision it turns on is extracted and tested directly. --- rust/src/api/cashu.rs | 77 +++++++++++++++++++++++++++++++++------- rust/src/cashu/wallet.rs | 16 +++++++-- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/rust/src/api/cashu.rs b/rust/src/api/cashu.rs index 4e6b1e86..d5a23595 100644 --- a/rust/src/api/cashu.rs +++ b/rust/src/api/cashu.rs @@ -38,7 +38,7 @@ fn changes() -> &'static broadcast::Sender { /// put two migration systems on one file. fn proof_store_path() -> Result { let app_db = crate::db::app_db::app_db_path() - .ok_or_else(|| anyhow::anyhow!("CashuStoreUnavailable: database not initialised"))?; + .ok_or_else(|| anyhow::anyhow!("CashuStoreUnavailable"))?; // `init_db`'s argument is a filesystem path on native and an IndexedDB // *database name* on web. A name has no parent, and joining onto `""` would @@ -54,6 +54,27 @@ fn proof_store_path() -> Result { }) } +/// Serializes wallet lifecycle changes: connect and disconnect. +/// +/// Without it two connects both open the proof store and both hit the mint, and +/// — worse — a disconnect issued during a connect clears an empty slot which the +/// connect then fills, rebinding a wallet the caller just dropped. +fn lifecycle_lock() -> &'static tokio::sync::Mutex<()> { + static LIFECYCLE: OnceLock> = OnceLock::new(); + LIFECYCLE.get_or_init(|| tokio::sync::Mutex::new(())) +} + +/// May a wallet built for `connected_to` still be installed? +/// +/// Only if the active node still resolves to that same mint. A node switch +/// during the connect makes the wallet stale before it is ever stored, and the +/// funds it would manage belong to a different node's mint. +fn should_install(connected_to: &str, resolved_now: Option<&str>) -> bool { + resolved_now + .map(|current| current.trim_end_matches('/') == connected_to.trim_end_matches('/')) + .unwrap_or(false) +} + /// Fail closed unless the active node was positively identified as Cashu. fn ensure_enabled() -> Result<()> { if !escrow_mode::is_cashu_mode() { @@ -115,14 +136,8 @@ async fn notify() { pub async fn cashu_connect() -> Result { ensure_enabled()?; - // One connect at a time. Without this, two callers both miss the check - // below, both open the proof store on the same file and both make the mint - // round trip, and one of the wallets is then thrown away. - static CONNECTING: OnceLock> = OnceLock::new(); - let _connecting = CONNECTING - .get_or_init(|| tokio::sync::Mutex::new(())) - .lock() - .await; + // One lifecycle change at a time — see [`lifecycle_lock`]. + let _lifecycle = lifecycle_lock().lock().await; { let guard = wallet_lock().read().await; @@ -146,11 +161,22 @@ pub async fn cashu_connect() -> Result { let db_path = proof_store_path()?; let wallet = CashuWallet::connect(&mint_url, seed, &db_path).await?; + // Re-check before installing. Holding the lifecycle lock keeps a + // `cashu_disconnect` from interleaving, but the *escrow mode* is not under + // that lock: a node switch during the mint round trip changes which mint we + // should be bound to, and installing anyway would leave the wallet pointing + // at the previous node's mint. + let resolved_now = escrow_mode::get_resolved(); + if !should_install(&mint_url, resolved_now.config.mint_url.as_deref()) { + log::warn!( + "[cashu] discarding a wallet for {mint_url}: the active node now resolves to {:?}", + resolved_now.config.mint_url + ); + bail!("CashuNotEnabled"); + } + { let mut guard = wallet_lock().write().await; - // The connect mutex above makes this the only writer, but a - // `cashu_disconnect` could have run in between; treat a live wallet as - // authoritative rather than replacing it. if guard.is_none() { *guard = Some(wallet); } @@ -234,6 +260,10 @@ pub async fn cashu_check_proofs_state() -> Result { /// wipe. Called when the active node changes, so a wallet bound to one node's /// mint never serves another's. pub async fn cashu_disconnect() -> Result<()> { + // Shares the lifecycle lock with `cashu_connect`, so a disconnect issued + // during a connect waits for it and then clears the slot, instead of + // clearing an empty slot and having the connect fill it back in. + let _lifecycle = lifecycle_lock().lock().await; { let mut guard = wallet_lock().write().await; *guard = None; @@ -260,7 +290,7 @@ impl CashuWalletStream { match self.rx.recv().await { Ok(status) => return Ok(status), Err(RecvError::Lagged(_)) => continue, - Err(RecvError::Closed) => bail!("CashuWalletStream closed"), + Err(RecvError::Closed) => bail!("CashuWalletStreamClosed"), } } } @@ -327,6 +357,27 @@ mod tests { assert_eq!(status.mint_url, None); } + #[test] + fn a_wallet_is_only_installed_while_its_mint_is_still_the_active_one() { + // The scenario: a connect is awaiting the mint when the user switches + // node. The lifecycle lock keeps `cashu_disconnect` from interleaving, + // but the escrow mode is not under that lock — so the connect can + // finish holding a wallet bound to the *previous* node's mint. Storing + // it would silently point the app's funds at the wrong mint. + let mint = "https://mint.example.com"; + + // Still the active mint — install. + assert!(should_install(mint, Some(mint))); + // Trailing slashes are a formatting difference, not a different mint. + assert!(should_install(mint, Some("https://mint.example.com/"))); + assert!(should_install("https://mint.example.com/", Some(mint))); + + // The node switched to a different Cashu node — discard. + assert!(!should_install(mint, Some("https://other.example.com"))); + // The node switched to Lightning, or the mode was cleared — discard. + assert!(!should_install(mint, None)); + } + #[tokio::test] async fn disconnect_is_idempotent_and_notifies() { // Arrange diff --git a/rust/src/cashu/wallet.rs b/rust/src/cashu/wallet.rs index 1569ce5e..0cd86673 100644 --- a/rust/src/cashu/wallet.rs +++ b/rust/src/cashu/wallet.rs @@ -209,8 +209,20 @@ impl CashuWallet { // handle, so they are now reserved with no owner. Left alone, // the balance silently drops by the send amount until the user // happens to run a proof-state check. Reclaim here instead. - let reclaimed = self.check_proofs_state().await.unwrap_or(0); - bail!("CashuSendFailed: {e} (reclaimed {reclaimed} sat)"); + // + // A failed reclaim is reported as such rather than as zero: + // "reclaimed 0" would tell the user their funds are accounted + // for when in fact nobody knows, which is the same + // unknown-versus-known-zero trap this module fixed for the + // balance. + match self.check_proofs_state().await { + Ok(reclaimed) => { + bail!("CashuSendFailed: {e} (reclaimed {reclaimed} sat)") + } + Err(reclaim_err) => bail!( + "CashuSendFailed: {e} (reclaim also failed: {reclaim_err} — run the proof-state check when the mint is reachable)" + ), + } } };