add nip60 crate

This commit is contained in:
2026-11-06 07:10:37 +07:00
parent 73962d351f
commit c5dc0ca242
7 changed files with 1116 additions and 37 deletions
Generated
+988 -8
View File
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -34,7 +34,7 @@ nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-browser-signer = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
nostr-sqlite = { git = "https://github.com/rust-nostr/nostr" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip98" ] }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip60", "nip98" ] }
# Crypto (NIP-17 encrypted file messages)
aes-gcm = "0.10"
@@ -48,6 +48,14 @@ hmac = "0.12"
# Pinned to the instance `nostr-sdk` already builds, so NIP-44 nonces share it
rand = { version = "0.10", default-features = false, features = [ "std", "sys_rng", "thread_rng" ] }
# Cashu (NIP-60). From git: the published 0.18.x line targets nostr 0.44 and
# cannot unify with the git rust-nostr used here, while main targets 0.45.x
# (redirected to this repo by the [patch.crates-io] section below). redb
# storage: pure Rust, avoids the libsqlite3-sys links clash with nostr-sqlite.
cdk = { git = "https://github.com/cashubtc/cdk", default-features = false, features = [ "wallet" ] }
cdk-redb = { git = "https://github.com/cashubtc/cdk", default-features = false, features = [ "wallet" ] }
cdk-nostr = { git = "https://github.com/cashubtc/cdk", default-features = false }
# Others
anyhow = "1.0.44"
tts = { git = "https://github.com/ndarilek/tts-rs" }
@@ -66,6 +74,12 @@ webbrowser = "1.0.4"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
instant = "0.1"
# cdk's registry nostr/nostr-sdk requirements are redirected onto the same
# git repo coop already uses, keeping a single copy of each in the graph.
[patch.crates-io]
nostr = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
[profile.release]
strip = true
opt-level = "z"
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "nip60"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
cdk.workspace = true
cdk-redb.workspace = true
cdk-nostr.workspace = true
+49
View File
@@ -0,0 +1,49 @@
#![cfg(not(target_arch = "wasm32"))]
use gpui::App;
use nostr::prelude::*;
/// Initialize the wallet registry.
pub fn init(_cx: &mut App) {}
/// Compile-only phase 0 spike: proves the `nostr::nips::nip60` types, kinds,
/// and the cdk crates resolve against the workspace's git nostr. Replaced by
/// the real implementation.
#[allow(dead_code)]
fn spike() {
let _kinds = (
Kind::CashuWallet,
Kind::CashuWalletUnspentProof,
Kind::CashuWalletSpendingHistory,
Kind::CashuWalletQuote,
);
let _history =
nostr::nips::nip60::SpendingHistory::new(nostr::nips::nip60::TransactionDirection::In, 1);
let _builder = cdk::wallet::WalletBuilder::new();
let _store: Option<cdk_redb::WalletRedbDatabase> = None;
let _inbox: Option<cdk_nostr::inbox::NostrInbox> = None;
if let Ok(mint) = nostr::types::Url::parse("https://mint.example.com") {
let proof = nostr::nips::nip60::CashuProof {
id: String::new(),
amount: 1,
secret: String::new(),
c: String::new(),
};
let _wallet_event = nostr::nips::nip60::WalletEvent::new("privkey", vec![mint.clone()]);
let _token_event =
nostr::nips::nip60::TokenEvent::new(mint, vec![proof]).destroyed("id".to_string());
}
}
/// Only compiles when `nostr` and `cdk_nostr` resolve to the same crate.
#[allow(dead_code)]
fn spike_unified(key: nostr::key::SecretKey) -> cdk_nostr::nostr::key::SecretKey {
key
}
#[allow(dead_code)]
fn spike_sdk_unified(client: nostr_sdk::client::Client) -> cdk_nostr::nostr_sdk::client::Client {
client
}
+1
View File
@@ -37,6 +37,7 @@ workspace = { path = "../crates/workspace" }
ui = { path = "../crates/ui" }
state = { path = "../crates/state" }
nip4e = { path = "../crates/nip4e" }
nip60 = { path = "../crates/nip60" }
chat = { path = "../crates/chat" }
chat_ui = { path = "../crates/chat_ui" }
nip29 = { path = "../crates/nip29" }
+5
View File
@@ -91,6 +91,11 @@ fn main() {
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
nip4e::init(cx);
// Initialize cashu wallet registry
//
// NIP-60: https://github.com/nostr-protocol/nips/blob/master/60.md
nip60::init(cx);
// Initialize app registry
chat::init(cx);
+44 -28
View File
@@ -1,7 +1,7 @@
# NIP-60: cashu wallet — implementation plan
Spec: https://github.com/nostr-protocol/nips/blob/master/60.md
CDK: https://github.com/cashubtc/cdk (crates `cdk`, `cdk-sqlite`, `cdk-nostr` 0.18.x)
CDK: https://github.com/cashubtc/cdk (crates `cdk`, `cdk-redb`, `cdk-nostr`, from git main)
NIP-60 stores a user's cashu wallet state on relays: the wallet config
(kind `17375`), its unspent proofs (kind `7375`), and an optional spending
@@ -50,7 +50,8 @@ Follows the existing crate patterns exactly; no new architecture.
(runtime is already initialized by `state::init`). The `cdk-nostr`
inbox listener also runs on that runtime.
- Signer-agnostic encryption: the signer may be a bunker/extension, so we
never use `nostr::nip60::*::prepare` (they need an owned `SecretKey`).
never use `nostr::nips::nip60` prepare helpers (they need an owned
`SecretKey`).
We build payloads as plain JSON, encrypt with
`signer.nip44_encrypt(self_pubkey, json)` and sign with
`EventBuilder::new(kind, content).finalize_async(signer)` — the same
@@ -72,31 +73,41 @@ Follows the existing crate patterns exactly; no new architecture.
## Dependencies
Workspace `Cargo.toml`:
Workspace `Cargo.toml` (final state after the phase 0 spike):
```toml
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip60", "nip98" ] }
# Cashu (NIP-60). From git: the published 0.18.x line targets nostr 0.44 and
# cannot unify with the git rust-nostr used here, while main targets 0.45.x
cdk = { git = "https://github.com/cashubtc/cdk", default-features = false, features = [ "wallet" ] }
cdk-redb = { git = "https://github.com/cashubtc/cdk", default-features = false, features = [ "wallet" ] }
cdk-nostr = { git = "https://github.com/cashubtc/cdk", default-features = false }
[patch.crates-io]
# cdk publishes against crates.io nostr 0.45.x; coop uses the git source.
# The patch unifies both on one copy of the crate so types cross crate
# boundaries (cdk-nostr inbox events, keys) without conversion.
nostr = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
```
`crates/nip60/Cargo.toml` (all under `cfg(not(wasm32))`):
Spike findings (all resolved and verified with `cargo check`):
```toml
cdk = { version = "0.18", default-features = false, features = ["wallet"] }
cdk-sqlite = { version = "0.18", default-features = false, features = ["wallet"] }
cdk-nostr = { version = "0.18", default-features = false } # inbox, for NIP-61
```
Version-unification is the one risky step; Phase 0 is a spike that proves
`cdk 0.18.x` + git rust-nostr build together. Fallback if the patch fights
back: convert only at the cdk-nostr inbox boundary (hex/serde) and skip the
patch for `nostr-sdk`.
- The cdk family hard-pins `cdk-common` with `=` per release, so all crates
must come from one release train. crates.io 0.18.3 pins nostr 0.44 and
cannot coexist with coop's git nostr 0.45.x; the git main line pins
nostr ^0.45.3, which the `[patch.crates-io]` block redirects onto the
same git rust-nostr checkout. The lockfile ends with exactly one `nostr`
and one `nostr-sdk`, proven by compile-time type-identity checks between
`nostr` and `cdk_nostr::nostr`.
- Storage is `cdk-redb` (`WalletRedbDatabase::new(&Path)`, pure Rust), not
`cdk-sqlite`: published cdk-sqlite pins rusqlite ^0.31, whose
`libsqlite3-sys` clashes at the `links` level with the `nostr-sqlite`
wasm dependency of the `state` crate.
- rust-nostr 0.45.x has no root re-exports: the module is
`nostr::nips::nip60`, and cdk-nostr re-exports the SDK as
`cdk_nostr::nostr` / `cdk_nostr::nostr_sdk`.
- The `nip60` crate exists (`crates/nip60`) with a compile-only spike in
`lib.rs` and a no-op `init(cx)` wired into `desktop/src/main.rs`; both
are replaced in phase 1.
## Design
@@ -116,7 +127,7 @@ patch for `nostr-sdk`.
### Local state
- cdk sqlite store at `config_dir().join("cashu")` (one `WalletDatabase`
- cdk redb store at `config_dir().join("cashu")` (one `WalletDatabase`
shared by all mint wallets).
- `WalletRegistry` holds: parsed `17375` (`WalletEvent` + event id), the
active `7375` events per mint (event id + `TokenEvent`), cdk `Wallet`
@@ -173,13 +184,14 @@ the UI crate when Phase 3 lands).
## Phases
### Phase 0 — dependency spike (small, throwaway)
### Phase 0 — dependency spike (done)
1. Enable `nip60` on the `nostr` workspace dep; confirm
`nostr::nip60` types and the four kinds compile.
2. Add the `[patch.crates-io]` block and a scratch dependency on
`cdk`/`cdk-sqlite`/`cdk-nostr`; `cargo check` desktop. Resolve the
nostr unification here before writing any code.
Outcome recorded in the Dependencies section: cdk family on git main,
`[patch.crates-io]` unification, cdk-redb storage, `nip60` crate skeleton
wired into desktop. `cargo check -p nip60` and the full desktop check pass.
The remaining spike code in `crates/nip60/src/lib.rs` is deleted in
phase 1.
### Phase 1 — bootstrap + load + balance
@@ -228,9 +240,13 @@ the UI crate when Phase 3 lands).
- Concurrent spends from two devices double-spend; recovery is the spec's
Appendix-1 validation (check proofs at the mint, roll over survivors) —
implemented as reconciliation on sync, not real-time locking.
- Web target gets no wallet in v1: `cdk-sqlite` (rusqlite bundled) does
not build on wasm32; the crate compiles to nothing there (auto_update
pattern). A wasm store would need a cdk `WalletDatabase` over
IndexedDB — explicitly out of scope.
- Tracking cdk via git main is a moving target; the lockfile pins a rev
and must be bumped deliberately (same policy as the rust-nostr git dep).
Watch for the next cdk release line built against nostr 0.45.x — moving
back to crates.io then removes the moving-target concern.
- Web target gets no wallet in v1: cdk store backends (redb, sqlite) need
a real filesystem and do not build on wasm32; the crate compiles to
nothing there (auto_update pattern). A wasm store would need a cdk
`WalletDatabase` over IndexedDB — explicitly out of scope.
- `7374` quote events are skipped (local cdk state per the spec); pending
quotes are not visible to other devices until claimed.