diff --git a/Cargo.toml b/Cargo.toml index e4553072..13d438eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ cdk = { path = "./crates/cdk", default-features = false } cdk-rexie = { path = "./crates/cdk-rexie", default-features = false } tokio = { version = "1.32", default-features = false } thiserror = "1" -tracing = { version = "0.1", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["attributes"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1" serde-wasm-bindgen = { version = "0.6.5", default-features = false } diff --git a/bindings/cdk-js/src/lib.rs b/bindings/cdk-js/src/lib.rs index 4f39ec59..beb70f8c 100644 --- a/bindings/cdk-js/src/lib.rs +++ b/bindings/cdk-js/src/lib.rs @@ -3,6 +3,7 @@ use wasm_bindgen::prelude::*; pub mod error; pub mod nuts; pub mod types; +#[cfg(all(feature = "wallet", target_arch = "wasm32"))] pub mod wallet; #[wasm_bindgen(start)] diff --git a/bindings/cdk-js/src/wallet.rs b/bindings/cdk-js/src/wallet.rs index 711b582a..47d54951 100644 --- a/bindings/cdk-js/src/wallet.rs +++ b/bindings/cdk-js/src/wallet.rs @@ -10,6 +10,8 @@ use cdk_rexie::RexieWalletDatabase; use wasm_bindgen::prelude::*; use crate::error::{into_err, Result}; +use crate::nuts::nut04::JsMintQuoteBolt11Response; +use crate::nuts::nut05::JsMeltQuoteBolt11Response; use crate::nuts::nut11::JsP2PKSpendingConditions; use crate::nuts::nut14::JsHTLCSpendingConditions; use crate::nuts::{JsCurrencyUnit, JsMintInfo, JsProof}; @@ -37,11 +39,11 @@ impl From for JsWallet { #[wasm_bindgen(js_class = Wallet)] impl JsWallet { #[wasm_bindgen(constructor)] - pub async fn new() -> Self { + pub async fn new(seed: Vec) -> Self { let client = HttpClient::new(); let db = RexieWalletDatabase::new().await.unwrap(); - Wallet::new(client, Arc::new(db), None).await.into() + Wallet::new(client, Arc::new(db), &seed).await.into() } #[wasm_bindgen(js_name = totalBalance)] @@ -95,6 +97,23 @@ impl JsWallet { Ok(quote.into()) } + #[wasm_bindgen(js_name = mintQuoteStatus)] + pub async fn mint_quote_status( + &self, + mint_url: String, + quote_id: String, + ) -> Result { + let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; + + let quote = self + .inner + .mint_quote_status(mint_url, "e_id) + .await + .map_err(into_err)?; + + Ok(quote.into()) + } + #[wasm_bindgen(js_name = mint)] pub async fn mint(&mut self, mint_url: String, quote_id: String) -> Result { let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; @@ -124,6 +143,23 @@ impl JsWallet { Ok(melt_quote.into()) } + #[wasm_bindgen(js_name = meltQuoteStatus)] + pub async fn melt_quote_status( + &self, + mint_url: String, + quote_id: String, + ) -> Result { + let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; + + let quote = self + .inner + .melt_quote_status(mint_url, "e_id) + .await + .map_err(into_err)?; + + Ok(quote.into()) + } + #[wasm_bindgen(js_name = melt)] pub async fn melt(&mut self, mint_url: String, quote_id: String) -> Result { let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; diff --git a/crates/cdk-redb/src/wallet.rs b/crates/cdk-redb/src/wallet.rs index 07d1f421..321f8e45 100644 --- a/crates/cdk-redb/src/wallet.rs +++ b/crates/cdk-redb/src/wallet.rs @@ -10,6 +10,7 @@ use cdk::types::{MeltQuote, MintQuote}; use cdk::url::UncheckedUrl; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use tokio::sync::Mutex; +use tracing::instrument; use super::error::Error; @@ -23,7 +24,7 @@ const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinitio const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("pending_proofs"); const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); -const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter"); +const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter"); const DATABASE_VERSION: u32 = 0; @@ -79,6 +80,7 @@ impl RedbWalletDatabase { impl WalletDatabase for RedbWalletDatabase { type Err = cdk_database::Error; + #[instrument(skip(self))] async fn add_mint( &self, mint_url: UncheckedUrl, @@ -104,6 +106,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] async fn get_mint(&self, mint_url: UncheckedUrl) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Into::::into)?; @@ -119,6 +122,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(None) } + #[instrument(skip(self))] async fn get_mints(&self) -> Result>, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; @@ -138,6 +142,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(mints) } + #[instrument(skip(self))] async fn add_mint_keysets( &self, mint_url: UncheckedUrl, @@ -168,6 +173,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] async fn get_mint_keysets( &self, mint_url: UncheckedUrl, @@ -188,6 +194,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(keysets) } + #[instrument(skip_all)] async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -209,6 +216,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip_all)] async fn get_mint_quote(&self, quote_id: &str) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Into::::into)?; @@ -223,6 +231,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(None) } + #[instrument(skip_all)] async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -239,6 +248,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip_all)] async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -260,6 +270,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip_all)] async fn get_melt_quote(&self, quote_id: &str) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; @@ -274,6 +285,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(None) } + #[instrument(skip_all)] async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -290,6 +302,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip_all)] async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -309,6 +322,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] async fn get_keys(&self, id: &Id) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; @@ -321,6 +335,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(None) } + #[instrument(skip(self))] async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> { let db = self.db.lock().await; let write_txn = db.begin_write().map_err(Error::from)?; @@ -336,6 +351,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self, proofs))] async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> { let db = self.db.lock().await; @@ -360,6 +376,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; @@ -377,6 +394,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(proofs) } + #[instrument(skip(self, proofs))] async fn remove_proofs( &self, mint_url: UncheckedUrl, @@ -405,6 +423,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self, proofs))] async fn add_pending_proofs( &self, mint_url: UncheckedUrl, @@ -433,6 +452,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] async fn get_pending_proofs( &self, mint_url: UncheckedUrl, @@ -453,6 +473,7 @@ impl WalletDatabase for RedbWalletDatabase { Ok(proofs) } + #[instrument(skip(self, proofs))] async fn remove_pending_proofs( &self, mint_url: UncheckedUrl, @@ -481,7 +502,8 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> { + #[instrument(skip(self))] + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { let db = self.db.lock().await; let current_counter; @@ -512,7 +534,8 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { + #[instrument(skip(self))] + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { let db = self.db.lock().await; let read_txn = db.begin_read().map_err(Error::from)?; let table = read_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?; diff --git a/crates/cdk-rexie/src/wallet.rs b/crates/cdk-rexie/src/wallet.rs index dd76996c..edb10a9b 100644 --- a/crates/cdk-rexie/src/wallet.rs +++ b/crates/cdk-rexie/src/wallet.rs @@ -592,7 +592,7 @@ impl WalletDatabase for RexieWalletDatabase { Ok(()) } - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> { + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { let rexie = self.db.lock().await; let transaction = rexie @@ -604,7 +604,7 @@ impl WalletDatabase for RexieWalletDatabase { let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?; let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?; - let current_count: Option = + let current_count: Option = serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?; let new_count = current_count.unwrap_or_default() + count; @@ -621,7 +621,7 @@ impl WalletDatabase for RexieWalletDatabase { Ok(()) } - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { let rexie = self.db.lock().await; let transaction = rexie @@ -633,7 +633,7 @@ impl WalletDatabase for RexieWalletDatabase { let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?; let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?; - let current_count: Option = + let current_count: Option = serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?; Ok(current_count) diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index c3d068ab..fd31c5e9 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -21,25 +21,39 @@ nut13 = ["dep:bip39"] async-trait = "0.1" base64 = "0.22" # bitcoin uses v0.13 (optional dep) bip39 = { version = "2.0", optional = true } -bitcoin = { version = "0.30", features = ["serde", "rand", "rand-std"] } # lightning-invoice uses v0.30 +bitcoin = { version = "0.30", features = [ + "serde", + "rand", + "rand-std", +] } # lightning-invoice uses v0.30 http = "1.0" lightning-invoice = { version = "0.30", features = ["serde"] } once_cell = "1.19" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "socks"], optional = true } -serde = { version = "1.0", default-features = false, features = ["derive"]} +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", + "socks", +], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = "1.0" serde_with = "3.4" -tracing = { version = "0.1", default-features = false } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } thiserror = "1.0" url = "2.3" uuid = { version = "1.6", features = ["v4"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] } +tokio = { workspace = true, features = [ + "rt-multi-thread", + "time", + "macros", + "sync", +] } [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] } getrandom = { version = "0.2", features = ["js"] } -instant = { version = "0.1", features = [ "wasm-bindgen", "inaccurate" ] } - - +instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] } diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index 21e7fb33..ba4bc533 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -88,8 +88,8 @@ pub trait WalletDatabase { proofs: &Proofs, ) -> Result<(), Self::Err>; - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err>; - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err>; + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>; + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err>; } #[cfg(feature = "mint")] diff --git a/crates/cdk/src/cdk_database/wallet_memory.rs b/crates/cdk/src/cdk_database/wallet_memory.rs index 298c79b5..fcbed567 100644 --- a/crates/cdk/src/cdk_database/wallet_memory.rs +++ b/crates/cdk/src/cdk_database/wallet_memory.rs @@ -21,7 +21,7 @@ pub struct WalletMemoryDatabase { mint_keys: Arc>>, proofs: Arc>>>, pending_proofs: Arc>>>, - keyset_counter: Arc>>, + keyset_counter: Arc>>, } impl WalletMemoryDatabase { @@ -29,7 +29,7 @@ impl WalletMemoryDatabase { mint_quotes: Vec, melt_quotes: Vec, mint_keys: Vec, - keyset_counter: HashMap, + keyset_counter: HashMap, ) -> Self { Self { mints: Arc::new(Mutex::new(HashMap::new())), @@ -215,7 +215,7 @@ impl WalletDatabase for WalletMemoryDatabase { Ok(()) } - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> { + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Error> { let keyset_counter = self.keyset_counter.lock().await; let current_counter = keyset_counter.get(keyset_id).unwrap_or(&0); self.keyset_counter @@ -225,7 +225,7 @@ impl WalletDatabase for WalletMemoryDatabase { Ok(()) } - async fn get_keyset_counter(&self, id: &Id) -> Result, Error> { + async fn get_keyset_counter(&self, id: &Id) -> Result, Error> { Ok(self.keyset_counter.lock().await.get(id).cloned()) } } diff --git a/crates/cdk/src/client.rs b/crates/cdk/src/client.rs index d793aa31..cd309f3f 100644 --- a/crates/cdk/src/client.rs +++ b/crates/cdk/src/client.rs @@ -3,6 +3,7 @@ use reqwest::Client; use serde_json::Value; use thiserror::Error; +use tracing::instrument; use url::Url; use crate::error::ErrorResponse; @@ -74,6 +75,7 @@ impl HttpClient { } /// Get Active Mint Keys [NUT-01] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_keys(&self, mint_url: Url) -> Result, Error> { let url = join_url(mint_url, &["v1", "keys"])?; let keys = self.inner.get(url).send().await?.json::().await?; @@ -83,6 +85,7 @@ impl HttpClient { } /// Get Keyset Keys [NUT-01] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_keyset(&self, mint_url: Url, keyset_id: Id) -> Result { let url = join_url(mint_url, &["v1", "keys", &keyset_id.to_string()])?; let keys = self @@ -99,6 +102,7 @@ impl HttpClient { } /// Get Keysets [NUT-02] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_keysets(&self, mint_url: Url) -> Result { let url = join_url(mint_url, &["v1", "keysets"])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -113,6 +117,7 @@ impl HttpClient { } /// Mint Quote [NUT-04] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_mint_quote( &self, mint_url: Url, @@ -136,7 +141,30 @@ impl HttpClient { } } + /// Mint Quote status + #[instrument(skip(self), fields(mint_url = %mint_url))] + pub async fn get_mint_quote_status( + &self, + mint_url: Url, + quote_id: &str, + ) -> Result { + let url = join_url(mint_url, &["v1", "mint", "quote", "bolt11", quote_id])?; + + let res = self.inner.get(url).send().await?; + + let status = res.status(); + + let response: Result = + serde_json::from_value(res.json().await?); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(ErrorResponse::from_json(&status.to_string())?.into()), + } + } + /// Mint Tokens [NUT-04] + #[instrument(skip(self, quote, premint_secrets), fields(mint_url = %mint_url))] pub async fn post_mint( &self, mint_url: Url, @@ -169,6 +197,7 @@ impl HttpClient { } /// Melt Quote [NUT-05] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_melt_quote( &self, mint_url: Url, @@ -192,8 +221,31 @@ impl HttpClient { } } + /// Melt Quote Status + #[instrument(skip(self), fields(mint_url = %mint_url))] + pub async fn get_melt_quote_status( + &self, + mint_url: Url, + quote_id: &str, + ) -> Result { + let url = join_url(mint_url, &["v1", "melt", "quote", "bolt11", quote_id])?; + + let res = self.inner.get(url).send().await?; + + let status = res.status(); + + let response: Result = + serde_json::from_value(res.json().await?); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(ErrorResponse::from_json(&status.to_string())?.into()), + } + } + /// Melt [NUT-05] /// [Nut-08] Lightning fee return if outputs defined + #[instrument(skip(self, quote, inputs, outputs), fields(mint_url = %mint_url))] pub async fn post_melt( &self, mint_url: Url, @@ -222,6 +274,7 @@ impl HttpClient { } /// Split Token [NUT-06] + #[instrument(skip(self, swap_request), fields(mint_url = %mint_url))] pub async fn post_swap( &self, mint_url: Url, @@ -241,6 +294,7 @@ impl HttpClient { } /// Get Mint Info [NUT-06] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_info(&self, mint_url: Url) -> Result { let url = join_url(mint_url, &["v1", "info"])?; @@ -255,6 +309,7 @@ impl HttpClient { } /// Spendable check [NUT-07] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_check_state( &self, mint_url: Url, @@ -281,6 +336,7 @@ impl HttpClient { } } + #[instrument(skip(self, request), fields(mint_url = %mint_url))] pub async fn post_restore( &self, mint_url: Url, diff --git a/crates/cdk/src/nuts/nut13.rs b/crates/cdk/src/nuts/nut13.rs index 89f27991..efb4f7a0 100644 --- a/crates/cdk/src/nuts/nut13.rs +++ b/crates/cdk/src/nuts/nut13.rs @@ -2,11 +2,7 @@ //! //! -use core::str::FromStr; - -use bip39::Mnemonic; -use bitcoin::bip32::{DerivationPath, ExtendedPrivKey}; -use bitcoin::Network; +use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; use super::nut00::{BlindedMessage, PreMint, PreMintSecrets}; use super::nut01::SecretKey; @@ -18,21 +14,12 @@ use crate::util::hex; use crate::{Amount, SECP256K1}; impl Secret { - pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result { - tracing::debug!( - "Deriving secret for {} with count {}", - keyset_id.to_string(), - counter.to_string() - ); - let path: DerivationPath = DerivationPath::from_str(&format!( - "m/129372'/0'/{}'/{}'/0", - u64::try_from(keyset_id)?, - counter - ))?; - - let seed: [u8; 64] = mnemonic.to_seed(""); - let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?; - let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?; + pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result { + tracing::debug!("Deriving secret for {} with count {}", keyset_id, counter); + let path = derive_path_from_keyset_id(keyset_id)? + .child(ChildNumber::from_hardened_idx(counter)?) + .child(ChildNumber::from_normal_idx(0)?); + let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?; Ok(Self::new(hex::encode( derived_xpriv.private_key.secret_bytes(), @@ -41,21 +28,12 @@ impl Secret { } impl SecretKey { - pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result { - tracing::debug!( - "Deriving key for {} with count {}", - keyset_id.to_string(), - counter.to_string() - ); - let path = DerivationPath::from_str(&format!( - "m/129372'/0'/{}'/{}'/1", - u64::try_from(keyset_id)?, - counter - ))?; - - let seed: [u8; 64] = mnemonic.to_seed(""); - let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?; - let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?; + pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result { + tracing::debug!("Deriving key for {} with count {}", keyset_id, counter); + let path = derive_path_from_keyset_id(keyset_id)? + .child(ChildNumber::from_hardened_idx(counter)?) + .child(ChildNumber::from_normal_idx(1)?); + let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?; Ok(Self::from(derived_xpriv.private_key)) } @@ -64,10 +42,10 @@ impl SecretKey { impl PreMintSecrets { /// Generate blinded messages from predetermined secrets and blindings /// factor - pub fn from_seed( + pub fn from_xpriv( keyset_id: Id, - counter: u64, - mnemonic: &Mnemonic, + counter: u32, + xpriv: ExtendedPrivKey, amount: Amount, zero_amount: bool, ) -> Result { @@ -76,8 +54,8 @@ impl PreMintSecrets { let mut counter = counter; for amount in amount.split() { - let secret = Secret::from_seed(mnemonic, keyset_id, counter)?; - let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, counter)?; + let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?; + let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?; let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?; @@ -103,15 +81,15 @@ impl PreMintSecrets { /// factor pub fn restore_batch( keyset_id: Id, - mnemonic: &Mnemonic, - start_count: u64, - end_count: u64, + xpriv: ExtendedPrivKey, + start_count: u32, + end_count: u32, ) -> Result { let mut pre_mint_secrets = PreMintSecrets::default(); for i in start_count..=end_count { - let secret = Secret::from_seed(mnemonic, keyset_id, i)?; - let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, i)?; + let secret = Secret::from_xpriv(xpriv, keyset_id, i)?; + let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, i)?; let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?; @@ -131,8 +109,23 @@ impl PreMintSecrets { } } +fn derive_path_from_keyset_id(id: Id) -> Result { + let index = (u64::try_from(id)? % (2u64.pow(31) - 1)) as u32; + let keyset_child_number = ChildNumber::from_hardened_idx(index)?; + Ok(DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(129372)?, + ChildNumber::from_hardened_idx(0)?, + keyset_child_number, + ])) +} + #[cfg(test)] mod tests { + use std::str::FromStr; + + use bip39::Mnemonic; + use bitcoin::Network; + use super::*; #[test] @@ -140,6 +133,8 @@ mod tests { let seed = "half depart obvious quality work element tank gorilla view sugar picture humble"; let mnemonic = Mnemonic::from_str(seed).unwrap(); + let seed: [u8; 64] = mnemonic.to_seed(""); + let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap(); let keyset_id = Id::from_str("009a1f293253e41e").unwrap(); let test_secrets = [ @@ -151,7 +146,7 @@ mod tests { ]; for (i, test_secret) in test_secrets.iter().enumerate() { - let secret = Secret::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap(); + let secret = Secret::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap(); assert_eq!(secret, Secret::from_str(test_secret).unwrap()) } } @@ -160,6 +155,8 @@ mod tests { let seed = "half depart obvious quality work element tank gorilla view sugar picture humble"; let mnemonic = Mnemonic::from_str(seed).unwrap(); + let seed: [u8; 64] = mnemonic.to_seed(""); + let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap(); let keyset_id = Id::from_str("009a1f293253e41e").unwrap(); let test_rs = [ @@ -171,7 +168,7 @@ mod tests { ]; for (i, test_r) in test_rs.iter().enumerate() { - let r = SecretKey::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap(); + let r = SecretKey::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap(); assert_eq!(r, SecretKey::from_hex(test_r).unwrap()) } } diff --git a/crates/cdk/src/wallet.rs b/crates/cdk/src/wallet.rs index ab7c6e98..83393bca 100644 --- a/crates/cdk/src/wallet.rs +++ b/crates/cdk/src/wallet.rs @@ -5,19 +5,21 @@ use std::num::ParseIntError; use std::str::FromStr; use std::sync::Arc; -use bip39::Mnemonic; +use bitcoin::bip32::ExtendedPrivKey; use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; +use bitcoin::Network; use thiserror::Error; +use tracing::instrument; -use crate::cdk_database::wallet_memory::WalletMemoryDatabase; use crate::cdk_database::{self, WalletDatabase}; use crate::client::HttpClient; use crate::dhke::{construct_proofs, hash_to_curve}; use crate::nuts::{ - nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, MintInfo, - PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SigFlag, - SigningKey, SpendingConditions, State, SwapRequest, Token, VerifyingKey, + nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, + MeltQuoteBolt11Response, MintInfo, MintQuoteBolt11Response, PreMintSecrets, PreSwap, Proof, + ProofState, Proofs, PublicKey, RestoreRequest, SigFlag, SigningKey, SpendingConditions, State, + SwapRequest, Token, VerifyingKey, }; use crate::types::{MeltQuote, Melted, MintQuote}; use crate::url::UncheckedUrl; @@ -47,9 +49,6 @@ pub enum Error { PreimageNotProvided, #[error("Unknown Key")] UnknownKey, - /// Mnemonic Required - #[error("Mnemonic Required")] - MnemonicRequired, /// Spending Locktime not provided #[error("Spending condition locktime not provided")] LocktimeNotProvided, @@ -93,38 +92,25 @@ impl From for cdk_database::Error { pub struct Wallet { pub client: HttpClient, pub localstore: Arc + Send + Sync>, - mnemonic: Option, -} - -impl Default for Wallet { - fn default() -> Self { - Self { - localstore: Arc::new(WalletMemoryDatabase::default()), - client: HttpClient::default(), - mnemonic: None, - } - } + xpriv: ExtendedPrivKey, } impl Wallet { - pub async fn new( - client: HttpClient, + pub fn new( localstore: Arc + Send + Sync>, - mnemonic: Option, + seed: &[u8], ) -> Self { + let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, seed) + .expect("Could not create master key"); Self { - mnemonic, - client, + client: HttpClient::new(), localstore, + xpriv, } } - /// Back up seed - pub fn mnemonic(&self) -> Option { - self.mnemonic.clone() - } - /// Total Balance of wallet + #[instrument(skip(self))] pub async fn total_balance(&self) -> Result { let mints = self.localstore.get_mints().await?; let mut balance = Amount::ZERO; @@ -140,6 +126,7 @@ impl Wallet { Ok(balance) } + #[instrument(skip(self))] pub async fn mint_balances(&self) -> Result, Error> { let mints = self.localstore.get_mints().await?; @@ -158,10 +145,12 @@ impl Wallet { Ok(balances) } + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { Ok(self.localstore.get_proofs(mint_url).await?) } + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn add_mint(&self, mint_url: UncheckedUrl) -> Result, Error> { let mint_info = match self .client @@ -182,6 +171,7 @@ impl Wallet { Ok(mint_info) } + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_keyset_keys( &self, mint_url: &UncheckedUrl, @@ -203,6 +193,7 @@ impl Wallet { Ok(keys) } + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_keysets( &self, mint_url: &UncheckedUrl, @@ -217,6 +208,7 @@ impl Wallet { } /// Get active mint keyset + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_active_mint_keys( &self, mint_url: &UncheckedUrl, @@ -237,6 +229,7 @@ impl Wallet { } /// Refresh Mint keys + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn refresh_mint_keys(&self, mint_url: &UncheckedUrl) -> Result<(), Error> { let current_mint_keysets_info = self .client @@ -275,6 +268,7 @@ impl Wallet { } /// Check if a proof is spent + #[instrument(skip(self, proofs), fields(mint_url = %mint_url))] pub async fn check_proofs_spent( &self, mint_url: UncheckedUrl, @@ -296,6 +290,7 @@ impl Wallet { } /// Mint Quote + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn mint_quote( &mut self, mint_url: UncheckedUrl, @@ -321,6 +316,34 @@ impl Wallet { Ok(quote) } + /// Mint quote status + #[instrument(skip(self, quote_id), fields(mint_url = %mint_url))] + pub async fn mint_quote_status( + &self, + mint_url: UncheckedUrl, + quote_id: &str, + ) -> Result { + let response = self + .client + .get_mint_quote_status(mint_url.try_into()?, quote_id) + .await?; + + match self.localstore.get_mint_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + + quote.paid = response.paid; + self.localstore.add_mint_quote(quote).await?; + } + None => { + tracing::info!("Quote mint {} unknown", quote_id); + } + } + + Ok(response) + } + + #[instrument(skip(self), fields(mint_url = %mint_url))] async fn active_mint_keyset( &mut self, mint_url: &UncheckedUrl, @@ -351,6 +374,7 @@ impl Wallet { Err(Error::NoActiveKeyset) } + #[instrument(skip(self), fields(mint_url = %mint_url))] async fn active_keys( &mut self, mint_url: &UncheckedUrl, @@ -376,6 +400,7 @@ impl Wallet { } /// Mint + #[instrument(skip(self, quote_id), fields(mint_url = %mint_url))] pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result { // Check that mint is in store of mints if self.localstore.get_mint(mint_url.clone()).await?.is_none() { @@ -396,42 +421,24 @@ impl Wallet { let active_keyset_id = self.active_mint_keyset(&mint_url, "e_info.unit).await?; - let mut counter: Option = None; + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - let premint_secrets; + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; - #[cfg(not(feature = "nut13"))] - { - premint_secrets = PreMintSecrets::random(active_keyset_id, quote_info.amount)?; - } - - #[cfg(feature = "nut13")] - { - premint_secrets = match &self.mnemonic { - Some(mnemonic) => { - let count = self - .localstore - .get_keyset_counter(&active_keyset_id) - .await?; - - let count = if let Some(count) = count { - count + 1 - } else { - 0 - }; - - counter = Some(count); - PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - quote_info.amount, - false, - )? - } - None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?, - }; - } + let premint_secrets = PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + quote_info.amount, + false, + )?; let mint_res = self .client @@ -469,12 +476,9 @@ impl Wallet { self.localstore.remove_mint_quote("e_info.id).await?; // Update counter for keyset - #[cfg(feature = "nut13")] - if counter.is_some() { - self.localstore - .increment_keyset_counter(&active_keyset_id, proofs.len() as u64) - .await?; - } + self.localstore + .increment_keyset_counter(&active_keyset_id, proofs.len() as u32) + .await?; // Add new proofs to store self.localstore.add_proofs(mint_url, proofs).await?; @@ -483,6 +487,7 @@ impl Wallet { } /// Swap + #[instrument(skip(self, input_proofs), fields(mint_url = %mint_url))] pub async fn swap( &mut self, mint_url: &UncheckedUrl, @@ -516,14 +521,11 @@ impl Wallet { .ok_or(Error::UnknownKey)?, )?; - #[cfg(feature = "nut13")] - if self.mnemonic.is_some() { - let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; + let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; - self.localstore - .increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u64) - .await?; - } + self.localstore + .increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u32) + .await?; let mut keep_proofs = Proofs::new(); let proofs_to_send; @@ -590,6 +592,7 @@ impl Wallet { } /// Create Swap Payload + #[instrument(skip(self, proofs), fields(mint_url = %mint_url))] async fn create_swap( &mut self, mint_url: &UncheckedUrl, @@ -606,105 +609,65 @@ impl Wallet { let desired_amount = amount.unwrap_or(proofs_total); let change_amount = proofs_total - desired_amount; - let mut desired_messages; - let change_messages; + let (mut desired_messages, change_messages) = match spending_conditions { + Some(conditions) => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - #[cfg(not(feature = "nut13"))] - { - (desired_messages, change_messages) = match spendig_conditions { - Some(conditions) => ( + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; + + let change_premint_secrets = PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + change_amount, + false, + )?; + + ( PreMintSecrets::with_conditions(active_keyset_id, desired_amount, conditions)?, - PreMintSecrets::random(active_keyset_id, change_amount), - ), - None => ( - PreMintSecrets::random(active_keyset_id, proofs_total)?, - PreMintSecrets::default(), - ), - }; - } + change_premint_secrets, + ) + } + None => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - #[cfg(feature = "nut13")] - { - (desired_messages, change_messages) = match &self.mnemonic { - Some(mnemonic) => match spending_conditions { - Some(conditions) => { - let count = self - .localstore - .get_keyset_counter(&active_keyset_id) - .await?; + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; - let count = if let Some(count) = count { - count + 1 - } else { - 0 - }; + let premint_secrets = PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + desired_amount, + false, + )?; - let change_premint_secrets = PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - change_amount, - false, - )?; + let count = count + premint_secrets.len() as u32; - ( - PreMintSecrets::with_conditions( - active_keyset_id, - desired_amount, - conditions, - )?, - change_premint_secrets, - ) - } - None => { - let count = self - .localstore - .get_keyset_counter(&active_keyset_id) - .await?; + let change_premint_secrets = PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + change_amount, + false, + )?; - let count = if let Some(count) = count { - count + 1 - } else { - 0 - }; - - let premint_secrets = PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - desired_amount, - false, - )?; - - let count = count + premint_secrets.len() as u64; - - let change_premint_secrets = PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - change_amount, - false, - )?; - - (premint_secrets, change_premint_secrets) - } - }, - None => match spending_conditions { - Some(conditions) => ( - PreMintSecrets::with_conditions( - active_keyset_id, - desired_amount, - conditions, - )?, - PreMintSecrets::random(active_keyset_id, change_amount)?, - ), - None => ( - PreMintSecrets::random(active_keyset_id, desired_amount)?, - PreMintSecrets::random(active_keyset_id, change_amount)?, - ), - }, - }; - } + (premint_secrets, change_premint_secrets) + } + }; // Combine the BlindedMessages totoalling the desired amount with change desired_messages.combine(change_messages); @@ -720,6 +683,7 @@ impl Wallet { } /// Send + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn send( &mut self, mint_url: &UncheckedUrl, @@ -730,14 +694,16 @@ impl Wallet { ) -> Result { let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?; - let send_proofs = match input_proofs - .iter() - .map(|p| p.amount) - .sum::() - .eq(&amount) - { - true => Some(input_proofs), - false => { + let send_proofs = match ( + input_proofs + .iter() + .map(|p| p.amount) + .sum::() + .eq(&amount), + &conditions, + ) { + (true, None) => Some(input_proofs), + _ => { self.swap(mint_url, unit, Some(amount), input_proofs, conditions) .await? } @@ -754,6 +720,7 @@ impl Wallet { } /// Melt Quote + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn melt_quote( &mut self, mint_url: UncheckedUrl, @@ -784,7 +751,35 @@ impl Wallet { Ok(quote) } + /// Melt quote status + #[instrument(skip(self, quote_id), fields(mint_url = %mint_url))] + pub async fn melt_quote_status( + &self, + mint_url: UncheckedUrl, + quote_id: &str, + ) -> Result { + let response = self + .client + .get_melt_quote_status(mint_url.try_into()?, quote_id) + .await?; + + match self.localstore.get_melt_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + + quote.paid = response.paid; + self.localstore.add_melt_quote(quote).await?; + } + None => { + tracing::info!("Quote melt {} unknown", quote_id); + } + } + + Ok(response) + } + // Select proofs + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn select_proofs( &self, mint_url: UncheckedUrl, @@ -845,6 +840,7 @@ impl Wallet { } /// Melt + #[instrument(skip(self, quote_id), fields(mint_url = %mint_url))] pub async fn melt(&mut self, mint_url: &UncheckedUrl, quote_id: &str) -> Result { let quote_info = self.localstore.get_melt_quote(quote_id).await?; @@ -864,44 +860,21 @@ impl Wallet { let proofs_amount = proofs.iter().map(|p| p.amount).sum(); - let mut counter: Option = None; - let active_keyset_id = self.active_mint_keyset(mint_url, "e_info.unit).await?; - let premint_secrets; + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; - #[cfg(not(feature = "nut13"))] - { - premint_secrets = PreMintSecrets::blank(active_keyset_id, proofs_amount)?; - } + let count = if let Some(count) = count { + count + 1 + } else { + 0 + }; - #[cfg(feature = "nut13")] - { - premint_secrets = match &self.mnemonic { - Some(mnemonic) => { - let count = self - .localstore - .get_keyset_counter(&active_keyset_id) - .await?; - - let count = if let Some(count) = count { - count + 1 - } else { - 0 - }; - - counter = Some(count); - PreMintSecrets::from_seed( - active_keyset_id, - count, - mnemonic, - proofs_amount, - true, - )? - } - None => PreMintSecrets::blank(active_keyset_id, proofs_amount)?, - }; - } + let premint_secrets = + PreMintSecrets::from_xpriv(active_keyset_id, count, self.xpriv, proofs_amount, true)?; let melt_response = self .client @@ -939,12 +912,9 @@ impl Wallet { ); // Update counter for keyset - #[cfg(feature = "nut13")] - if counter.is_some() { - self.localstore - .increment_keyset_counter(&active_keyset_id, change_proofs.len() as u64) - .await?; - } + self.localstore + .increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32) + .await?; self.localstore .add_proofs(mint_url.clone(), change_proofs) @@ -961,6 +931,7 @@ impl Wallet { } /// Receive + #[instrument(skip_all)] pub async fn receive( &mut self, encoded_token: &str, @@ -977,6 +948,16 @@ impl Wallet { continue; } + // Add mint if it does not exist in the store + if self + .localstore + .get_mint(token.mint.clone()) + .await? + .is_none() + { + self.add_mint(token.mint.clone()).await?; + } + let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?; let keys = self.get_keyset_keys(&token.mint, active_keyset_id).await?; @@ -1078,12 +1059,9 @@ impl Wallet { )?; let mint_proofs = received_proofs.entry(token.mint).or_default(); - #[cfg(feature = "nut13")] - if self.mnemonic.is_some() { - self.localstore - .increment_keyset_counter(&active_keyset_id, p.len() as u64) - .await?; - } + self.localstore + .increment_keyset_counter(&active_keyset_id, p.len() as u32) + .await?; mint_proofs.extend(p); } @@ -1095,6 +1073,7 @@ impl Wallet { Ok(()) } + #[instrument(skip(self, proofs), fields(mint_url = %mint_url))] pub fn proofs_to_token( &self, mint_url: UncheckedUrl, @@ -1106,6 +1085,7 @@ impl Wallet { } #[cfg(feature = "nut13")] + #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn restore(&mut self, mint_url: UncheckedUrl) -> Result { // Check that mint is in store of mints if self.localstore.get_mint(mint_url.clone()).await?.is_none() { @@ -1124,7 +1104,7 @@ impl Wallet { while empty_batch.lt(&3) { let premint_secrets = PreMintSecrets::restore_batch( keyset.id, - &self.mnemonic.clone().ok_or(Error::MnemonicRequired)?, + self.xpriv, start_counter, start_counter + 100, )?; @@ -1178,7 +1158,7 @@ impl Wallet { #[cfg(feature = "nut13")] self.localstore - .increment_keyset_counter(&keyset.id, proofs.len() as u64) + .increment_keyset_counter(&keyset.id, proofs.len() as u32) .await?; let states = self @@ -1209,6 +1189,7 @@ impl Wallet { /// Verify all proofs in token have meet the required spend /// Can be used to allow a wallet to accept payments offline while reducing /// the risk of claiming back to the limits let by the spending_conditions + #[instrument(skip(self, token))] pub fn verify_token_p2pk( &self, token: &Token, @@ -1326,6 +1307,7 @@ impl Wallet { } /// Verify all proofs in token have a valid DLEQ proof + #[instrument(skip(self, token))] pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> { let mut keys_cache: HashMap = HashMap::new();