add nip60 crate
This commit is contained in:
Generated
+988
-8
File diff suppressed because it is too large
Load Diff
+15
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user