diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 27b66676..e179c7ef 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -157,9 +157,6 @@ pub enum Error { /// Receive can only be used with tokens from single mint #[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")] MultiMintTokenNotSupported, - /// Unit Not supported - #[error("Unit not supported for method")] - UnitUnsupported, /// Preimage not provided #[error("Preimage not provided")] PreimageNotProvided, @@ -233,6 +230,9 @@ pub enum Error { /// NUT03 error #[error(transparent)] NUT03(#[from] crate::nuts::nut03::Error), + /// NUT04 error + #[error(transparent)] + NUT04(#[from] crate::nuts::nut04::Error), /// NUT05 error #[error(transparent)] NUT05(#[from] crate::nuts::nut05::Error), @@ -329,7 +329,7 @@ impl From for ErrorResponse { detail: None, }, Error::UnsupportedUnit => ErrorResponse { - code: ErrorCode::UnitUnsupported, + code: ErrorCode::UnsupportedUnit, error: Some(err.to_string()), detail: None, }, @@ -412,7 +412,7 @@ impl From for Error { ErrorCode::KeysetNotFound => Self::UnknownKeySet, ErrorCode::KeysetInactive => Self::InactiveKeyset, ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned, - ErrorCode::UnitUnsupported => Self::UnitUnsupported, + ErrorCode::UnsupportedUnit => Self::UnsupportedUnit, ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0), ErrorCode::MintingDisabled => Self::MintingDisabled, ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid, @@ -449,7 +449,7 @@ pub enum ErrorCode { /// Blinded Message Already signed BlindedMessageAlreadySigned, /// Unsupported unit - UnitUnsupported, + UnsupportedUnit, /// Token already issed for quote TokensAlreadyIssued, /// Minting Disabled @@ -478,7 +478,7 @@ impl ErrorCode { 10003 => Self::TokenNotVerified, 11001 => Self::TokenAlreadySpent, 11002 => Self::TransactionUnbalanced, - 11005 => Self::UnitUnsupported, + 11005 => Self::UnsupportedUnit, 11006 => Self::AmountOutofLimitRange, 11007 => Self::TokenPending, 12001 => Self::KeysetNotFound, @@ -502,7 +502,7 @@ impl ErrorCode { Self::TokenNotVerified => 10003, Self::TokenAlreadySpent => 11001, Self::TransactionUnbalanced => 11002, - Self::UnitUnsupported => 11005, + Self::UnsupportedUnit => 11005, Self::AmountOutofLimitRange => 11006, Self::TokenPending => 11007, Self::KeysetNotFound => 12001, diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index 831ff079..46a55216 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -46,6 +46,20 @@ where Arc::new(fake_wallet), ); + let fee_reserve = FeeReserve { + min_fee_reserve: 1.into(), + percent_fee_reserve: 1.0, + }; + + let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0); + + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Usd, + PaymentMethod::Bolt11, + MintMeltLimits::new(1, 5_000), + Arc::new(fake_wallet), + ); + let mnemonic = Mnemonic::generate(12)?; mint_builder = mint_builder diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index e3c1fa4b..983ad341 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -476,3 +476,125 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> { Ok(_) => bail!("Minting should not have succeed without a witness"), } } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_inflated() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_quote = wallet.mint_quote(100.into(), None).await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + + let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?; + + let quote_info = wallet + .localstore + .get_mint_quote(&mint_quote.id) + .await? + .expect("there is a quote"); + + let mut mint_request = MintBolt11Request { + quote: mint_quote.id, + outputs: pre_mint.blinded_messages(), + signature: None, + }; + + if let Some(secret_key) = quote_info.secret_key { + mint_request.sign(secret_key)?; + } + let http_client = HttpClient::new(MINT_URL.parse()?); + + let response = http_client.post_mint(mint_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::TransactionUnbalanced(_, _, _) => (), + err => { + bail!("Wrong mint error returned: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed second payment"); + } + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_multiple_units() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_quote = wallet.mint_quote(100.into(), None).await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + + let pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None)?; + + let wallet_usd = Wallet::new( + MINT_URL, + CurrencyUnit::Usd, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let active_keyset_id = wallet_usd.get_active_mint_keyset().await?.id; + + let usd_pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None)?; + + let quote_info = wallet + .localstore + .get_mint_quote(&mint_quote.id) + .await? + .expect("there is a quote"); + + let mut sat_outputs = pre_mint.blinded_messages(); + + let mut usd_outputs = usd_pre_mint.blinded_messages(); + + sat_outputs.append(&mut usd_outputs); + + let mut mint_request = MintBolt11Request { + quote: mint_quote.id, + outputs: sat_outputs, + signature: None, + }; + + if let Some(secret_key) = quote_info.secret_key { + mint_request.sign(secret_key)?; + } + let http_client = HttpClient::new(MINT_URL.parse()?); + + let response = http_client.post_mint(mint_request.clone()).await; + + match response { + Err(err) => match err { + cdk::Error::UnsupportedUnit => (), + err => { + bail!("Wrong mint error returned: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed to mint with multiple units"); + } + } + + Ok(()) +} diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 89ba6d4a..e6eb86e9 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -41,7 +41,7 @@ impl Mint { let settings = nut05 .get_settings(&unit, &method) - .ok_or(Error::UnitUnsupported)?; + .ok_or(Error::UnsupportedUnit)?; if matches!(options, Some(MeltOptions::Mpp { mpp: _ })) { // Verify there is no corresponding mint quote. @@ -103,7 +103,7 @@ impl Mint { .ok_or_else(|| { tracing::info!("Could not get ln backend for {}, bolt11 ", unit); - Error::UnitUnsupported + Error::UnsupportedUnit })?; let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| { @@ -113,7 +113,7 @@ impl Mint { err ); - Error::UnitUnsupported + Error::UnsupportedUnit })?; // We only want to set the msats_to_pay of the melt quote if the invoice is amountless @@ -224,7 +224,7 @@ impl Mint { Some( to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit) - .map_err(|_| Error::UnitUnsupported)?, + .map_err(|_| Error::UnsupportedUnit)?, ) } false => None, @@ -233,7 +233,7 @@ impl Mint { let amount_to_pay = match partial_amount { Some(amount_to_pay) => amount_to_pay, None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit) - .map_err(|_| Error::UnitUnsupported)?, + .map_err(|_| Error::UnsupportedUnit)?, }; let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| { @@ -502,7 +502,7 @@ impl Mint { tracing::error!("Could not reset melt quote state: {}", err); } - return Err(Error::UnitUnsupported); + return Err(Error::UnsupportedUnit); } }; diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index ab2ef58a..717e8181 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -1,3 +1,6 @@ +use std::collections::HashSet; + +use cdk_common::Id; use tracing::instrument; use uuid::Uuid; @@ -49,7 +52,7 @@ impl Mint { } } None => { - return Err(Error::UnitUnsupported); + return Err(Error::UnsupportedUnit); } } @@ -77,7 +80,7 @@ impl Mint { .ok_or_else(|| { tracing::info!("Bolt11 mint request for unsupported unit"); - Error::UnitUnsupported + Error::UnsupportedUnit })?; let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl; @@ -292,6 +295,35 @@ impl Mint { mint_request.verify_signature(pubkey)?; } + // We check the the total value of blinded messages == mint quote + if mint_request.total_amount()? != mint_quote.amount { + return Err(Error::TransactionUnbalanced( + mint_quote.amount.into(), + mint_request.total_amount()?.into(), + 0, + )); + } + + let keyset_ids: HashSet = mint_request.outputs.iter().map(|b| b.keyset_id).collect(); + + let mut keyset_units = HashSet::new(); + + for keyset_id in keyset_ids { + let keyset = self.keyset(&keyset_id).await?.ok_or(Error::UnknownKeySet)?; + + keyset_units.insert(keyset.unit); + } + + if keyset_units.len() != 1 { + tracing::debug!("Client attempted to mint with outputs of multiple units"); + return Err(Error::UnsupportedUnit); + } + + if keyset_units.iter().next().expect("Checked len above") != &mint_quote.unit { + tracing::debug!("Client attempted to mint with unit not in quote"); + return Err(Error::UnsupportedUnit); + } + let blinded_messages: Vec = mint_request .outputs .iter() @@ -315,8 +347,7 @@ impl Mint { self.localstore .update_mint_quote_state(&mint_request.quote, MintQuoteState::Paid) - .await - .unwrap(); + .await?; return Err(Error::BlindedMessageAlreadySigned); } diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 92d06161..1baddf34 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -195,7 +195,7 @@ impl Wallet { let unit = token.unit().unwrap_or_default(); if unit != self.unit { - return Err(Error::UnitUnsupported); + return Err(Error::UnsupportedUnit); } let proofs = token.proofs();