From f6533a08de2f8c21b67a8ef407a4884507c25cfa Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:31:12 +0200 Subject: [PATCH 01/16] Add Proofs trait to consolidate aggregate fns --- crates/cdk-redb/src/mint/mod.rs | 6 ++---- crates/cdk-sqlite/src/mint/mod.rs | 6 ++---- crates/cdk/src/cdk_database/mint_memory.rs | 6 ++---- crates/cdk/src/mint/melt.rs | 20 ++++------------- crates/cdk/src/mint/swap.rs | 8 ++----- crates/cdk/src/nuts/nut00/mod.rs | 22 +++++++++++++++++++ crates/cdk/src/nuts/nut00/token.rs | 3 ++- crates/cdk/src/types.rs | 5 +++-- crates/cdk/src/wallet/melt.rs | 17 ++++++--------- crates/cdk/src/wallet/mint.rs | 3 ++- crates/cdk/src/wallet/mod.rs | 3 ++- crates/cdk/src/wallet/proofs.rs | 25 ++++++---------------- crates/cdk/src/wallet/receive.rs | 3 ++- crates/cdk/src/wallet/send.rs | 20 ++++++----------- crates/cdk/src/wallet/swap.rs | 10 ++++----- 15 files changed, 70 insertions(+), 87 deletions(-) diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index d53055b21..0ca822e9f 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -10,6 +10,7 @@ use async_trait::async_trait; use cdk::cdk_database::MintDatabase; use cdk::dhke::hash_to_curve; use cdk::mint::{MintKeySetInfo, MintQuote}; +use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, State, @@ -603,10 +604,7 @@ impl MintDatabase for MintRedbDatabase { .filter(|p| &p.keyset_id == keyset_id) .collect::(); - let proof_ys = proofs_for_id - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let proof_ys = proofs_for_id.ys()?; assert_eq!(proofs_for_id.len(), proof_ys.len()); diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 20fc70bba..c72e5220a 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -10,6 +10,7 @@ use bitcoin::bip32::DerivationPath; use cdk::cdk_database::{self, MintDatabase}; use cdk::mint::{MintKeySetInfo, MintQuote}; use cdk::mint_url::MintUrl; +use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut05::QuoteState; use cdk::nuts::{ BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, @@ -838,10 +839,7 @@ WHERE quote_id=?; .map(sqlite_row_to_proof) .collect::, _>>()?; - proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()? + proofs.ys()? } Err(err) => match err { sqlx::Error::RowNotFound => { diff --git a/crates/cdk/src/cdk_database/mint_memory.rs b/crates/cdk/src/cdk_database/mint_memory.rs index 5ae612aa5..3b4e1e79e 100644 --- a/crates/cdk/src/cdk_database/mint_memory.rs +++ b/crates/cdk/src/cdk_database/mint_memory.rs @@ -9,6 +9,7 @@ use tokio::sync::{Mutex, RwLock}; use super::{Error, MintDatabase}; use crate::dhke::hash_to_curve; use crate::mint::{self, MintKeySetInfo, MintQuote}; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut07::State; use crate::nuts::{ nut07, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, @@ -346,10 +347,7 @@ impl MintDatabase for MintMemoryDatabase { .cloned() .collect(); - let proof_ys = proofs_for_id - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let proof_ys = proofs_for_id.ys()?; assert_eq!(proofs_for_id.len(), proof_ys.len()); diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index a46452de2..f187d282c 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -8,7 +8,7 @@ use tracing::instrument; use crate::cdk_lightning; use crate::cdk_lightning::MintLightning; use crate::cdk_lightning::PayInvoiceResponse; -use crate::dhke::hash_to_curve; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut11::enforce_sig_flag; use crate::nuts::nut11::EnforceSigFlag; use crate::{ @@ -264,11 +264,7 @@ impl Mint { } } - let ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let ys = melt_request.inputs.ys()?; // Ensure proofs are unique and not being double spent if melt_request.inputs.len() != ys.iter().collect::>().len() { @@ -374,11 +370,7 @@ impl Mint { /// quote should be unpaid #[instrument(skip_all)] pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> { - let input_ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = melt_request.inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Unspent) @@ -615,11 +607,7 @@ impl Mint { .await? .ok_or(Error::UnknownQuote)?; - let input_ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = melt_request.inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Spent) diff --git a/crates/cdk/src/mint/swap.rs b/crates/cdk/src/mint/swap.rs index f763fff1a..16a72afea 100644 --- a/crates/cdk/src/mint/swap.rs +++ b/crates/cdk/src/mint/swap.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use tracing::instrument; -use crate::dhke::hash_to_curve; +use crate::nuts::nut00::ProofsMethods; use crate::Error; use super::nut11::{enforce_sig_flag, EnforceSigFlag}; @@ -59,11 +59,7 @@ impl Mint { let proof_count = swap_request.inputs.len(); - let input_ys = swap_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = swap_request.inputs.ys()?; self.localstore .add_proofs(swap_request.inputs.clone(), None) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 98223792b..5b03a185b 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -29,6 +29,28 @@ pub use token::{Token, TokenV3, TokenV4}; /// List of [Proof] pub type Proofs = Vec; +/// Utility methods for [Proofs] +pub trait ProofsMethods { + /// Try to sum up the amounts of all [Proof]s + fn total_amount(&self) -> Result; + + /// Try to fetch the pubkeys of all [Proof]s + fn ys(&self) -> Result, Error>; +} + +impl ProofsMethods for Proofs { + fn total_amount(&self) -> Result { + Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into) + } + + fn ys(&self) -> Result, Error> { + self.iter() + .map(|p| p.y()) + .collect::, _>>() + .map_err(Into::into) + } +} + /// NUT00 Error #[derive(Debug, Error)] pub enum Error { diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index f46166207..8fe9e99fd 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -13,6 +13,7 @@ use url::Url; use super::{Error, Proof, ProofV4, Proofs}; use crate::mint_url::MintUrl; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::{CurrencyUnit, Id}; use crate::Amount; @@ -211,7 +212,7 @@ impl TokenV3 { Ok(Amount::try_sum( self.token .iter() - .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount))) + .map(|t| t.proofs.total_amount()) .collect::, _>>()?, )?) } diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index 60c360fe8..213a8e7ef 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::error::Error; use crate::mint_url::MintUrl; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions, State, @@ -34,9 +35,9 @@ impl Melted { proofs: Proofs, change_proofs: Option, ) -> Result { - let proofs_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_amount = proofs.total_amount()?; let change_amount = match &change_proofs { - Some(change_proofs) => Amount::try_sum(change_proofs.iter().map(|p| p.amount))?, + Some(change_proofs) => change_proofs.total_amount()?, None => Amount::ZERO, }; let fee_paid = proofs_amount diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index c7217c287..37b3f71e4 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -3,9 +3,10 @@ use std::str::FromStr; use lightning_invoice::Bolt11Invoice; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ dhke::construct_proofs, - nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, PublicKey, State}, + nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, State}, types::{Melted, ProofInfo}, util::unix_time, Amount, Error, Wallet, @@ -121,15 +122,12 @@ impl Wallet { return Err(Error::UnknownQuote); }; - let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_total = proofs.total_amount()?; if proofs_total < quote_info.amount + quote_info.fee_reserve { return Err(Error::InsufficientFunds); } - let ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let ys = proofs.ys()?; self.localstore.set_pending_proofs(ys).await?; let active_keyset_id = self.get_active_mint_keyset().await?.id; @@ -213,7 +211,7 @@ impl Wallet { Some(change_proofs) => { tracing::debug!( "Change amount returned from melt: {}", - Amount::try_sum(change_proofs.iter().map(|p| p.amount))? + change_proofs.total_amount()? ); // Update counter for keyset @@ -238,10 +236,7 @@ impl Wallet { self.localstore.remove_melt_quote("e_info.id).await?; - let deleted_ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let deleted_ys = proofs.ys()?; self.localstore .update_proofs(change_proof_infos, deleted_ys) .await?; diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 6b2414529..766f40c86 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -1,6 +1,7 @@ use tracing::instrument; use super::MintQuote; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, dhke::construct_proofs, @@ -242,7 +243,7 @@ impl Wallet { &keys, )?; - let minted_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let minted_amount = proofs.total_amount()?; // Remove filled quote from store self.localstore.remove_mint_quote("e_info.id).await?; diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 93e1b76b5..174a83a8b 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -35,6 +35,7 @@ mod swap; pub mod types; pub mod util; +use crate::nuts::nut00::ProofsMethods; pub use multi_mint_wallet::MultiMintWallet; pub use types::{MeltQuote, MintQuote, SendKind}; @@ -327,7 +328,7 @@ impl Wallet { .cloned() .collect(); - restored_value += Amount::try_sum(unspent_proofs.iter().map(|p| p.amount))?; + restored_value += unspent_proofs.total_amount()?; let unspent_proofs = unspent_proofs .into_iter() diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 9d652f171..79764a73d 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, - dhke::hash_to_curve, nuts::{Proof, ProofState, Proofs, PublicKey, State}, types::ProofInfo, Amount, Error, Wallet, @@ -73,11 +73,7 @@ impl Wallet { /// Checks the stats of [`Proofs`] swapping for a new [`Proof`] if unspent #[instrument(skip(self, proofs))] pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> { - let proof_ys = proofs - .iter() - // Find Y for the secret - .map(|p| hash_to_curve(p.secret.as_bytes())) - .collect::, _>>()?; + let proof_ys = proofs.ys()?; let spendable = self .client @@ -102,14 +98,7 @@ impl Wallet { pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, Error> { let spendable = self .client - .post_check_state( - self.mint_url.clone().try_into()?, - proofs - .iter() - // Find Y for the secret - .map(|p| hash_to_curve(p.secret.as_bytes())) - .collect::, _>>()?, - ) + .post_check_state(self.mint_url.clone().try_into()?, proofs.ys()?) .await?; let spent_ys: Vec<_> = spendable .states @@ -186,7 +175,7 @@ impl Wallet { ) -> Result { // TODO: Check all proofs are same unit - if Amount::try_sum(proofs.iter().map(|p| p.amount))? < amount { + if proofs.total_amount()? < amount { return Err(Error::InsufficientFunds); } @@ -226,7 +215,7 @@ impl Wallet { } remaining_amount = amount.checked_add(fees).ok_or(Error::AmountOverflow)? - - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + - selected_proofs.total_amount()?; (proofs_larger, proofs_smaller) = proofs_smaller .into_iter() .skip(1) @@ -262,7 +251,7 @@ impl Wallet { for inactive_proof in inactive_proofs { selected_proofs.push(inactive_proof); - let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_total = selected_proofs.total_amount()?; let fees = self.get_proofs_fee(&selected_proofs).await?; if selected_total >= amount + fees { @@ -274,7 +263,7 @@ impl Wallet { for active_proof in active_proofs { selected_proofs.push(active_proof); - let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_total = selected_proofs.total_amount()?; let fees = self.get_proofs_fee(&selected_proofs).await?; if selected_total >= amount + fees { diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 88dc4a081..898eb40c2 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -4,6 +4,7 @@ use bitcoin::hashes::Hash; use bitcoin::{hashes::sha256::Hash as Sha256Hash, XOnlyPublicKey}; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut10::Kind; use crate::nuts::{Conditions, Token}; use crate::{ @@ -148,7 +149,7 @@ impl Wallet { .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32) .await?; - let total_amount = Amount::try_sum(recv_proofs.iter().map(|p| p.amount))?; + let total_amount = recv_proofs.total_amount()?; let recv_proof_infos = recv_proofs .into_iter() diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index a3024bde7..1e8b99fc2 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -1,8 +1,9 @@ use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, - nuts::{Proofs, PublicKey, SpendingConditions, State, Token}, + nuts::{Proofs, SpendingConditions, State, Token}, Amount, Error, Wallet, }; @@ -12,10 +13,7 @@ impl Wallet { /// Send specific proofs #[instrument(skip(self))] pub async fn send_proofs(&self, memo: Option, proofs: Proofs) -> Result { - let ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let ys = proofs.ys()?; self.localstore.reserve_proofs(ys).await?; Ok(Token::new( @@ -114,8 +112,7 @@ impl Wallet { let send_proofs: Proofs = match (send_kind, selected, conditions.clone()) { // Handle exact matches offline (SendKind::OfflineExact, Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -131,8 +128,7 @@ impl Wallet { // Handle exact matches (SendKind::OnlineExact, Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -152,8 +148,7 @@ impl Wallet { // Handle offline tolerance (SendKind::OfflineTolerance(tolerance), Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -178,8 +173,7 @@ impl Wallet { // Handle online tolerance with successful selection (SendKind::OnlineTolerance(tolerance), Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, false => amount, diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 7f59866ae..82274fb99 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -2,6 +2,7 @@ use tracing::instrument; use crate::amount::SplitTarget; use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut10; use crate::nuts::PreMintSecrets; use crate::nuts::PreSwap; @@ -85,8 +86,7 @@ impl Wallet { let mut proofs_to_keep = Vec::new(); for proof in all_proofs { - let proofs_to_send_amount = - Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?; + let proofs_to_send_amount = proofs_to_send.total_amount()?; if proof.amount + proofs_to_send_amount <= amount + pre_swap.fee { proofs_to_send.push(proof); } else { @@ -98,7 +98,7 @@ impl Wallet { } }; - let send_amount = Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?; + let send_amount = proofs_to_send.total_amount()?; if send_amount.ne(&(amount + pre_swap.fee)) { tracing::warn!( @@ -199,9 +199,9 @@ impl Wallet { let active_keyset_id = self.get_active_mint_keyset().await?.id; // Desired amount is either amount passed or value of all proof - let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_total = proofs.total_amount()?; - let ys: Vec = proofs.iter().map(|p| p.y()).collect::>()?; + let ys: Vec = proofs.ys()?; self.localstore.set_pending_proofs(ys).await?; let fee = self.get_proofs_fee(&proofs).await?; From 84428a9503ff853f90aa877bba6b971034e1365f Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 20 Oct 2024 10:52:44 +0100 Subject: [PATCH 02/16] feat: verify mint keyset id when getting keys --- crates/cdk/src/nuts/nut02.rs | 16 ++++++++++++++++ crates/cdk/src/wallet/keysets.rs | 2 ++ 2 files changed, 18 insertions(+) diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index f38855933..6a6656e8f 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -43,6 +43,9 @@ pub enum Error { /// Unknown version #[error("NUT02: Unknown Version")] UnknownVersion, + /// Keyset id does not match + #[error("Keyset id incorrect")] + IncorrectKeysetId, /// Slice Error #[error(transparent)] Slice(#[from] TryFromSliceError), @@ -242,6 +245,19 @@ pub struct KeySet { pub keys: Keys, } +impl KeySet { + /// Verify the keyset is matches keys + pub fn verify_id(&self) -> Result<(), Error> { + let keys_id: Id = (&self.keys).into(); + + if keys_id != self.id { + return Err(Error::IncorrectKeysetId); + } + + Ok(()) + } +} + #[cfg(feature = "mint")] impl From for KeySet { fn from(keyset: MintKeySet) -> Self { diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index a8c1f27ad..5a7f680e3 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -21,6 +21,8 @@ impl Wallet { .get_mint_keyset(self.mint_url.clone().try_into()?, keyset_id) .await?; + keys.verify_id()?; + self.localstore.add_keys(keys.keys.clone()).await?; keys.keys From 98c94e28bb62d2d0bec4041899cc0191d9951b56 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:33:22 +0200 Subject: [PATCH 03/16] Simplify MintUrl, use it directly in wallet/client.rs --- crates/cdk-cli/src/sub_commands/mint_info.rs | 3 +- crates/cdk/src/mint_url.rs | 41 +++-------- crates/cdk/src/nuts/nut00/mod.rs | 6 -- crates/cdk/src/nuts/nut00/token.rs | 4 -- crates/cdk/src/wallet/client.rs | 71 ++++++++------------ crates/cdk/src/wallet/keysets.rs | 12 +--- crates/cdk/src/wallet/melt.rs | 6 +- crates/cdk/src/wallet/mint.rs | 10 +-- crates/cdk/src/wallet/mod.rs | 8 +-- crates/cdk/src/wallet/proofs.rs | 4 +- crates/cdk/src/wallet/receive.rs | 7 +- crates/cdk/src/wallet/swap.rs | 2 +- 12 files changed, 52 insertions(+), 122 deletions(-) diff --git a/crates/cdk-cli/src/sub_commands/mint_info.rs b/crates/cdk-cli/src/sub_commands/mint_info.rs index 17804f2fd..bcf9f5ec2 100644 --- a/crates/cdk-cli/src/sub_commands/mint_info.rs +++ b/crates/cdk-cli/src/sub_commands/mint_info.rs @@ -6,7 +6,6 @@ use url::Url; #[derive(Args)] pub struct MintInfoSubcommand { - /// Cashu Token mint_url: MintUrl, } @@ -17,7 +16,7 @@ pub async fn mint_info(proxy: Option, sub_command_args: &MintInfoSubcommand }; let info = client - .get_mint_info(sub_command_args.mint_url.clone().try_into()?) + .get_mint_info(sub_command_args.mint_url.clone()) .await?; println!("{:#?}", info); diff --git a/crates/cdk/src/mint_url.rs b/crates/cdk/src/mint_url.rs index 1232ee399..917d6ad81 100644 --- a/crates/cdk/src/mint_url.rs +++ b/crates/cdk/src/mint_url.rs @@ -6,7 +6,7 @@ use core::fmt; use core::str::FromStr; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use thiserror::Error; use url::{ParseError, Url}; @@ -22,7 +22,7 @@ pub enum Error { } /// MintUrl Url -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct MintUrl(String); impl MintUrl { @@ -60,25 +60,16 @@ impl MintUrl { Ok(formatted_url) } - /// Empty mint url - pub fn empty() -> Self { - Self(String::new()) - } - /// Join onto url pub fn join(&self, path: &str) -> Result { - let url: Url = self.try_into()?; - Ok(url.join(path)?) + Url::parse(&self.0) + .and_then(|url| url.join(path)) + .map_err(Into::into) } -} -impl<'de> Deserialize<'de> for MintUrl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - MintUrl::from_str(&s).map_err(serde::de::Error::custom) + /// Append path elements onto the URL + pub fn join_paths(&self, path_elements: &[&str]) -> Result { + self.join(&path_elements.join("/")) } } @@ -94,22 +85,6 @@ impl FromStr for MintUrl { } } -impl TryFrom for Url { - type Error = Error; - - fn try_from(mint_url: MintUrl) -> Result { - Ok(Self::parse(&mint_url.0)?) - } -} - -impl TryFrom<&MintUrl> for Url { - type Error = Error; - - fn try_from(mint_url: &MintUrl) -> Result { - Ok(Self::parse(mint_url.0.as_str())?) - } -} - impl fmt::Display for MintUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 5b03a185b..ea0e395f0 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -66,9 +66,6 @@ pub enum Error { /// Unsupported token #[error("Unsupported payment method")] UnsupportedPaymentMethod, - /// Invalid Url - #[error("Invalid URL")] - InvalidUrl, /// Serde Json error #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -78,9 +75,6 @@ pub enum Error { /// Base64 error #[error(transparent)] Base64Error(#[from] bitcoin::base64::DecodeError), - /// Parse Url Error - #[error(transparent)] - UrlParseError(#[from] url::ParseError), /// Ciborium error #[error(transparent)] CiboriumError(#[from] ciborium::de::Error), diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index 8fe9e99fd..abaa10240 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -9,7 +9,6 @@ use std::str::FromStr; use bitcoin::base64::engine::{general_purpose, GeneralPurpose}; use bitcoin::base64::{alphabet, Engine as _}; use serde::{Deserialize, Serialize}; -use url::Url; use super::{Error, Proof, ProofV4, Proofs}; use crate::mint_url::MintUrl; @@ -181,9 +180,6 @@ impl TokenV3 { return Err(Error::ProofsRequired); } - // Check Url is valid - let _: Url = (&mint_url).try_into().map_err(|_| Error::InvalidUrl)?; - Ok(Self { token: vec![TokenV3Token::new(mint_url, proofs)], memo, diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index c99723a19..d5b413ce4 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -7,6 +7,7 @@ use url::Url; use super::Error; use crate::error::ErrorResponse; +use crate::mint_url::MintUrl; use crate::nuts::nut15::Mpp; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, @@ -17,24 +18,6 @@ use crate::nuts::{ }; use crate::{Amount, Bolt11Invoice}; -fn join_url(url: Url, paths: &[&str]) -> Result { - let mut url = url; - for path in paths { - if !url.path().ends_with('/') { - url.path_segments_mut() - .map_err(|_| Error::UrlPathSegments)? - .push(path); - } else { - url.path_segments_mut() - .map_err(|_| Error::UrlPathSegments)? - .pop() - .push(path); - } - } - - Ok(url) -} - /// Http Client #[derive(Debug, Clone)] pub struct HttpClient { @@ -87,8 +70,8 @@ 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"])?; + pub async fn get_mint_keys(&self, mint_url: MintUrl) -> Result, Error> { + let url = mint_url.join_paths(&["v1", "keys"])?; let keys = self.inner.get(url).send().await?.json::().await?; match serde_json::from_value::(keys.clone()) { @@ -99,8 +82,8 @@ 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()])?; + pub async fn get_mint_keyset(&self, mint_url: MintUrl, keyset_id: Id) -> Result { + let url = mint_url.join_paths(&["v1", "keys", &keyset_id.to_string()])?; let keys = self.inner.get(url).send().await?.json::().await?; match serde_json::from_value::(keys.clone()) { @@ -111,8 +94,8 @@ 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"])?; + pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result { + let url = mint_url.join_paths(&["v1", "keysets"])?; let res = self.inner.get(url).send().await?.json::().await?; match serde_json::from_value::(res.clone()) { @@ -125,12 +108,12 @@ impl HttpClient { #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_mint_quote( &self, - mint_url: Url, + mint_url: MintUrl, amount: Amount, unit: CurrencyUnit, description: Option, ) -> Result { - let url = join_url(mint_url, &["v1", "mint", "quote", "bolt11"])?; + let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt11"])?; let request = MintQuoteBolt11Request { amount, @@ -160,10 +143,10 @@ impl HttpClient { #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_quote_status( &self, - mint_url: Url, + mint_url: MintUrl, quote_id: &str, ) -> Result { - let url = join_url(mint_url, &["v1", "mint", "quote", "bolt11", quote_id])?; + let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -180,11 +163,11 @@ impl HttpClient { #[instrument(skip(self, quote, premint_secrets), fields(mint_url = %mint_url))] pub async fn post_mint( &self, - mint_url: Url, + mint_url: MintUrl, quote: &str, premint_secrets: PreMintSecrets, ) -> Result { - let url = join_url(mint_url, &["v1", "mint", "bolt11"])?; + let url = mint_url.join_paths(&["v1", "mint", "bolt11"])?; let request = MintBolt11Request { quote: quote.to_string(), @@ -210,12 +193,12 @@ impl HttpClient { #[instrument(skip(self, request), fields(mint_url = %mint_url))] pub async fn post_melt_quote( &self, - mint_url: Url, + mint_url: MintUrl, unit: CurrencyUnit, request: Bolt11Invoice, mpp_amount: Option, ) -> Result { - let url = join_url(mint_url, &["v1", "melt", "quote", "bolt11"])?; + let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt11"])?; let options = mpp_amount.map(|amount| Mpp { amount }); @@ -244,10 +227,10 @@ impl HttpClient { #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_melt_quote_status( &self, - mint_url: Url, + mint_url: MintUrl, quote_id: &str, ) -> Result { - let url = join_url(mint_url, &["v1", "melt", "quote", "bolt11", quote_id])?; + let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -262,12 +245,12 @@ impl HttpClient { #[instrument(skip(self, quote, inputs, outputs), fields(mint_url = %mint_url))] pub async fn post_melt( &self, - mint_url: Url, + mint_url: MintUrl, quote: String, inputs: Vec, outputs: Option>, ) -> Result { - let url = join_url(mint_url, &["v1", "melt", "bolt11"])?; + let url = mint_url.join_paths(&["v1", "melt", "bolt11"])?; let request = MeltBolt11Request { quote, @@ -299,10 +282,10 @@ impl HttpClient { #[instrument(skip(self, swap_request), fields(mint_url = %mint_url))] pub async fn post_swap( &self, - mint_url: Url, + mint_url: MintUrl, swap_request: SwapRequest, ) -> Result { - let url = join_url(mint_url, &["v1", "swap"])?; + let url = mint_url.join_paths(&["v1", "swap"])?; let res = self .inner @@ -321,8 +304,8 @@ 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"])?; + pub async fn get_mint_info(&self, mint_url: MintUrl) -> Result { + let url = mint_url.join_paths(&["v1", "info"])?; let res = self.inner.get(url).send().await?.json::().await?; @@ -339,10 +322,10 @@ impl HttpClient { #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_check_state( &self, - mint_url: Url, + mint_url: MintUrl, ys: Vec, ) -> Result { - let url = join_url(mint_url, &["v1", "checkstate"])?; + let url = mint_url.join_paths(&["v1", "checkstate"])?; let request = CheckStateRequest { ys }; let res = self @@ -364,10 +347,10 @@ impl HttpClient { #[instrument(skip(self, request), fields(mint_url = %mint_url))] pub async fn post_restore( &self, - mint_url: Url, + mint_url: MintUrl, request: RestoreRequest, ) -> Result { - let url = join_url(mint_url, &["v1", "restore"])?; + let url = mint_url.join_paths(&["v1", "restore"])?; let res = self .inner diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index a8c1f27ad..77fe39fe5 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -18,7 +18,7 @@ impl Wallet { } else { let keys = self .client - .get_mint_keyset(self.mint_url.clone().try_into()?, keyset_id) + .get_mint_keyset(self.mint_url.clone(), keyset_id) .await?; self.localstore.add_keys(keys.keys.clone()).await?; @@ -34,10 +34,7 @@ impl Wallet { /// Queries mint for all keysets #[instrument(skip(self))] pub async fn get_mint_keysets(&self) -> Result, Error> { - let keysets = self - .client - .get_mint_keysets(self.mint_url.clone().try_into()?) - .await?; + let keysets = self.client.get_mint_keysets(self.mint_url.clone()).await?; self.localstore .add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone()) @@ -52,10 +49,7 @@ impl Wallet { /// keysets #[instrument(skip(self))] pub async fn get_active_mint_keyset(&self) -> Result { - let keysets = self - .client - .get_mint_keysets(self.mint_url.clone().try_into()?) - .await?; + let keysets = self.client.get_mint_keysets(self.mint_url.clone()).await?; let keysets = keysets.keysets; self.localstore diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index 37b3f71e4..6d0f709b2 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -59,7 +59,7 @@ impl Wallet { let quote_res = self .client - .post_melt_quote(self.mint_url.clone().try_into()?, self.unit, invoice, mpp) + .post_melt_quote(self.mint_url.clone(), self.unit, invoice, mpp) .await?; if quote_res.amount != amount { @@ -90,7 +90,7 @@ impl Wallet { ) -> Result { let response = self .client - .get_melt_quote_status(self.mint_url.clone().try_into()?, quote_id) + .get_melt_quote_status(self.mint_url.clone(), quote_id) .await?; match self.localstore.get_melt_quote(quote_id).await? { @@ -149,7 +149,7 @@ impl Wallet { let melt_response = self .client .post_melt( - self.mint_url.clone().try_into()?, + self.mint_url.clone(), quote_id.to_string(), proofs.clone(), Some(premint_secrets.blinded_messages()), diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 766f40c86..3717edad4 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -67,7 +67,7 @@ impl Wallet { let quote_res = self .client - .post_mint_quote(mint_url.clone().try_into()?, amount, unit, description) + .post_mint_quote(mint_url.clone(), amount, unit, description) .await?; let quote = MintQuote { @@ -90,7 +90,7 @@ impl Wallet { pub async fn mint_quote_state(&self, quote_id: &str) -> Result { let response = self .client - .get_mint_quote_status(self.mint_url.clone().try_into()?, quote_id) + .get_mint_quote_status(self.mint_url.clone(), quote_id) .await?; match self.localstore.get_mint_quote(quote_id).await? { @@ -215,11 +215,7 @@ impl Wallet { let mint_res = self .client - .post_mint( - self.mint_url.clone().try_into()?, - quote_id, - premint_secrets.clone(), - ) + .post_mint(self.mint_url.clone(), quote_id, premint_secrets.clone()) .await?; let keys = self.get_keyset_keys(active_keyset_id).await?; diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 174a83a8b..0d9e9bec3 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -161,11 +161,7 @@ impl Wallet { /// Qeury mint for current mint information #[instrument(skip(self))] pub async fn get_mint_info(&self) -> Result, Error> { - let mint_info = match self - .client - .get_mint_info(self.mint_url.clone().try_into()?) - .await - { + let mint_info = match self.client.get_mint_info(self.mint_url.clone()).await { Ok(mint_info) => Some(mint_info), Err(err) => { tracing::warn!("Could not get mint info {}", err); @@ -281,7 +277,7 @@ impl Wallet { let response = self .client - .post_restore(self.mint_url.clone().try_into()?, restore_request) + .post_restore(self.mint_url.clone(), restore_request) .await?; if response.signatures.is_empty() { diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 79764a73d..5ea4f53ba 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -77,7 +77,7 @@ impl Wallet { let spendable = self .client - .post_check_state(self.mint_url.clone().try_into()?, proof_ys) + .post_check_state(self.mint_url.clone(), proof_ys) .await? .states; @@ -98,7 +98,7 @@ impl Wallet { pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, Error> { let spendable = self .client - .post_check_state(self.mint_url.clone().try_into()?, proofs.ys()?) + .post_check_state(self.mint_url.clone(), proofs.ys()?) .await?; let spent_ys: Vec<_> = spendable .states diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 898eb40c2..fe565b30f 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -34,10 +34,7 @@ impl Wallet { .await? .is_none() { - tracing::debug!( - "Mint not in localstore fetching info for: {}", - self.mint_url - ); + tracing::debug!("Mint not in localstore fetching info for: {mint_url}"); self.get_mint_info().await?; } @@ -134,7 +131,7 @@ impl Wallet { let swap_response = self .client - .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) + .post_swap(mint_url.clone(), pre_swap.swap_request) .await?; // Proof to keep diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 82274fb99..043d37c55 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -42,7 +42,7 @@ impl Wallet { let swap_response = self .client - .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) + .post_swap(mint_url.clone(), pre_swap.swap_request) .await?; let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id; From 6ee79feff50168928878ee073ae6af356c2bdb76 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:45:06 +0200 Subject: [PATCH 04/16] fix: typos (#411) --- README.md | 2 +- crates/cdk-cli/README.md | 2 +- crates/cdk-phoenixd/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8714af90..174184b22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ > **Warning** -> This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing. +> This project is in early development, it does however work with real sats! Always use amounts you don't mind losing. # Cashu Development Kit diff --git a/crates/cdk-cli/README.md b/crates/cdk-cli/README.md index de5bfa523..169a47203 100644 --- a/crates/cdk-cli/README.md +++ b/crates/cdk-cli/README.md @@ -1,5 +1,5 @@ > **Warning** -> This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing. +> This project is in early development, it does however work with real sats! Always use amounts you don't mind losing. cdk-cli is a CLI wallet implementation using of CDK(../cdk) diff --git a/crates/cdk-phoenixd/README.md b/crates/cdk-phoenixd/README.md index 046566f91..a3a9a63e7 100644 --- a/crates/cdk-phoenixd/README.md +++ b/crates/cdk-phoenixd/README.md @@ -29,7 +29,7 @@ Once the node is running, create an invoice using the phoenixd-cli to fund your ## Check Channel state -After paying the invoice view that a channal has been opened. +After paying the invoice view that a channel has been opened. ```sh ./phoenix-cli listchannels ``` From dffc30233c25297d5780f4dd44e0ad4678873cdf Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sat, 5 Oct 2024 11:53:34 +0200 Subject: [PATCH 05/16] mintd: add utoipa swagger UI --- .github/workflows/ci.yml | 2 + crates/cdk-axum/Cargo.toml | 4 + crates/cdk-axum/src/lib.rs | 111 ++++++++++++++- crates/cdk-axum/src/router_handlers.rs | 174 +++++++++++++++++++++++- crates/cdk-mintd/Cargo.toml | 5 + crates/cdk-mintd/example.config.toml | 1 + crates/cdk-mintd/src/config.rs | 7 + crates/cdk-mintd/src/main.rs | 14 +- crates/cdk-redb/src/wallet/mod.rs | 11 +- crates/cdk/Cargo.toml | 2 + crates/cdk/src/amount.rs | 1 + crates/cdk/src/error.rs | 4 +- crates/cdk/src/nuts/nut00/mod.rs | 10 ++ crates/cdk/src/nuts/nut01/mod.rs | 2 + crates/cdk/src/nuts/nut01/public_key.rs | 2 + crates/cdk/src/nuts/nut01/secret_key.rs | 2 + crates/cdk/src/nuts/nut02.rs | 5 + crates/cdk/src/nuts/nut03.rs | 3 + crates/cdk/src/nuts/nut04.rs | 9 ++ crates/cdk/src/nuts/nut05.rs | 11 +- crates/cdk/src/nuts/nut06.rs | 5 + crates/cdk/src/nuts/nut07.rs | 8 +- crates/cdk/src/nuts/nut09.rs | 2 + crates/cdk/src/nuts/nut11/mod.rs | 1 + crates/cdk/src/nuts/nut12.rs | 7 + crates/cdk/src/nuts/nut14/mod.rs | 1 + crates/cdk/src/nuts/nut15.rs | 3 + flake.nix | 2 +- 28 files changed, 392 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd3956f8c..37149ba2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,7 @@ jobs: -p cdk --no-default-features, -p cdk --no-default-features --features wallet, -p cdk --no-default-features --features mint, + -p cdk --no-default-features --features "mint swagger", -p cdk-redb, -p cdk-sqlite, -p cdk-axum, @@ -143,6 +144,7 @@ jobs: -p cdk --no-default-features, -p cdk --no-default-features --features wallet, -p cdk --no-default-features --features mint, + -p cdk --no-default-features --features "mint swagger", -p cdk-axum, -p cdk-strike, -p cdk-lnbits, diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index 45a71d4b5..241da9627 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -15,7 +15,11 @@ axum = "0.6.20" cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } tokio = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } +utoipa = { version = "4", features = ["preserve_order", "preserve_path_order"], optional = true } futures = { version = "0.3.28", default-features = false } moka = { version = "0.11.1", features = ["future"] } serde_json = "1" paste = "1.0.15" + +[features] +swagger = ["cdk/swagger", "dep:utoipa"] \ No newline at end of file diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index 35078446d..9083163b0 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -4,6 +4,7 @@ #![warn(rustdoc::bare_urls)] use std::sync::Arc; +use std::time::Duration; use anyhow::Result; use axum::routing::{get, post}; @@ -11,10 +12,42 @@ use axum::Router; use cdk::mint::Mint; use moka::future::Cache; use router_handlers::*; -use std::time::Duration; mod router_handlers; +#[cfg(feature = "swagger")] +mod swagger_imports { + pub use cdk::amount::Amount; + pub use cdk::error::{ErrorCode, ErrorResponse}; + pub use cdk::nuts::nut00::{ + BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proof, Witness, + }; + pub use cdk::nuts::nut01::{Keys, KeysResponse, PublicKey, SecretKey}; + pub use cdk::nuts::nut02::{Id, KeySet, KeySetInfo, KeySetVersion, KeysetResponse}; + pub use cdk::nuts::nut03::{SwapRequest, SwapResponse}; + pub use cdk::nuts::nut04; + pub use cdk::nuts::nut04::{ + MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request, + MintQuoteBolt11Response, + }; + pub use cdk::nuts::nut05; + pub use cdk::nuts::nut05::{ + MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response, + }; + pub use cdk::nuts::nut06::{ContactInfo, MintInfo, MintVersion, Nuts, SupportedSettings}; + pub use cdk::nuts::nut07::{CheckStateRequest, CheckStateResponse, ProofState, State}; + pub use cdk::nuts::nut09::{RestoreRequest, RestoreResponse}; + pub use cdk::nuts::nut11::P2PKWitness; + pub use cdk::nuts::nut12::{BlindSignatureDleq, ProofDleq}; + pub use cdk::nuts::nut14::HTLCWitness; + pub use cdk::nuts::nut15; + pub use cdk::nuts::nut15::{Mpp, MppMethodSettings}; + pub use cdk::nuts::{MeltQuoteState, MintQuoteState}; +} + +#[cfg(feature = "swagger")] +use swagger_imports::*; + /// CDK Mint State #[derive(Clone)] pub struct MintState { @@ -22,6 +55,82 @@ pub struct MintState { cache: Cache, } +#[cfg(feature = "swagger")] +#[derive(utoipa::OpenApi)] +#[openapi( + components(schemas( + Amount, + BlindedMessage, + BlindSignature, + BlindSignatureDleq, + CheckStateRequest, + CheckStateResponse, + ContactInfo, + CurrencyUnit, + ErrorCode, + ErrorResponse, + HTLCWitness, + Id, + Keys, + KeysResponse, + KeysetResponse, + KeySet, + KeySetInfo, + KeySetVersion, + MeltBolt11Request, + MeltQuoteBolt11Request, + MeltQuoteBolt11Response, + MeltQuoteState, + MeltMethodSettings, + MintBolt11Request, + MintBolt11Response, + MintInfo, + MintQuoteBolt11Request, + MintQuoteBolt11Response, + MintQuoteState, + MintMethodSettings, + MintVersion, + Mpp, + MppMethodSettings, + Nuts, + P2PKWitness, + PaymentMethod, + Proof, + ProofDleq, + ProofState, + PublicKey, + RestoreRequest, + RestoreResponse, + SecretKey, + State, + SupportedSettings, + SwapRequest, + SwapResponse, + Witness, + nut04::Settings, + nut05::Settings, + nut15::Settings + )), + info(description = "Cashu CDK mint APIs", title = "cdk-mintd",), + paths( + get_keys, + get_keyset_pubkeys, + get_keysets, + get_mint_info, + get_mint_bolt11_quote, + get_check_mint_bolt11_quote, + post_mint_bolt11, + get_melt_bolt11_quote, + get_check_melt_bolt11_quote, + post_melt_bolt11, + post_swap, + post_check, + post_restore + ) +)] +/// OpenAPI spec for the mint's v1 APIs +pub struct ApiDocV1; + /// Create mint [`Router`] with required endpoints for cashu mint pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) -> Result { let state = MintState { diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 0ee99d896..c4ba781ab 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -49,6 +49,17 @@ post_cache_wrapper!(post_swap, SwapRequest, SwapResponse); post_cache_wrapper!(post_mint_bolt11, MintBolt11Request, MintBolt11Response); post_cache_wrapper!(post_melt_bolt11, MeltBolt11Request, MeltQuoteBolt11Response); +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/keys", + responses( + (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json") + ) +))] +/// Get the public keys of the newest mint keyset +/// +/// This endpoint returns a dictionary of all supported token values of the mint and their associated public key. pub async fn get_keys(State(state): State) -> Result, Response> { let pubkeys = state.mint.pubkeys().await.map_err(|err| { tracing::error!("Could not get keys: {}", err); @@ -58,6 +69,21 @@ pub async fn get_keys(State(state): State) -> Result, Path(keyset_id): Path, @@ -70,15 +96,40 @@ pub async fn get_keyset_pubkeys( Ok(Json(pubkeys)) } +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/keysets", + responses( + (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Get all active keyset IDs of the mint +/// +/// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from. pub async fn get_keysets(State(state): State) -> Result, Response> { - let mint = state.mint.keysets().await.map_err(|err| { - tracing::error!("Could not get keyset: {}", err); + let keysets = state.mint.keysets().await.map_err(|err| { + tracing::error!("Could not get keysets: {}", err); into_response(err) })?; - Ok(Json(mint)) + Ok(Json(keysets)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/mint/quote/bolt11", + request_body(content = MintQuoteBolt11Request, description = "Request params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MintQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Request a quote for minting of new tokens +/// +/// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow. pub async fn get_mint_bolt11_quote( State(state): State, Json(payload): Json, @@ -92,6 +143,21 @@ pub async fn get_mint_bolt11_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/mint/quote/bolt11/{quote_id}", + params( + ("quote_id" = String, description = "The quote ID"), + ), + responses( + (status = 200, description = "Successful response", body = MintQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Get mint quote by ID +/// +/// Get mint quote state. pub async fn get_check_mint_bolt11_quote( State(state): State, Path(quote_id): Path, @@ -108,6 +174,21 @@ pub async fn get_check_mint_bolt11_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/mint/bolt11", + request_body(content = MintBolt11Request, description = "Request params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Mint tokens by paying a BOLT11 Lightning invoice. +/// +/// Requests the minting of tokens belonging to a paid payment request. +/// +/// Call this endpoint after `POST /v1/mint/quote`. pub async fn post_mint_bolt11( State(state): State, Json(payload): Json, @@ -124,6 +205,17 @@ pub async fn post_mint_bolt11( Ok(Json(res)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/melt/quote/bolt11", + request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Request a quote for melting tokens pub async fn get_melt_bolt11_quote( State(state): State, Json(payload): Json, @@ -137,6 +229,21 @@ pub async fn get_melt_bolt11_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/melt/quote/bolt11/{quote_id}", + params( + ("quote_id" = String, description = "The quote ID"), + ), + responses( + (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Get melt quote by ID +/// +/// Get melt quote state. pub async fn get_check_melt_bolt11_quote( State(state): State, Path(quote_id): Path, @@ -153,6 +260,19 @@ pub async fn get_check_melt_bolt11_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/melt/bolt11", + request_body(content = MeltBolt11Request, description = "Melt params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange +/// +/// Requests tokens to be destroyed and sent out via Lightning. pub async fn post_melt_bolt11( State(state): State, Json(payload): Json, @@ -166,6 +286,19 @@ pub async fn post_melt_bolt11( Ok(Json(res)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/checkstate", + request_body(content = CheckStateRequest, description = "State params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = CheckStateResponse, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Check whether a proof is spent already or is pending in a transaction +/// +/// Check whether a secret has been spent already or not. pub async fn post_check( State(state): State, Json(payload): Json, @@ -178,10 +311,34 @@ pub async fn post_check( Ok(Json(state)) } +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/info", + responses( + (status = 200, description = "Successful response", body = MintInfo) + ) +))] +/// Mint information, operator contact information, and other info pub async fn get_mint_info(State(state): State) -> Result, Response> { Ok(Json(state.mint.mint_info().clone().time(unix_time()))) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/swap", + request_body(content = SwapRequest, description = "Swap params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = SwapResponse, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Swap inputs for outputs of the same value +/// +/// Requests a set of Proofs to be swapped for another set of BlindSignatures. +/// +/// This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. It can then used by Carol to redeem the tokens for new proofs. pub async fn post_swap( State(state): State, Json(payload): Json, @@ -197,6 +354,17 @@ pub async fn post_swap( Ok(Json(swap_response)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/restore", + request_body(content = RestoreRequest, description = "Restore params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = RestoreResponse, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Restores blind signature for a set of outputs. pub async fn post_restore( State(state): State, Json(payload): Json, diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index c61db669c..34640215e 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -34,3 +34,8 @@ tower-http = { version = "0.4.4", features = ["cors"] } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } home = "0.5.5" url = "2.3" +utoipa = { version = "4", optional = true } +utoipa-swagger-ui = { version = "4", features = ["axum"], optional = true } + +[features] +swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] \ No newline at end of file diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 7878d1254..448f1f7a6 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -4,6 +4,7 @@ listen_host = "127.0.0.1" listen_port = 8085 mnemonic = "" # input_fee_ppk = 0 +# enable_swagger_ui = false diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 756c2d148..5e94f69bc 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -15,6 +15,12 @@ pub struct Info { pub seconds_to_cache_requests_for: Option, pub seconds_to_extend_cache_by: Option, pub input_fee_ppk: Option, + + /// When this is set to true, the mint exposes a Swagger UI for it's API at + /// `[listen_host]:[listen_port]/swagger-ui` + /// + /// This requires `mintd` was built with the `swagger` feature flag. + pub enable_swagger_ui: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] @@ -94,6 +100,7 @@ pub struct Database { pub engine: DatabaseEngine, } +/// CDK settings, derived from `config.toml` #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { pub info: Info, diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 6462236b4..4adc2809d 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -36,6 +36,8 @@ use tokio::sync::{Mutex, Notify}; use tower_http::cors::CorsLayer; use tracing_subscriber::EnvFilter; use url::Url; +#[cfg(feature = "swagger")] +use utoipa::OpenApi; mod cli; mod config; @@ -451,7 +453,7 @@ async fn main() -> anyhow::Result<()> { // Checks the status of all pending melt quotes // Pending melt quotes where the payment has gone through inputs are burnt - // Pending melt quotes where the paynment has **failed** inputs are reset to unspent + // Pending melt quotes where the payment has **failed** inputs are reset to unspent check_pending_melt_quotes(Arc::clone(&mint), &ln_backends).await?; let listen_addr = settings.info.listen_host; @@ -475,6 +477,16 @@ async fn main() -> anyhow::Result<()> { .merge(v1_service) .layer(CorsLayer::permissive()); + #[cfg(feature = "swagger")] + { + if settings.info.enable_swagger_ui.unwrap_or(false) { + mint_service = mint_service.merge( + utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") + .url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()), + ); + } + } + for router in ln_routers { mint_service = mint_service.merge(router); } diff --git a/crates/cdk-redb/src/wallet/mod.rs b/crates/cdk-redb/src/wallet/mod.rs index 1e73f467b..29e1ff50e 100644 --- a/crates/cdk-redb/src/wallet/mod.rs +++ b/crates/cdk-redb/src/wallet/mod.rs @@ -649,14 +649,9 @@ impl WalletDatabase for WalletRedbDatabase { let mut proof = None; if let Ok(proof_info) = serde_json::from_str::(v.value()) { - match proof_info.matches_conditions( - &mint_url, - &unit, - &state, - &spending_conditions, - ) { - true => proof = Some(proof_info), - false => (), + if proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions) + { + proof = Some(proof_info) } } diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 6df40fdb4..b72e50d23 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" [features] default = ["mint", "wallet"] mint = ["dep:futures"] +swagger = ["mint", "dep:utoipa"] wallet = ["dep:reqwest"] bench = [] @@ -39,6 +40,7 @@ tracing = { version = "0.1", default-features = false, features = ["attributes", thiserror = "1" futures = { version = "0.3.28", default-features = false, optional = true } url = "2.3" +utoipa = { version = "4", optional = true } uuid = { version = "1", features = ["v4"] } # -Z minimal-versions diff --git a/crates/cdk/src/amount.rs b/crates/cdk/src/amount.rs index 33181dd31..d2c599d04 100644 --- a/crates/cdk/src/amount.rs +++ b/crates/cdk/src/amount.rs @@ -27,6 +27,7 @@ pub enum Error { /// Amount can be any unit #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] #[serde(transparent)] pub struct Amount(u64); diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 5a39d7312..5ca14821b 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -242,8 +242,9 @@ pub enum Error { /// CDK Error Response /// -/// See NUT defenation in [00](https://github.com/cashubtc/nuts/blob/main/00.md) +/// See NUT definition in [00](https://github.com/cashubtc/nuts/blob/main/00.md) #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct ErrorResponse { /// Error Code pub code: ErrorCode, @@ -399,6 +400,7 @@ impl From for Error { /// Possible Error Codes #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum ErrorCode { /// Token is already spent TokenAlreadySpent, diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 5b03a185b..57117bf01 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -103,6 +103,7 @@ pub enum Error { /// Blinded Message (also called `output`) #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct BlindedMessage { /// Amount /// @@ -117,6 +118,7 @@ pub struct BlindedMessage { /// /// The blinded secret message generated by the sender. #[serde(rename = "B_")] + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub blinded_secret: PublicKey, /// Witness /// @@ -146,6 +148,7 @@ impl BlindedMessage { /// Blind Signature (also called `promise`) #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct BlindSignature { /// Amount /// @@ -160,6 +163,7 @@ pub struct BlindSignature { /// /// The blinded signature on the secret message `B_` of [BlindedMessage]. #[serde(rename = "C_")] + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub c: PublicKey, /// DLEQ Proof /// @@ -183,6 +187,7 @@ impl PartialOrd for BlindSignature { /// Witness #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(untagged)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum Witness { /// P2PK Witness #[serde(with = "serde_p2pk_witness")] @@ -226,6 +231,7 @@ impl Witness { /// Proofs #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Proof { /// Amount pub amount: Amount, @@ -233,9 +239,11 @@ pub struct Proof { #[serde(rename = "id")] pub keyset_id: Id, /// Secret message + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub secret: Secret, /// Unblinded signature #[serde(rename = "C")] + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub c: PublicKey, /// Witness #[serde(skip_serializing_if = "Option::is_none")] @@ -360,6 +368,7 @@ where /// Currency Unit #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum CurrencyUnit { /// Sat #[default] @@ -436,6 +445,7 @@ impl<'de> Deserialize<'de> for CurrencyUnit { /// Payment Method #[non_exhaustive] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum PaymentMethod { /// Bolt11 payment type #[default] diff --git a/crates/cdk/src/nuts/nut01/mod.rs b/crates/cdk/src/nuts/nut01/mod.rs index 12819222c..bda245fd4 100644 --- a/crates/cdk/src/nuts/nut01/mod.rs +++ b/crates/cdk/src/nuts/nut01/mod.rs @@ -43,6 +43,7 @@ pub enum Error { /// /// See [NUT-01] #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Keys(BTreeMap); impl From for Keys { @@ -85,6 +86,7 @@ impl Keys { /// Mint Public Keys [NUT-01] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct KeysResponse { /// Keysets #[serde_as(as = "VecSkipError<_>")] diff --git a/crates/cdk/src/nuts/nut01/public_key.rs b/crates/cdk/src/nuts/nut01/public_key.rs index ddb58b81f..23e2f1dee 100644 --- a/crates/cdk/src/nuts/nut01/public_key.rs +++ b/crates/cdk/src/nuts/nut01/public_key.rs @@ -13,7 +13,9 @@ use crate::SECP256K1; /// PublicKey #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct PublicKey { + #[cfg_attr(feature = "swagger", schema(value_type = String))] inner: secp256k1::PublicKey, } diff --git a/crates/cdk/src/nuts/nut01/secret_key.rs b/crates/cdk/src/nuts/nut01/secret_key.rs index 11b7e00b3..23eca6e34 100644 --- a/crates/cdk/src/nuts/nut01/secret_key.rs +++ b/crates/cdk/src/nuts/nut01/secret_key.rs @@ -15,7 +15,9 @@ use crate::SECP256K1; /// SecretKey #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SecretKey { + #[cfg_attr(feature = "swagger", schema(value_type = String))] inner: secp256k1::SecretKey, } diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index 6a6656e8f..32e961909 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -53,6 +53,7 @@ pub enum Error { /// Keyset version #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum KeySetVersion { /// Current Version 00 Version00, @@ -88,6 +89,7 @@ impl fmt::Display for KeySetVersion { /// be stored in a Cashu token such that the token can be used to identify /// which mint or keyset it was generated from. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Id { version: KeySetVersion, id: [u8; Self::BYTELEN], @@ -228,6 +230,7 @@ impl From<&Keys> for Id { /// Ids of mints keyset ids #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct KeysetResponse { /// set of public key ids that the mint generates #[serde_as(as = "VecSkipError<_>")] @@ -236,6 +239,7 @@ pub struct KeysetResponse { /// Keyset #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct KeySet { /// Keyset [`Id`] pub id: Id, @@ -271,6 +275,7 @@ impl From for KeySet { /// KeySetInfo #[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct KeySetInfo { /// Keyset [`Id`] pub id: Id, diff --git a/crates/cdk/src/nuts/nut03.rs b/crates/cdk/src/nuts/nut03.rs index eca008ba4..535c89a9e 100644 --- a/crates/cdk/src/nuts/nut03.rs +++ b/crates/cdk/src/nuts/nut03.rs @@ -34,8 +34,10 @@ pub struct PreSwap { /// Split Request [NUT-06] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SwapRequest { /// Proofs that are to be spent in `Split` + #[cfg_attr(feature = "swagger", schema(value_type = Vec))] pub inputs: Proofs, /// Blinded Messages for Mint to sign pub outputs: Vec, @@ -64,6 +66,7 @@ impl SwapRequest { /// Split Response [NUT-06] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SwapResponse { /// Promises pub signatures: Vec, diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index fd60a57ac..3067628f6 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -26,6 +26,7 @@ pub enum Error { /// Mint quote request [NUT-04] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintQuoteBolt11Request { /// Amount pub amount: Amount, @@ -38,6 +39,7 @@ pub struct MintQuoteBolt11Request { /// Possible states of a quote #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))] pub enum QuoteState { /// Quote has not been paid #[default] @@ -79,6 +81,7 @@ impl FromStr for QuoteState { /// Mint quote response [NUT-04] #[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintQuoteBolt11Response { /// Quote Id pub quote: String, @@ -175,10 +178,13 @@ impl From for MintQuoteBolt11Response { /// Mint request [NUT-04] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintBolt11Request { /// Quote id + #[cfg_attr(feature = "swagger", schema(max_length = 1_000))] pub quote: String, /// Outputs + #[cfg_attr(feature = "swagger", schema(max_items = 1_000))] pub outputs: Vec, } @@ -196,6 +202,7 @@ impl MintBolt11Request { /// Mint response [NUT-04] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintBolt11Response { /// Blinded Signatures pub signatures: Vec, @@ -203,6 +210,7 @@ pub struct MintBolt11Response { /// Mint Method Settings #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintMethodSettings { /// Payment Method e.g. bolt11 pub method: PaymentMethod, @@ -221,6 +229,7 @@ pub struct MintMethodSettings { /// Mint Settings #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))] pub struct Settings { /// Methods to mint pub methods: Vec, diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index c459341d8..e7ac3153a 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -13,6 +13,7 @@ use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, use super::nut15::Mpp; #[cfg(feature = "mint")] use crate::mint; +use crate::nuts::MeltQuoteState; use crate::{Amount, Bolt11Invoice}; /// NUT05 Error @@ -28,8 +29,10 @@ pub enum Error { /// Melt quote request [NUT-05] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MeltQuoteBolt11Request { /// Bolt11 invoice to be paid + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub request: Bolt11Invoice, /// Unit wallet would like to pay with pub unit: CurrencyUnit, @@ -40,6 +43,7 @@ pub struct MeltQuoteBolt11Request { /// Possible states of a quote #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))] pub enum QuoteState { /// Quote has not been paid #[default] @@ -83,6 +87,7 @@ impl FromStr for QuoteState { /// Melt quote response [NUT-05] #[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MeltQuoteBolt11Response { /// Quote Id pub quote: String, @@ -95,7 +100,7 @@ pub struct MeltQuoteBolt11Response { /// Deprecated pub paid: Option, /// Quote State - pub state: QuoteState, + pub state: MeltQuoteState, /// Unix timestamp until the quote is valid pub expiry: u64, /// Payment preimage @@ -209,10 +214,12 @@ impl From for MeltQuoteBolt11Response { /// Melt Bolt11 Request [NUT-05] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MeltBolt11Request { /// Quote ID pub quote: String, /// Proofs + #[cfg_attr(feature = "swagger", schema(value_type = Vec))] pub inputs: Proofs, /// Blinded Message that can be used to return change [NUT-08] /// Amount field of BlindedMessages `SHOULD` be set to zero @@ -229,6 +236,7 @@ impl MeltBolt11Request { /// Melt Method Settings #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MeltMethodSettings { /// Payment Method e.g. bolt11 pub method: PaymentMethod, @@ -266,6 +274,7 @@ impl Settings { /// Melt Settings #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))] pub struct Settings { /// Methods to melt pub methods: Vec, diff --git a/crates/cdk/src/nuts/nut06.rs b/crates/cdk/src/nuts/nut06.rs index e2d4465f0..358cc4ff4 100644 --- a/crates/cdk/src/nuts/nut06.rs +++ b/crates/cdk/src/nuts/nut06.rs @@ -9,6 +9,7 @@ use super::{nut04, nut05, nut15, MppMethodSettings}; /// Mint Version #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintVersion { /// Mint Software name pub name: String, @@ -52,6 +53,7 @@ impl<'de> Deserialize<'de> for MintVersion { /// Mint Info [NIP-06] #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintInfo { /// name of the mint and should be recognizable #[serde(skip_serializing_if = "Option::is_none")] @@ -188,6 +190,7 @@ impl MintInfo { /// Supported nuts and settings #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Nuts { /// NUT04 Settings #[serde(default)] @@ -322,12 +325,14 @@ impl Nuts { /// Check state Settings #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SupportedSettings { supported: bool, } /// Contact Info #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct ContactInfo { /// Contact Method i.e. nostr pub method: String, diff --git a/crates/cdk/src/nuts/nut07.rs b/crates/cdk/src/nuts/nut07.rs index 3b69102d3..b56830ea1 100644 --- a/crates/cdk/src/nuts/nut07.rs +++ b/crates/cdk/src/nuts/nut07.rs @@ -21,6 +21,7 @@ pub enum Error { /// State of Proof #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum State { /// Spent Spent, @@ -63,19 +64,23 @@ impl FromStr for State { } } -/// Check spendabale request [NUT-07] +/// Check spendable request [NUT-07] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct CheckStateRequest { /// Y's of the proofs to check #[serde(rename = "Ys")] + #[cfg_attr(feature = "swagger", schema(value_type = Vec, max_items = 1_000))] pub ys: Vec, } /// Proof state [NUT-07] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct ProofState { /// Y of proof #[serde(rename = "Y")] + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub y: PublicKey, /// State of proof pub state: State, @@ -85,6 +90,7 @@ pub struct ProofState { /// Check Spendable Response [NUT-07] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct CheckStateResponse { /// Proof states pub states: Vec, diff --git a/crates/cdk/src/nuts/nut09.rs b/crates/cdk/src/nuts/nut09.rs index 672ef8de9..abcd8f85a 100644 --- a/crates/cdk/src/nuts/nut09.rs +++ b/crates/cdk/src/nuts/nut09.rs @@ -8,6 +8,7 @@ use super::nut00::{BlindSignature, BlindedMessage}; /// Restore Request [NUT-09] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct RestoreRequest { /// Outputs pub outputs: Vec, @@ -15,6 +16,7 @@ pub struct RestoreRequest { /// Restore Response [NUT-09] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct RestoreResponse { /// Outputs pub outputs: Vec, diff --git a/crates/cdk/src/nuts/nut11/mod.rs b/crates/cdk/src/nuts/nut11/mod.rs index 28d466ecd..6d407e2b1 100644 --- a/crates/cdk/src/nuts/nut11/mod.rs +++ b/crates/cdk/src/nuts/nut11/mod.rs @@ -88,6 +88,7 @@ pub enum Error { /// P2Pk Witness #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct P2PKWitness { /// Signatures pub signatures: Vec, diff --git a/crates/cdk/src/nuts/nut12.rs b/crates/cdk/src/nuts/nut12.rs index f3928a6e2..232368b04 100644 --- a/crates/cdk/src/nuts/nut12.rs +++ b/crates/cdk/src/nuts/nut12.rs @@ -41,10 +41,13 @@ pub enum Error { /// /// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md) #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct BlindSignatureDleq { /// e + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub e: SecretKey, /// s + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub s: SecretKey, } @@ -52,12 +55,16 @@ pub struct BlindSignatureDleq { /// /// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md) #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct ProofDleq { /// e + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub e: SecretKey, /// s + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub s: SecretKey, /// Blinding factor + #[cfg_attr(feature = "swagger", schema(value_type = String))] pub r: SecretKey, } diff --git a/crates/cdk/src/nuts/nut14/mod.rs b/crates/cdk/src/nuts/nut14/mod.rs index 7f53f41f5..07da54dc4 100644 --- a/crates/cdk/src/nuts/nut14/mod.rs +++ b/crates/cdk/src/nuts/nut14/mod.rs @@ -52,6 +52,7 @@ pub enum Error { /// HTLC Witness #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct HTLCWitness { /// Primage pub preimage: String, diff --git a/crates/cdk/src/nuts/nut15.rs b/crates/cdk/src/nuts/nut15.rs index e61a861b7..abaa5c422 100644 --- a/crates/cdk/src/nuts/nut15.rs +++ b/crates/cdk/src/nuts/nut15.rs @@ -10,6 +10,7 @@ use crate::Amount; /// Multi-part payment #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename = "lowercase")] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct Mpp { /// Amount pub amount: Amount, @@ -17,6 +18,7 @@ pub struct Mpp { /// Mpp Method Settings #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MppMethodSettings { /// Payment Method e.g. bolt11 pub method: PaymentMethod, @@ -28,6 +30,7 @@ pub struct MppMethodSettings { /// Mpp Settings #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut15::Settings))] pub struct Settings { /// Method settings pub methods: Vec, diff --git a/flake.nix b/flake.nix index c1344873d..b40785577 100644 --- a/flake.nix +++ b/flake.nix @@ -135,7 +135,7 @@ cargo update -p serde_with --precise 3.1.0 cargo update -p regex --precise 1.9.6 cargo update -p backtrace --precise 0.3.58 - # For wasm32-unknown-unknown target + # For wasm32-unknown-unknown target cargo update -p bumpalo --precise 3.12.0 cargo update -p moka --precise 0.11.1 cargo update -p triomphe --precise 0.1.11 From 2905b36a1c57eed4c49f7227c4e6d273564e0d80 Mon Sep 17 00:00:00 2001 From: mubarak23 Date: Wed, 23 Oct 2024 12:57:47 +0100 Subject: [PATCH 06/16] chore:change cli command name from Pay to Melt --- crates/cdk-cli/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 0de66bc51..c452b9523 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -51,7 +51,7 @@ enum Commands { /// Balance Balance, /// Pay bolt11 invoice - Pay(sub_commands::melt::MeltSubCommand), + Melt(sub_commands::melt::MeltSubCommand), /// Claim pending mint quotes that have been paid MintPending, /// Receive token @@ -158,7 +158,7 @@ async fn main() -> Result<()> { sub_commands::decode_token::decode_token(sub_command_args) } Commands::Balance => sub_commands::balance::balance(&multi_mint_wallet).await, - Commands::Pay(sub_command_args) => { + Commands::Melt(sub_command_args) => { sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await } Commands::Receive(sub_command_args) => { From 419b1a360d83a2b1a4a149e387956ff3cc19943f Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:07:47 +0200 Subject: [PATCH 07/16] TokenV4: unit is mandatory --- crates/cdk/src/nuts/nut00/token.rs | 29 +++++++++++++---------------- crates/cdk/src/wallet/mint.rs | 1 - crates/cdk/src/wallet/send.rs | 7 +------ crates/cdk/src/wallet/types.rs | 3 +-- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index abaa10240..12ffbd5db 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -43,7 +43,7 @@ impl Token { mint_url: MintUrl, proofs: Proofs, memo: Option, - unit: Option, + unit: CurrencyUnit, ) -> Self { let proofs = proofs .into_iter() @@ -90,10 +90,10 @@ impl Token { } /// Unit - pub fn unit(&self) -> &Option { + pub fn unit(&self) -> Option { match self { Self::TokenV3(token) => token.unit(), - Self::TokenV4(token) => token.unit(), + Self::TokenV4(token) => Some(token.unit()), } } @@ -219,8 +219,8 @@ impl TokenV3 { } #[inline] - fn unit(&self) -> &Option { - &self.unit + fn unit(&self) -> Option { + self.unit } } @@ -257,7 +257,7 @@ impl From for TokenV3 { TokenV3 { token: vec![TokenV3Token::new(mint_url, proofs)], memo: token.memo, - unit: token.unit, + unit: Some(token.unit), } } } @@ -269,14 +269,12 @@ pub struct TokenV4 { #[serde(rename = "m")] pub mint_url: MintUrl, /// Token Unit - #[serde(rename = "u", skip_serializing_if = "Option::is_none")] - pub unit: Option, + #[serde(rename = "u")] + pub unit: CurrencyUnit, /// Memo for token #[serde(rename = "d", skip_serializing_if = "Option::is_none")] pub memo: Option, - /// Proofs - /// - /// Proofs separated by keyset_id + /// Proofs grouped by keyset_id #[serde(rename = "t")] pub token: Vec, } @@ -319,8 +317,8 @@ impl TokenV4 { } #[inline] - fn unit(&self) -> &Option { - &self.unit + fn unit(&self) -> CurrencyUnit { + self.unit } } @@ -374,7 +372,7 @@ impl TryFrom for TokenV4 { mint_url: mint_url.to_owned(), token: proofs, memo: token.memo, - unit: token.unit, + unit: token.unit.ok_or(Error::UnsupportedUnit)?, }) } } @@ -469,8 +467,7 @@ mod tests { assert_eq!(amount, Amount::from(4)); - let unit = (*token.unit()).unwrap(); - + let unit = token.unit().unwrap(); assert_eq!(CurrencyUnit::Sat, unit); match token { diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 3717edad4..b429daaf8 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -47,7 +47,6 @@ impl Wallet { let mint_url = self.mint_url.clone(); let unit = self.unit; - // If we have a description, we check that the mint supports it. // If we have a description, we check that the mint supports it. if description.is_some() { let mint_method_settings = self diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index 1e8b99fc2..f9a62b9a7 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -16,12 +16,7 @@ impl Wallet { let ys = proofs.ys()?; self.localstore.reserve_proofs(ys).await?; - Ok(Token::new( - self.mint_url.clone(), - proofs, - memo, - Some(self.unit), - )) + Ok(Token::new(self.mint_url.clone(), proofs, memo, self.unit)) } /// Send diff --git a/crates/cdk/src/wallet/types.rs b/crates/cdk/src/wallet/types.rs index 301c63ee1..309a4c1cf 100644 --- a/crates/cdk/src/wallet/types.rs +++ b/crates/cdk/src/wallet/types.rs @@ -54,8 +54,7 @@ pub enum SendKind { OnlineExact, /// Prefer offline send if difference is less then tolerance OnlineTolerance(Amount), - /// Wallet cannot do an online swap and selectedp proof must be exactly send - /// amount + /// Wallet cannot do an online swap and selected proof must be exactly send amount OfflineExact, /// Wallet must remain offline but can over pay if below tolerance OfflineTolerance(Amount), From c91297aac64b7d8309a062d67d8a9ba94a198a97 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:13:56 +0200 Subject: [PATCH 08/16] Minor rewrites for clarity in melt.rs --- crates/cdk/src/mint/melt.rs | 58 ++++++++++++------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index f187d282c..53268b4d2 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -34,36 +34,20 @@ impl Mint { return Err(Error::MeltingDisabled); } - match nut05.get_settings(&unit, &method) { - Some(settings) => { - if settings - .max_amount - .map_or(false, |max_amount| amount > max_amount) - { - return Err(Error::AmountOutofLimitRange( - settings.min_amount.unwrap_or_default(), - settings.max_amount.unwrap_or_default(), - amount, - )); - } - - if settings - .min_amount - .map_or(false, |min_amount| amount < min_amount) - { - return Err(Error::AmountOutofLimitRange( - settings.min_amount.unwrap_or_default(), - settings.max_amount.unwrap_or_default(), - amount, - )); - } - } - None => { - return Err(Error::UnitUnsupported); - } + let settings = nut05 + .get_settings(&unit, &method) + .ok_or(Error::UnitUnsupported)?; + + let is_above_max = matches!(settings.max_amount, Some(max) if amount > max); + let is_below_min = matches!(settings.min_amount, Some(min) if amount < min); + match is_above_max || is_below_min { + true => Err(Error::AmountOutofLimitRange( + settings.min_amount.unwrap_or_default(), + settings.max_amount.unwrap_or_default(), + amount, + )), + false => Ok(()), } - - Ok(()) } /// Get melt bolt11 quote @@ -252,17 +236,11 @@ impl Mint { .await?; match state { - MeltQuoteState::Unpaid | MeltQuoteState::Failed => (), - MeltQuoteState::Pending => { - return Err(Error::PendingQuote); - } - MeltQuoteState::Paid => { - return Err(Error::PaidQuote); - } - MeltQuoteState::Unknown => { - return Err(Error::UnknownPaymentState); - } - } + MeltQuoteState::Unpaid | MeltQuoteState::Failed => Ok(()), + MeltQuoteState::Pending => Err(Error::PendingQuote), + MeltQuoteState::Paid => Err(Error::PaidQuote), + MeltQuoteState::Unknown => Err(Error::UnknownPaymentState), + }?; let ys = melt_request.inputs.ys()?; From 4e6bf594beab803c9980d4535f23a3c8240dc437 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 08:56:04 +0100 Subject: [PATCH 09/16] feat: remove multimint support --- crates/cdk-cli/src/sub_commands/receive.rs | 2 +- crates/cdk/src/nuts/nut00/token.rs | 113 +++++++------ crates/cdk/src/wallet/mod.rs | 179 +++++++++++---------- crates/cdk/src/wallet/multi_mint_wallet.rs | 46 +++--- crates/cdk/src/wallet/receive.rs | 4 +- 5 files changed, 177 insertions(+), 167 deletions(-) diff --git a/crates/cdk-cli/src/sub_commands/receive.rs b/crates/cdk-cli/src/sub_commands/receive.rs index 24eea39b5..f63bed667 100644 --- a/crates/cdk-cli/src/sub_commands/receive.rs +++ b/crates/cdk-cli/src/sub_commands/receive.rs @@ -137,7 +137,7 @@ async fn receive_token( ) -> Result { let token: Token = Token::from_str(token_str)?; - let mint_url = token.proofs().into_keys().next().expect("Mint in token"); + let mint_url = token.mint_url()?; let wallet_key = WalletKey::new(mint_url.clone(), token.unit().unwrap_or_default()); diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index 12ffbd5db..96e15302a 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -66,7 +66,7 @@ impl Token { } /// Proofs in [`Token`] - pub fn proofs(&self) -> HashMap { + pub fn proofs(&self) -> Proofs { match self { Self::TokenV3(token) => token.proofs(), Self::TokenV4(token) => token.proofs(), @@ -92,11 +92,27 @@ impl Token { /// Unit pub fn unit(&self) -> Option { match self { - Self::TokenV3(token) => token.unit(), + Self::TokenV3(token) => *token.unit(), Self::TokenV4(token) => Some(token.unit()), } } + /// Mint url + pub fn mint_url(&self) -> Result { + match self { + Self::TokenV3(token) => { + let mint_urls = token.mint_urls(); + + if mint_urls.len() != 1 { + return Err(Error::UnsupportedToken); + } + + Ok(mint_urls.first().expect("Length is checked above").clone()) + } + Self::TokenV4(token) => Ok(token.mint_url.clone()), + } + } + /// To v3 string pub fn to_v3_string(&self) -> String { let v3_token = match self { @@ -187,24 +203,17 @@ impl TokenV3 { }) } - fn proofs(&self) -> HashMap { - let mut proofs: HashMap = HashMap::new(); - - for token in self.token.clone() { - let mint_url = token.mint; - let mut mint_proofs = token.proofs; - - proofs - .entry(mint_url) - .and_modify(|p| p.append(&mut mint_proofs)) - .or_insert(mint_proofs); - } - - proofs + /// Proofs + pub fn proofs(&self) -> Proofs { + self.token + .iter() + .flat_map(|token| token.proofs.clone()) + .collect() } + /// Value #[inline] - fn value(&self) -> Result { + pub fn value(&self) -> Result { Ok(Amount::try_sum( self.token .iter() @@ -213,14 +222,27 @@ impl TokenV3 { )?) } + /// Memo #[inline] - fn memo(&self) -> &Option { + pub fn memo(&self) -> &Option { &self.memo } + /// Unit #[inline] - fn unit(&self) -> Option { - self.unit + pub fn unit(&self) -> &Option { + &self.unit + } + + /// Mint Url + pub fn mint_urls(&self) -> Vec { + let mut mint_urls = Vec::new(); + + for token in self.token.iter() { + mint_urls.push(token.mint.clone()); + } + + mint_urls } } @@ -249,13 +271,10 @@ impl fmt::Display for TokenV3 { impl From for TokenV3 { fn from(token: TokenV4) -> Self { - let (mint_url, proofs) = token - .proofs() - .into_iter() - .next() - .expect("Token has no proofs"); + let proofs = token.proofs(); + TokenV3 { - token: vec![TokenV3Token::new(mint_url, proofs)], + token: vec![TokenV3Token::new(token.mint_url, proofs)], memo: token.memo, unit: Some(token.unit), } @@ -281,28 +300,16 @@ pub struct TokenV4 { impl TokenV4 { /// Proofs from token - pub fn proofs(&self) -> HashMap { - let mint_url = &self.mint_url; - let mut proofs: HashMap = HashMap::new(); - - for token in self.token.clone() { - let mut mint_proofs = token - .proofs - .iter() - .map(|p| p.into_proof(&token.keyset_id)) - .collect(); - - proofs - .entry(mint_url.clone()) - .and_modify(|p| p.append(&mut mint_proofs)) - .or_insert(mint_proofs); - } - - proofs + pub fn proofs(&self) -> Proofs { + self.token + .iter() + .flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id))) + .collect() } + /// Value #[inline] - fn value(&self) -> Result { + pub fn value(&self) -> Result { Ok(Amount::try_sum( self.token .iter() @@ -311,13 +318,15 @@ impl TokenV4 { )?) } + /// Memo #[inline] - fn memo(&self) -> &Option { + pub fn memo(&self) -> &Option { &self.memo } + /// Unit #[inline] - fn unit(&self) -> CurrencyUnit { + pub fn unit(&self) -> CurrencyUnit { self.unit } } @@ -350,13 +359,15 @@ impl TryFrom for TokenV4 { type Error = Error; fn try_from(token: TokenV3) -> Result { let proofs = token.proofs(); - if proofs.len() != 1 { + let mint_urls = token.mint_urls(); + + if mint_urls.len() != 1 { return Err(Error::UnsupportedToken); } - let (mint_url, mint_proofs) = proofs.iter().next().expect("No proofs"); + let mint_url = mint_urls.first().expect("Len is checked"); - let proofs = mint_proofs + let proofs = proofs .iter() .fold(HashMap::new(), |mut acc, val| { acc.entry(val.keyset_id) @@ -369,7 +380,7 @@ impl TryFrom for TokenV4 { .collect(); Ok(TokenV4 { - mint_url: mint_url.to_owned(), + mint_url: mint_url.clone(), token: proofs, memo: token.memo, unit: token.unit.ok_or(Error::UnsupportedUnit)?, diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 0d9e9bec3..c0488c2f6 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -393,87 +393,87 @@ impl Wallet { "Must set locktime".to_string(), )); } + if token.mint_url()? != self.mint_url { + return Err(Error::IncorrectWallet(format!( + "Should be {} not {}", + self.mint_url, + token.mint_url()? + ))); + } - for (mint_url, proofs) in &token.proofs() { - if mint_url != &self.mint_url { - return Err(Error::IncorrectWallet(format!( - "Should be {} not {}", - self.mint_url, mint_url - ))); - } - for proof in proofs { - let secret: nut10::Secret = (&proof.secret).try_into()?; + let proofs = token.proofs(); + for proof in proofs { + let secret: nut10::Secret = (&proof.secret).try_into()?; - let proof_conditions: SpendingConditions = secret.try_into()?; + let proof_conditions: SpendingConditions = secret.try_into()?; - if num_sigs.ne(&proof_conditions.num_sigs()) { - tracing::debug!( - "Spending condition requires: {:?} sigs proof secret specifies: {:?}", - num_sigs, - proof_conditions.num_sigs() - ); + if num_sigs.ne(&proof_conditions.num_sigs()) { + tracing::debug!( + "Spending condition requires: {:?} sigs proof secret specifies: {:?}", + num_sigs, + proof_conditions.num_sigs() + ); - return Err(Error::P2PKConditionsNotMet( - "Num sigs did not match spending condition".to_string(), - )); - } + return Err(Error::P2PKConditionsNotMet( + "Num sigs did not match spending condition".to_string(), + )); + } - let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default(); - let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default(); + let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default(); + let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default(); - // Check the Proof has the required pubkeys - if proof_pubkeys.len().ne(&spending_condition_pubkeys.len()) - || !proof_pubkeys - .iter() - .all(|pubkey| spending_condition_pubkeys.contains(pubkey)) - { - tracing::debug!("Proof did not included Publickeys meeting condition"); - tracing::debug!("{:?}", proof_pubkeys); - tracing::debug!("{:?}", spending_condition_pubkeys); - return Err(Error::P2PKConditionsNotMet( - "Pubkeys in proof not allowed by spending condition".to_string(), - )); - } + // Check the Proof has the required pubkeys + if proof_pubkeys.len().ne(&spending_condition_pubkeys.len()) + || !proof_pubkeys + .iter() + .all(|pubkey| spending_condition_pubkeys.contains(pubkey)) + { + tracing::debug!("Proof did not included Publickeys meeting condition"); + tracing::debug!("{:?}", proof_pubkeys); + tracing::debug!("{:?}", spending_condition_pubkeys); + return Err(Error::P2PKConditionsNotMet( + "Pubkeys in proof not allowed by spending condition".to_string(), + )); + } - // If spending condition refund keys is allowed (Some(Empty Vec)) - // If spending conition refund keys is allowed to restricted set of keys check - // it is one of them Check that proof locktime is > condition - // locktime + // If spending condition refund keys is allowed (Some(Empty Vec)) + // If spending conition refund keys is allowed to restricted set of keys check + // it is one of them Check that proof locktime is > condition + // locktime - if let Some(proof_refund_keys) = proof_conditions.refund_keys() { - let proof_locktime = proof_conditions - .locktime() - .ok_or(Error::LocktimeNotProvided)?; + if let Some(proof_refund_keys) = proof_conditions.refund_keys() { + let proof_locktime = proof_conditions + .locktime() + .ok_or(Error::LocktimeNotProvided)?; - if let (Some(condition_refund_keys), Some(condition_locktime)) = - (&refund_keys, locktime) + if let (Some(condition_refund_keys), Some(condition_locktime)) = + (&refund_keys, locktime) + { + // Proof locktime must be greater then condition locktime to ensure it + // cannot be claimed back + if proof_locktime.lt(&condition_locktime) { + return Err(Error::P2PKConditionsNotMet( + "Proof locktime less then required".to_string(), + )); + } + + // A non empty condition refund key list is used as a restricted set of keys + // returns are allowed to An empty list means the + // proof can be refunded to anykey set in the secret + if !condition_refund_keys.is_empty() + && !proof_refund_keys + .iter() + .all(|refund_key| condition_refund_keys.contains(refund_key)) { - // Proof locktime must be greater then condition locktime to ensure it - // cannot be claimed back - if proof_locktime.lt(&condition_locktime) { - return Err(Error::P2PKConditionsNotMet( - "Proof locktime less then required".to_string(), - )); - } - - // A non empty condition refund key list is used as a restricted set of keys - // returns are allowed to An empty list means the - // proof can be refunded to anykey set in the secret - if !condition_refund_keys.is_empty() - && !proof_refund_keys - .iter() - .all(|refund_key| condition_refund_keys.contains(refund_key)) - { - return Err(Error::P2PKConditionsNotMet( - "Refund Key not allowed".to_string(), - )); - } - } else { - // Spending conditions does not allow refund keys return Err(Error::P2PKConditionsNotMet( - "Spending condition does not allow refund keys".to_string(), + "Refund Key not allowed".to_string(), )); } + } else { + // Spending conditions does not allow refund keys + return Err(Error::P2PKConditionsNotMet( + "Spending condition does not allow refund keys".to_string(), + )); } } } @@ -486,31 +486,32 @@ impl Wallet { pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> { let mut keys_cache: HashMap = HashMap::new(); - for (mint_url, proofs) in &token.proofs() { - if mint_url != &self.mint_url { - return Err(Error::IncorrectWallet(format!( - "Should be {} not {}", - self.mint_url, mint_url - ))); - } - for proof in proofs { - let mint_pubkey = match keys_cache.get(&proof.keyset_id) { - Some(keys) => keys.amount_key(proof.amount), - None => { - let keys = self.get_keyset_keys(proof.keyset_id).await?; + // TODO: Get mint url + // if mint_url != &self.mint_url { + // return Err(Error::IncorrectWallet(format!( + // "Should be {} not {}", + // self.mint_url, mint_url + // ))); + // } - let key = keys.amount_key(proof.amount); - keys_cache.insert(proof.keyset_id, keys); + let proofs = token.proofs(); + for proof in proofs { + let mint_pubkey = match keys_cache.get(&proof.keyset_id) { + Some(keys) => keys.amount_key(proof.amount), + None => { + let keys = self.get_keyset_keys(proof.keyset_id).await?; - key - } - } - .ok_or(Error::AmountKey)?; + let key = keys.amount_key(proof.amount); + keys_cache.insert(proof.keyset_id, keys); - proof - .verify_dleq(mint_pubkey) - .map_err(|_| Error::CouldNotVerifyDleq)?; + key + } } + .ok_or(Error::AmountKey)?; + + proof + .verify_dleq(mint_pubkey) + .map_err(|_| Error::CouldNotVerifyDleq)?; } Ok(()) diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index 3852fe496..6f3c33341 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -237,36 +237,36 @@ impl MultiMintWallet { let token_data = Token::from_str(encoded_token)?; let unit = token_data.unit().unwrap_or_default(); - let mint_proofs = token_data.proofs(); + let proofs = token_data.proofs(); let mut amount_received = Amount::ZERO; let mut mint_errors = None; + let mint_url = token_data.mint_url()?; + // Check that all mints in tokes have wallets - for (mint_url, proofs) in mint_proofs { - let wallet_key = WalletKey::new(mint_url.clone(), unit); - if !self.has(&wallet_key).await { - return Err(Error::UnknownWallet(wallet_key.clone())); - } + let wallet_key = WalletKey::new(mint_url.clone(), unit); + if !self.has(&wallet_key).await { + return Err(Error::UnknownWallet(wallet_key.clone())); + } - let wallet_key = WalletKey::new(mint_url.clone(), unit); - let wallet = self - .get_wallet(&wallet_key) - .await - .ok_or(Error::UnknownWallet(wallet_key.clone()))?; - - match wallet - .receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages) - .await - { - Ok(amount) => { - amount_received += amount; - } - Err(err) => { - tracing::error!("Could no receive proofs for mint: {}", err); - mint_errors = Some(err); - } + let wallet_key = WalletKey::new(mint_url.clone(), unit); + let wallet = self + .get_wallet(&wallet_key) + .await + .ok_or(Error::UnknownWallet(wallet_key.clone()))?; + + match wallet + .receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages) + .await + { + Ok(amount) => { + amount_received += amount; + } + Err(err) => { + tracing::error!("Could no receive proofs for mint: {}", err); + mint_errors = Some(err); } } diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index fe565b30f..d5999232a 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -207,9 +207,7 @@ impl Wallet { return Err(Error::MultiMintTokenNotSupported); } - let (mint_url, proofs) = proofs.into_iter().next().expect("Token has proofs"); - - if self.mint_url != mint_url { + if self.mint_url != token_data.mint_url()? { return Err(Error::IncorrectMint); } From bd6f9b3263c6e779445984a818240d7c2b0a8a6d Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 10:12:11 +0100 Subject: [PATCH 10/16] chore: changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78408d9a0..2a8ff7e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,9 @@ - cdk: Bump `lightning-invoice` to `0.32.2` ([prusnak]). - cdk: Bump `lightning` to `0.0.124` ([prusnak]). - cdk: `PaymentMethod` as a `non_exhaustive` enum ([thesimplekid]). -- cdk: `CurrencyUnit` as a `non_exhaustive` enum ([thesimpekid]). +- cdk: `CurrencyUnit` as a `non_exhaustive` enum ([thesimplekid]). +- cdk: Enforce token is single mint ([thesimplekid]). + ### Added - cdk: Added description to `MintQuoteBolt11Request` ([lollerfirst]). From 4a6b2b20cedb97195eeba0cec14b7cfa667ed78e Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 10:30:02 +0100 Subject: [PATCH 11/16] chore: changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8ff7e49..35e308bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,13 @@ - cdk: `PaymentMethod` as a `non_exhaustive` enum ([thesimplekid]). - cdk: `CurrencyUnit` as a `non_exhaustive` enum ([thesimplekid]). - cdk: Enforce token is single mint ([thesimplekid]). +- cdk: Mint will return change for over paid melt even over fee reserve ([davidcaseria]). +- cdk: Refactor ln_backeds to be on the `cdk::Mint` and not with axum ([thesimplekid]). +- cdk: Change is returned in the check quote response ([thesimplekid]). +- cdk: Move unit conversion util fn to amount module ([davidcaseria]). +- cdk: Remove spent proofs from db when check state is called ([mubarak23]). +- cdk: Use `MintUrl` directly in wallet client ([ok300]). +- cdk-cli: Change cdk-cli pay command to melt ([mubarak23]). ### Added @@ -36,10 +43,21 @@ - cdk: Add `amount` and `fee_paid` to `Melted` ([davidcaseria]). - cdk: Add `from_proofs` on `Melted` ([davidcaseria]). - cdk: Add unit on `PaymentResponse` ([thesimplekid]). +- cdk: Add description for mint quote ([lollerfirst]). +- cdk-axum: Add cache to some endpoints ([lollerfirst]). +- cdk: Add Proofs trait ([ok300]). +- cdk: Wallet verifiys keyset id when first fetching keys ([thesimplekid]). +- cdk-mind: Add swagger docs ([ok300]). ### Removed - cdk: Remove `MintMeltSettings` since it is no longer used ([lollerfirst]). - cdk: `PaymentMethod::Custom` ([thesimplekid]). +- cdk: Remove deprecated `MeltBolt11Response` ([thesimplekid]). + +### Fixed +- cdk: Check of inputs to include fee ([thesimplekid]). +- cdk: Make unit mandatory in tokenv4 ([ok300]). + @@ -176,4 +194,5 @@ Additionally, this release introduces a Mint binary cdk-mintd that uses the cdk- [ok300]: https://github.com/ok300 [lollerfirst]: https://github.com/lollerfirst [prusnak]: https://github.com/prusnak +[mubarak23]: https://github.com/mubarak23 From 3cd5f463d70accd41f269bb15327dad36a5680eb Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Tue, 22 Oct 2024 11:15:01 +0100 Subject: [PATCH 12/16] feat(NUT18): Payment request --- crates/cdk/src/error.rs | 3 + crates/cdk/src/nuts/mod.rs | 1 + crates/cdk/src/nuts/nut18.rs | 163 +++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 crates/cdk/src/nuts/nut18.rs diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 5ca14821b..1695a58c7 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -234,6 +234,9 @@ pub enum Error { /// NUT14 Error #[error(transparent)] NUT14(#[from] crate::nuts::nut14::Error), + /// NUT18 Error + #[error(transparent)] + NUT18(#[from] crate::nuts::nut18::Error), /// Database Error #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index c710ac030..12d317df9 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -18,6 +18,7 @@ pub mod nut12; pub mod nut13; pub mod nut14; pub mod nut15; +pub mod nut18; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, diff --git a/crates/cdk/src/nuts/nut18.rs b/crates/cdk/src/nuts/nut18.rs new file mode 100644 index 000000000..2c574d153 --- /dev/null +++ b/crates/cdk/src/nuts/nut18.rs @@ -0,0 +1,163 @@ +//! NUT-18: Payment Requests +//! +//! + +use std::{fmt, str::FromStr}; + +use bitcoin::base64::{ + alphabet, + engine::{general_purpose, GeneralPurpose}, + Engine, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{mint_url::MintUrl, Amount}; + +use super::CurrencyUnit; + +const PAYMENT_REQUEST_PREFIX: &str = "creqA"; + +/// NUT18 Error +#[derive(Debug, Error)] +pub enum Error { + /// Invalid Prefix + #[error("Invalid Prefix")] + InvalidPrefix, + /// Ciborium error + #[error(transparent)] + CiboriumError(#[from] ciborium::de::Error), + /// Base64 error + #[error(transparent)] + Base64Error(#[from] bitcoin::base64::DecodeError), +} + +/// Transport +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Transport { + /// Type + #[serde(rename = "t")] + pub _type: String, + /// Target + #[serde(rename = "a")] + pub target: String, + /// Tags + #[serde(rename = "g")] + pub tags: Option>>, +} + +/// Payment Request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentRequest { + /// `Payment id` + #[serde(rename = "i")] + pub payment_id: Option, + /// Amount + #[serde(rename = "a")] + pub amount: Option, + /// Unit + #[serde(rename = "u")] + pub unit: Option, + /// Single use + #[serde(rename = "s")] + pub single_use: Option, + /// Mints + #[serde(rename = "m")] + pub mints: Option>, + /// Description + #[serde(rename = "d")] + pub description: Option, + /// Transport + #[serde(rename = "t")] + pub transports: Vec, +} + +impl fmt::Display for PaymentRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use serde::ser::Error; + let mut data = Vec::new(); + ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?; + let encoded = general_purpose::URL_SAFE.encode(data); + write!(f, "{}{}", PAYMENT_REQUEST_PREFIX, encoded) + } +} + +impl FromStr for PaymentRequest { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s + .strip_prefix(PAYMENT_REQUEST_PREFIX) + .ok_or(Error::InvalidPrefix)?; + + let decode_config = general_purpose::GeneralPurposeConfig::new() + .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent); + let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?; + + Ok(ciborium::from_reader(&decoded[..])?) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U="; + + #[test] + fn test_decode_payment_req() -> anyhow::Result<()> { + let req = PaymentRequest::from_str(PAYMENT_REQUEST)?; + + assert_eq!(&req.payment_id.unwrap(), "b7a90176"); + assert_eq!(req.amount.unwrap(), 10.into()); + assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + assert_eq!( + req.mints.unwrap(), + vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?] + ); + assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + + let transport = req.transports.first().unwrap(); + + let expected_transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; + + assert_eq!(transport, &expected_transport); + + Ok(()) + } + + #[test] + fn test_roundtrip_payment_req() -> anyhow::Result<()> { + let transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; + + let request = PaymentRequest { + payment_id: Some("b7a90176".to_string()), + amount: Some(10.into()), + unit: Some(CurrencyUnit::Sat), + single_use: None, + mints: Some(vec!["https://nofees.testnut.cashu.space".parse()?]), + description: None, + transports: vec![transport.clone()], + }; + + let request_str = request.to_string(); + + let req = PaymentRequest::from_str(&request_str)?; + + assert_eq!(&req.payment_id.unwrap(), "b7a90176"); + assert_eq!(req.amount.unwrap(), 10.into()); + assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + assert_eq!( + req.mints.unwrap(), + vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?] + ); + assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); + + let t = req.transports.first().unwrap(); + assert_eq!(&transport, t); + + Ok(()) + } +} From c4abafb6174abaa97fd9a73e64c9937a2e4ab3b6 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Tue, 22 Oct 2024 14:04:42 +0100 Subject: [PATCH 13/16] feat(cdk-cli): decode, send, receice payment request --- crates/cdk-cli/Cargo.toml | 11 +- crates/cdk-cli/src/main.rs | 15 ++ .../src/sub_commands/create_request.rs | 104 ++++++++++ .../src/sub_commands/decode_request.rs | 19 ++ crates/cdk-cli/src/sub_commands/mod.rs | 3 + .../cdk-cli/src/sub_commands/pay_request.rs | 177 ++++++++++++++++++ crates/cdk-cli/src/sub_commands/receive.rs | 17 +- crates/cdk/src/nuts/mod.rs | 1 + crates/cdk/src/nuts/nut18.rs | 54 +++++- 9 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 crates/cdk-cli/src/sub_commands/create_request.rs create mode 100644 crates/cdk-cli/src/sub_commands/decode_request.rs create mode 100644 crates/cdk-cli/src/sub_commands/pay_request.rs diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index bd3b26b0f..45f0b1133 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -25,8 +25,15 @@ tracing = { version = "0.1", default-features = false, features = ["attributes", tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } rand = "0.8.5" home = "0.5.5" -nostr-sdk = { version = "0.33.0", default-features = false, features = [ +nostr-sdk = { version = "0.35.0", default-features = false, features = [ "nip04", - "nip44" + "nip44", + "nip59" +]} +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", + "rustls-tls-native-roots", + "socks", ]} url = "2.3" diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index c452b9523..7ff70cda0 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -72,6 +72,12 @@ enum Commands { UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand), /// Get proofs from mint. ListMintProofs, + /// Decode a payment request + DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand), + /// Pay a payment request + PayRequest(sub_commands::pay_request::PayRequestSubCommand), + /// Create Payment request + CreateRequest(sub_commands::create_request::CreateRequestSubCommand), } #[tokio::main] @@ -204,5 +210,14 @@ async fn main() -> Result<()> { Commands::ListMintProofs => { sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await } + Commands::DecodeRequest(sub_command_args) => { + sub_commands::decode_request::decode_payment_request(sub_command_args) + } + Commands::PayRequest(sub_command_args) => { + sub_commands::pay_request::pay_request(&multi_mint_wallet, sub_command_args).await + } + Commands::CreateRequest(sub_command_args) => { + sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await + } } } diff --git a/crates/cdk-cli/src/sub_commands/create_request.rs b/crates/cdk-cli/src/sub_commands/create_request.rs new file mode 100644 index 000000000..ca7392b2d --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/create_request.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use cdk::{ + nuts::{ + nut18::TransportType, CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport, + }, + wallet::MultiMintWallet, +}; +use clap::Args; +use nostr_sdk::prelude::*; +use nostr_sdk::{nips::nip19::Nip19Profile, Client as NostrClient, Filter, Keys, ToBech32}; + +#[derive(Args)] +pub struct CreateRequestSubCommand { + #[arg(short, long)] + amount: Option, + /// Currency unit e.g. sat + #[arg(default_value = "sat")] + unit: String, + /// Quote description + description: Option, +} + +pub async fn create_request( + multi_mint_wallet: &MultiMintWallet, + sub_command_args: &CreateRequestSubCommand, +) -> Result<()> { + let keys = Keys::generate(); + let relays = vec!["wss://relay.nos.social", "wss://relay.damus.io"]; + + let nprofile = Nip19Profile::new(keys.public_key, relays.clone())?; + + let nostr_transport = Transport { + _type: TransportType::Nostr, + target: nprofile.to_bech32()?, + tags: Some(vec![vec!["n".to_string(), "17".to_string()]]), + }; + + let mints: Vec = multi_mint_wallet + .get_balances(&CurrencyUnit::Sat) + .await? + .keys() + .cloned() + .collect(); + + let req = PaymentRequest { + payment_id: None, + amount: sub_command_args.amount.map(|a| a.into()), + unit: None, + single_use: Some(true), + mints: Some(mints), + description: sub_command_args.description.clone(), + transports: vec![nostr_transport], + }; + + println!("{}", req); + + let client = NostrClient::new(keys); + + let filter = Filter::new().pubkey(nprofile.public_key); + + for relay in relays { + client.add_read_relay(relay).await?; + } + + client.connect().await; + + client.subscribe(vec![filter], None).await?; + + // Handle subscription notifications with `handle_notifications` method + client + .handle_notifications(|notification| async { + let mut exit = false; + if let RelayPoolNotification::Event { + subscription_id: _, + event, + .. + } = notification + { + let unwrapped = client.unwrap_gift_wrap(&event).await?; + + let rumor = unwrapped.rumor; + + let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?; + + let token = Token::new( + payload.mint, + payload.proofs, + payload.memo, + Some(payload.unit), + ); + + let amount = multi_mint_wallet + .receive(&token.to_string(), &[], &[]) + .await?; + + println!("Received {}", amount); + exit = true; + } + Ok(exit) // Set to true to exit from the loop + }) + .await?; + + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/decode_request.rs b/crates/cdk-cli/src/sub_commands/decode_request.rs new file mode 100644 index 000000000..3a0f1c88f --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/decode_request.rs @@ -0,0 +1,19 @@ +use std::str::FromStr; + +use anyhow::Result; +use cdk::nuts::PaymentRequest; +use cdk::util::serialize_to_cbor_diag; +use clap::Args; + +#[derive(Args)] +pub struct DecodePaymentRequestSubCommand { + /// Payment request + payment_request: String, +} + +pub fn decode_payment_request(sub_command_args: &DecodePaymentRequestSubCommand) -> Result<()> { + let payment_request = PaymentRequest::from_str(&sub_command_args.payment_request)?; + + println!("{:}", serialize_to_cbor_diag(&payment_request)?); + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index eee3cf327..8256d0aea 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -1,11 +1,14 @@ pub mod balance; pub mod burn; pub mod check_spent; +pub mod create_request; +pub mod decode_request; pub mod decode_token; pub mod list_mint_proofs; pub mod melt; pub mod mint; pub mod mint_info; +pub mod pay_request; pub mod pending_mints; pub mod receive; pub mod restore; diff --git a/crates/cdk-cli/src/sub_commands/pay_request.rs b/crates/cdk-cli/src/sub_commands/pay_request.rs new file mode 100644 index 000000000..fc840361d --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/pay_request.rs @@ -0,0 +1,177 @@ +use std::io::{self, Write}; + +use anyhow::{anyhow, Result}; +use cdk::{ + amount::SplitTarget, + nuts::{nut18::TransportType, PaymentRequest, PaymentRequestPayload}, + wallet::{MultiMintWallet, SendKind}, +}; +use clap::Args; +use nostr_sdk::{nips::nip19::Nip19Profile, Client as NostrClient, EventBuilder, FromBech32, Keys}; +use reqwest::Client; + +#[derive(Args)] +pub struct PayRequestSubCommand { + payment_request: PaymentRequest, +} + +pub async fn pay_request( + multi_mint_wallet: &MultiMintWallet, + sub_command_args: &PayRequestSubCommand, +) -> Result<()> { + let payment_request = &sub_command_args.payment_request; + + let unit = payment_request.unit; + + let amount = match payment_request.amount { + Some(amount) => amount, + None => { + println!("Enter the amount you would like to pay"); + + let mut user_input = String::new(); + let stdin = io::stdin(); + io::stdout().flush().unwrap(); + stdin.read_line(&mut user_input)?; + + let amount: u64 = user_input.trim().parse()?; + + amount.into() + } + }; + + let request_mints = &payment_request.mints; + + let wallet_mints = multi_mint_wallet.get_wallets().await; + + // Wallets where unit, balance and mint match request + let mut matching_wallets = vec![]; + + for wallet in wallet_mints.iter() { + let balance = wallet.total_balance().await?; + + if let Some(request_mints) = request_mints { + if !request_mints.contains(&wallet.mint_url) { + continue; + } + } + + if let Some(unit) = unit { + if wallet.unit != unit { + continue; + } + } + + if balance >= amount { + matching_wallets.push(wallet); + } + } + + let matching_wallet = matching_wallets.first().unwrap(); + + // We prefer nostr transport if it is available to hide ip. + let transport = payment_request + .transports + .iter() + .find(|t| t._type == TransportType::Nostr) + .or_else(|| { + payment_request + .transports + .iter() + .find(|t| t._type == TransportType::HttpPost) + }) + .ok_or(anyhow!("No supported transport method found"))?; + + let proofs = matching_wallet + .send( + amount, + None, + None, + &SplitTarget::default(), + &SendKind::default(), + true, + ) + .await? + .proofs() + .get(&matching_wallet.mint_url) + .unwrap() + .clone(); + + let payload = PaymentRequestPayload { + id: payment_request.payment_id.clone(), + memo: None, + mint: matching_wallet.mint_url.clone(), + unit: matching_wallet.unit, + proofs, + }; + + match transport._type { + TransportType::Nostr => { + let keys = Keys::generate(); + let client = NostrClient::new(keys); + let nprofile = Nip19Profile::from_bech32(&transport.target)?; + + println!("{:?}", nprofile.relays); + + let rumor = EventBuilder::new( + nostr_sdk::Kind::from_u16(14), + serde_json::to_string(&payload)?, + [], + ); + + let relays = nprofile.relays; + + for relay in relays.iter() { + client.add_write_relay(relay).await?; + } + + client.connect().await; + + let gift_wrap = client + .gift_wrap_to(relays, &nprofile.public_key, rumor, None) + .await?; + + println!( + "Published event {} succufully to {}", + gift_wrap.val, + gift_wrap + .success + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + ); + + if !gift_wrap.failed.is_empty() { + println!( + "Could not publish to {:?}", + gift_wrap + .failed + .keys() + .map(|relay| relay.to_string()) + .collect::>() + .join(", ") + ); + } + } + + TransportType::HttpPost => { + let client = Client::new(); + + let res = client + .post(transport.target.clone()) + .json(&payload) + .send() + .await?; + + let status = res.status(); + if status.is_success() { + println!("Successfully posted payment"); + } else { + println!("{:?}", res); + println!("Error posting payment"); + } + } + } + + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/receive.rs b/crates/cdk-cli/src/sub_commands/receive.rs index f63bed667..f0d197381 100644 --- a/crates/cdk-cli/src/sub_commands/receive.rs +++ b/crates/cdk-cli/src/sub_commands/receive.rs @@ -184,20 +184,25 @@ async fn nostr_receive( let client = nostr_sdk::Client::default(); - client.add_relays(relays).await?; - client.connect().await; - let events = client.get_events_of(vec![filter], None).await?; + let events = client + .get_events_of( + vec![filter], + nostr_sdk::EventSource::Relays { + timeout: None, + specific_relays: Some(relays), + }, + ) + .await?; let mut tokens: HashSet = HashSet::new(); let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?; for event in events { - if event.kind() == Kind::EncryptedDirectMessage { - if let Ok(msg) = nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content()) - { + if event.kind == Kind::EncryptedDirectMessage { + if let Ok(msg) = nip04::decrypt(keys.secret_key(), &event.pubkey, event.content) { if let Some(token) = cdk::wallet::util::token_from_text(&msg) { tokens.insert(token.to_string()); } diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index 12d317df9..07518bff1 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -47,3 +47,4 @@ pub use nut11::{Conditions, P2PKWitness, SigFlag, SpendingConditions}; pub use nut12::{BlindSignatureDleq, ProofDleq}; pub use nut14::HTLCWitness; pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings}; +pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport}; diff --git a/crates/cdk/src/nuts/nut18.rs b/crates/cdk/src/nuts/nut18.rs index 2c574d153..d165b1484 100644 --- a/crates/cdk/src/nuts/nut18.rs +++ b/crates/cdk/src/nuts/nut18.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::{mint_url::MintUrl, Amount}; -use super::CurrencyUnit; +use super::{CurrencyUnit, Proofs}; const PAYMENT_REQUEST_PREFIX: &str = "creqA"; @@ -32,12 +32,39 @@ pub enum Error { Base64Error(#[from] bitcoin::base64::DecodeError), } +/// Transport Type +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum TransportType { + /// Nostr + #[serde(rename = "nostr")] + Nostr, + /// Http post + #[serde(rename = "post")] + HttpPost, +} + +impl fmt::Display for TransportType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use serde::ser::Error; + let t = serde_json::to_string(self).map_err(|e| fmt::Error::custom(e.to_string()))?; + write!(f, "{}", t) + } +} + +impl FromStr for Transport { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + /// Transport -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct Transport { /// Type #[serde(rename = "t")] - pub _type: String, + pub _type: TransportType, /// Target #[serde(rename = "a")] pub target: String, @@ -47,7 +74,7 @@ pub struct Transport { } /// Payment Request -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct PaymentRequest { /// `Payment id` #[serde(rename = "i")] @@ -98,6 +125,21 @@ impl FromStr for PaymentRequest { } } +/// Payment Request +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaymentRequestPayload { + /// Id + pub id: Option, + /// Memo + pub memo: Option, + /// Mint + pub mint: MintUrl, + /// Unit + pub unit: CurrencyUnit, + /// Proofs + pub proofs: Proofs, +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -121,7 +163,7 @@ mod tests { let transport = req.transports.first().unwrap(); - let expected_transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; + let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; assert_eq!(transport, &expected_transport); @@ -130,7 +172,7 @@ mod tests { #[test] fn test_roundtrip_payment_req() -> anyhow::Result<()> { - let transport = Transport {_type: "nostr".to_string(), target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; + let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; let request = PaymentRequest { payment_id: Some("b7a90176".to_string()), From 4e52609d62de0d0ddd851cb0bf4c14449f4283ca Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 13:26:21 +0100 Subject: [PATCH 14/16] chore: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e308bf7..f3477baf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - cdk: Add Proofs trait ([ok300]). - cdk: Wallet verifiys keyset id when first fetching keys ([thesimplekid]). - cdk-mind: Add swagger docs ([ok300]). +- cdk: NUT18 payment request support ([thesimplekid]). ### Removed - cdk: Remove `MintMeltSettings` since it is no longer used ([lollerfirst]). From 58e7226cff3834fc19c4f5518c458ab322a370d1 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 13:41:40 +0100 Subject: [PATCH 15/16] fix: rebase issues --- crates/cdk-cli/src/sub_commands/create_request.rs | 7 +------ crates/cdk-cli/src/sub_commands/pay_request.rs | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/cdk-cli/src/sub_commands/create_request.rs b/crates/cdk-cli/src/sub_commands/create_request.rs index ca7392b2d..de362be5d 100644 --- a/crates/cdk-cli/src/sub_commands/create_request.rs +++ b/crates/cdk-cli/src/sub_commands/create_request.rs @@ -82,12 +82,7 @@ pub async fn create_request( let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?; - let token = Token::new( - payload.mint, - payload.proofs, - payload.memo, - Some(payload.unit), - ); + let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit); let amount = multi_mint_wallet .receive(&token.to_string(), &[], &[]) diff --git a/crates/cdk-cli/src/sub_commands/pay_request.rs b/crates/cdk-cli/src/sub_commands/pay_request.rs index fc840361d..1498f9804 100644 --- a/crates/cdk-cli/src/sub_commands/pay_request.rs +++ b/crates/cdk-cli/src/sub_commands/pay_request.rs @@ -91,10 +91,7 @@ pub async fn pay_request( true, ) .await? - .proofs() - .get(&matching_wallet.mint_url) - .unwrap() - .clone(); + .proofs(); let payload = PaymentRequestPayload { id: payment_request.payment_id.clone(), From 09b5a55239787aa623ec6b210a7296fd8b4d8a05 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:24:51 +0200 Subject: [PATCH 16/16] Keyset ID: fix deserialization edge-case, add unit tests --- crates/cdk/Cargo.toml | 2 +- crates/cdk/src/nuts/nut02.rs | 118 +++++++++++++++++------------------ 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index b72e50d23..e89311a53 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -35,7 +35,7 @@ reqwest = { version = "0.12", default-features = false, features = [ ], optional = true } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1" -serde_with = "3.1" +serde_with = "3" tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } thiserror = "1" futures = { version = "0.3.28", default-features = false, optional = true } diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index 32e961909..0de24c984 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -18,7 +18,7 @@ use bitcoin::hashes::Hash; use bitcoin::key::Secp256k1; #[cfg(feature = "mint")] use bitcoin::secp256k1; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use serde_with::{serde_as, VecSkipError}; use thiserror::Error; @@ -86,10 +86,11 @@ impl fmt::Display for KeySetVersion { /// A keyset ID is an identifier for a specific keyset. It can be derived by /// anyone who knows the set of public keys of a mint. The keyset ID **CAN** -/// be stored in a Cashu token such that the token can be used to identify +/// be stored in a Cashu token such that the token can be used to identify /// which mint or keyset it was generated from. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = String))] pub struct Id { version: KeySetVersion, id: [u8; Self::BYTELEN], @@ -130,17 +131,16 @@ impl fmt::Display for Id { } } -impl FromStr for Id { - type Err = Error; +impl TryFrom for Id { + type Error = Error; - fn from_str(s: &str) -> Result { - // Check if the string length is valid + fn try_from(s: String) -> Result { if s.len() != 16 { return Err(Error::Length); } Ok(Self { - version: KeySetVersion::Version00, + version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?, id: hex::decode(&s[2..])? .try_into() .map_err(|_| Error::Length)?, @@ -148,63 +148,29 @@ impl FromStr for Id { } } -impl Serialize for Id { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) +impl FromStr for Id { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s.to_string()) } } -impl<'de> Deserialize<'de> for Id { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct IdVisitor; - - impl<'de> serde::de::Visitor<'de> for IdVisitor { - type Value = Id; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("Expecting a 14 char hex string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - Id::from_str(v).map_err(|e| match e { - Error::Length => E::custom(format!( - "Invalid Length: Expected {}, got {}: - {}", - Id::STRLEN, - v.len(), - v - )), - _ => E::custom(e), - }) - } - } - - deserializer.deserialize_str(IdVisitor) +impl From for String { + fn from(value: Id) -> Self { + value.to_string() } } impl From<&Keys> for Id { + /// As per NUT-02: + /// 1. sort public keys by their amount in ascending order + /// 2. concatenate all public keys to one string + /// 3. HASH_SHA256 the concatenated public keys + /// 4. take the first 14 characters of the hex-encoded hash + /// 5. prefix it with a keyset ID version byte fn from(map: &Keys) -> Self { - // REVIEW: Is it 16 or 14 bytes - /* NUT-02 - 1 - sort public keys by their amount in ascending order - 2 - concatenate all public keys to one string - 3 - HASH_SHA256 the concatenated public keys - 4 - take the first 14 characters of the hex-encoded hash - 5 - prefix it with a keyset ID version byte - */ - let mut keys: Vec<(&AmountStr, &super::PublicKey)> = map.iter().collect(); - keys.sort_by_key(|(amt, _v)| *amt); let pubkeys_concat: Vec = keys @@ -400,12 +366,14 @@ impl From<&MintKeys> for Id { #[cfg(test)] mod test { - use std::str::FromStr; + use rand::RngCore; + use super::{KeySetInfo, Keys, KeysetResponse}; - use crate::nuts::nut02::Id; + use crate::nuts::nut02::{Error, Id}; use crate::nuts::KeysResponse; + use crate::util::hex; const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46"; const SHORT_KEYSET: &str = r#" @@ -547,4 +515,36 @@ mod test { assert_eq!(keys_response.keysets.len(), 2); } + + fn generate_random_id() -> Id { + let mut rand_bytes = vec![0u8; 8]; + rand::thread_rng().fill_bytes(&mut rand_bytes[1..]); + Id::from_bytes(&rand_bytes) + .unwrap_or_else(|e| panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))) + } + + #[test] + fn test_id_serialization() { + let id = generate_random_id(); + let id_str = id.to_string(); + + assert!(id_str.chars().all(|c| c.is_ascii_hexdigit())); + assert_eq!(16, id_str.len()); + assert_eq!(id_str.to_lowercase(), id_str); + } + + #[test] + fn test_id_deserialization() { + let id_from_short_str = Id::from_str("00123"); + assert!(matches!(id_from_short_str, Err(Error::Length))); + + let id_from_non_hex_str = Id::from_str(&SHORT_KEYSET_ID.replace('a', "x")); + assert!(matches!(id_from_non_hex_str, Err(Error::HexError(_)))); + + let id_invalid_version = Id::from_str(&SHORT_KEYSET_ID.replace("00", "99")); + assert!(matches!(id_invalid_version, Err(Error::UnknownVersion))); + + let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase()); + assert!(id_from_uppercase.is_ok()); + } }