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-browser-signer = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-sdk = { 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-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)
|
# Crypto (NIP-17 encrypted file messages)
|
||||||
aes-gcm = "0.10"
|
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
|
# 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" ] }
|
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
|
# Others
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
tts = { git = "https://github.com/ndarilek/tts-rs" }
|
tts = { git = "https://github.com/ndarilek/tts-rs" }
|
||||||
@@ -66,6 +74,12 @@ webbrowser = "1.0.4"
|
|||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
instant = "0.1"
|
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]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
opt-level = "z"
|
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" }
|
ui = { path = "../crates/ui" }
|
||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
nip4e = { path = "../crates/nip4e" }
|
nip4e = { path = "../crates/nip4e" }
|
||||||
|
nip60 = { path = "../crates/nip60" }
|
||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
chat_ui = { path = "../crates/chat_ui" }
|
chat_ui = { path = "../crates/chat_ui" }
|
||||||
nip29 = { path = "../crates/nip29" }
|
nip29 = { path = "../crates/nip29" }
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ fn main() {
|
|||||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
nip4e::init(cx);
|
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
|
// Initialize app registry
|
||||||
chat::init(cx);
|
chat::init(cx);
|
||||||
|
|
||||||
|
|||||||
+44
-28
@@ -1,7 +1,7 @@
|
|||||||
# NIP-60: cashu wallet — implementation plan
|
# NIP-60: cashu wallet — implementation plan
|
||||||
|
|
||||||
Spec: https://github.com/nostr-protocol/nips/blob/master/60.md
|
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
|
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
|
(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`
|
(runtime is already initialized by `state::init`). The `cdk-nostr`
|
||||||
inbox listener also runs on that runtime.
|
inbox listener also runs on that runtime.
|
||||||
- Signer-agnostic encryption: the signer may be a bunker/extension, so we
|
- 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
|
We build payloads as plain JSON, encrypt with
|
||||||
`signer.nip44_encrypt(self_pubkey, json)` and sign with
|
`signer.nip44_encrypt(self_pubkey, json)` and sign with
|
||||||
`EventBuilder::new(kind, content).finalize_async(signer)` — the same
|
`EventBuilder::new(kind, content).finalize_async(signer)` — the same
|
||||||
@@ -72,31 +73,41 @@ Follows the existing crate patterns exactly; no new architecture.
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Workspace `Cargo.toml`:
|
Workspace `Cargo.toml` (final state after the phase 0 spike):
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip60", "nip98" ] }
|
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]
|
[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 = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-sdk = { 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
|
- The cdk family hard-pins `cdk-common` with `=` per release, so all crates
|
||||||
cdk = { version = "0.18", default-features = false, features = ["wallet"] }
|
must come from one release train. crates.io 0.18.3 pins nostr 0.44 and
|
||||||
cdk-sqlite = { version = "0.18", default-features = false, features = ["wallet"] }
|
cannot coexist with coop's git nostr 0.45.x; the git main line pins
|
||||||
cdk-nostr = { version = "0.18", default-features = false } # inbox, for NIP-61
|
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
|
||||||
Version-unification is the one risky step; Phase 0 is a spike that proves
|
`nostr` and `cdk_nostr::nostr`.
|
||||||
`cdk 0.18.x` + git rust-nostr build together. Fallback if the patch fights
|
- Storage is `cdk-redb` (`WalletRedbDatabase::new(&Path)`, pure Rust), not
|
||||||
back: convert only at the cdk-nostr inbox boundary (hex/serde) and skip the
|
`cdk-sqlite`: published cdk-sqlite pins rusqlite ^0.31, whose
|
||||||
patch for `nostr-sdk`.
|
`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
|
## Design
|
||||||
|
|
||||||
@@ -116,7 +127,7 @@ patch for `nostr-sdk`.
|
|||||||
|
|
||||||
### Local state
|
### 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).
|
shared by all mint wallets).
|
||||||
- `WalletRegistry` holds: parsed `17375` (`WalletEvent` + event id), the
|
- `WalletRegistry` holds: parsed `17375` (`WalletEvent` + event id), the
|
||||||
active `7375` events per mint (event id + `TokenEvent`), cdk `Wallet`
|
active `7375` events per mint (event id + `TokenEvent`), cdk `Wallet`
|
||||||
@@ -173,13 +184,14 @@ the UI crate when Phase 3 lands).
|
|||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
### Phase 0 — dependency spike (small, throwaway)
|
### Phase 0 — dependency spike (done)
|
||||||
|
|
||||||
1. Enable `nip60` on the `nostr` workspace dep; confirm
|
Outcome recorded in the Dependencies section: cdk family on git main,
|
||||||
`nostr::nip60` types and the four kinds compile.
|
`[patch.crates-io]` unification, cdk-redb storage, `nip60` crate skeleton
|
||||||
2. Add the `[patch.crates-io]` block and a scratch dependency on
|
wired into desktop. `cargo check -p nip60` and the full desktop check pass.
|
||||||
`cdk`/`cdk-sqlite`/`cdk-nostr`; `cargo check` desktop. Resolve the
|
|
||||||
nostr unification here before writing any code.
|
The remaining spike code in `crates/nip60/src/lib.rs` is deleted in
|
||||||
|
phase 1.
|
||||||
|
|
||||||
### Phase 1 — bootstrap + load + balance
|
### 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
|
- Concurrent spends from two devices double-spend; recovery is the spec's
|
||||||
Appendix-1 validation (check proofs at the mint, roll over survivors) —
|
Appendix-1 validation (check proofs at the mint, roll over survivors) —
|
||||||
implemented as reconciliation on sync, not real-time locking.
|
implemented as reconciliation on sync, not real-time locking.
|
||||||
- Web target gets no wallet in v1: `cdk-sqlite` (rusqlite bundled) does
|
- Tracking cdk via git main is a moving target; the lockfile pins a rev
|
||||||
not build on wasm32; the crate compiles to nothing there (auto_update
|
and must be bumped deliberately (same policy as the rust-nostr git dep).
|
||||||
pattern). A wasm store would need a cdk `WalletDatabase` over
|
Watch for the next cdk release line built against nostr 0.45.x — moving
|
||||||
IndexedDB — explicitly out of scope.
|
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
|
- `7374` quote events are skipped (local cdk state per the spec); pending
|
||||||
quotes are not visible to other devices until claimed.
|
quotes are not visible to other devices until claimed.
|
||||||
|
|||||||
Reference in New Issue
Block a user