diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index f30035842..05145f150 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -64,6 +64,8 @@ enum Commands { MintInfo(sub_commands::mint_info::MintInfoSubcommand), /// Mint proofs via bolt11 Mint(sub_commands::mint::MintSubCommand), + /// Remint + ReMint(sub_commands::remint_bolt12::ReMintSubCommand), /// Burn Spent tokens Burn(sub_commands::burn::BurnSubCommand), /// Restore proofs from seed @@ -199,5 +201,14 @@ async fn main() -> Result<()> { sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args) .await } + Commands::ReMint(sub_command_args) => { + sub_commands::remint_bolt12::remint( + &multi_mint_wallet, + &mnemonic.to_seed_normalized(""), + localstore, + sub_command_args, + ) + .await + } } } diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index cb79db557..923f9ff0d 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -7,6 +7,7 @@ pub mod mint; pub mod mint_info; pub mod pending_mints; pub mod receive; +pub mod remint_bolt12; pub mod restore; pub mod send; pub mod update_mint_url; diff --git a/crates/cdk-cli/src/sub_commands/remint_bolt12.rs b/crates/cdk-cli/src/sub_commands/remint_bolt12.rs new file mode 100644 index 000000000..4d63c9f49 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/remint_bolt12.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use anyhow::Result; +use cdk::amount::SplitTarget; +use cdk::cdk_database::{Error, WalletDatabase}; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::multi_mint_wallet::WalletKey; +use cdk::wallet::{MultiMintWallet, Wallet}; +use clap::Args; +use serde::{Deserialize, Serialize}; + +#[derive(Args, Serialize, Deserialize)] +pub struct ReMintSubCommand { + /// Mint url + mint_url: MintUrl, + #[arg(long)] + quote_id: String, +} + +pub async fn remint( + multi_mint_wallet: &MultiMintWallet, + seed: &[u8], + localstore: Arc + Sync + Send>, + sub_command_args: &ReMintSubCommand, +) -> Result<()> { + let mint_url = sub_command_args.mint_url.clone(); + let quote_id = sub_command_args.quote_id.clone(); + + let wallet = match multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat)) + .await + { + Some(wallet) => wallet.clone(), + None => { + let wallet = Wallet::new( + &mint_url.to_string(), + CurrencyUnit::Sat, + localstore, + seed, + None, + )?; + + multi_mint_wallet.add_wallet(wallet.clone()).await; + wallet + } + }; + + let receive_amount = wallet.mint("e_id, SplitTarget::default(), None).await?; + + println!("Received {receive_amount} from mint {mint_url}"); + + Ok(()) +} diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 3e9f718a2..040c465a7 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -105,7 +105,7 @@ impl MintLightning for Cln { // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let last_pay_index = self.get_last_pay_index().await?; let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?; @@ -162,6 +162,11 @@ impl MintLightning for Cln { let payment_hash = wait_any_response.payment_hash.to_string(); + + // TODO: Handle unit conversion + let amount_msats = wait_any_response.amount_received_msat.expect("status is paid there should be an amount"); + let amount_sats = amount_msats.msat() / 1000; + let request_look_up = match wait_any_response.bolt12 { // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. // Since this is not returned in the wait any response, @@ -192,7 +197,7 @@ impl MintLightning for Cln { None => payment_hash, }; - break Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active))); + break Some(((request_look_up, amount_sats.into()), (cln_client, last_pay_idx, cancel_token, is_active))); } Err(e) => { tracing::warn!("Error fetching invoice: {e}"); @@ -597,7 +602,7 @@ impl MintLightning for Cln { let label = Uuid::new_v4().to_string(); - let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; + let _amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; let cln_response = cln_client .call(cln_rpc::Request::Offer(OfferRequest { @@ -611,8 +616,8 @@ impl MintLightning for Cln { recurrence_limit: None, recurrence_paywindow: None, recurrence_start_any_period: None, - single_use: Some(true), - amount: amount.to_string(), + single_use: Some(false), + amount: "any".to_string(), })) .await .map_err(Error::from)?; diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index cd3fbad2d..fede3d8b5 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -122,11 +122,11 @@ impl MintLightning for FakeWallet { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; let receiver_stream = ReceiverStream::new(receiver); self.wait_invoice_is_active.store(true, Ordering::SeqCst); - Ok(Box::pin(receiver_stream.map(|label| label))) + Ok(Box::pin(receiver_stream.map(|label| (label, Amount::ZERO)))) } async fn get_payment_quote( diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index c86e2dd31..fa800acd5 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -72,11 +72,14 @@ async fn mint_proofs( amount, unix_time() + 36000, request_lookup.to_string(), + Amount::ZERO, + Amount::ZERO, ); mint.localstore.add_mint_quote(quote.clone()).await?; - mint.pay_mint_quote_for_request_id(&request_lookup).await?; + mint.pay_mint_quote_for_request_id(&request_lookup, amount) + .await?; let keyset_id = Id::from(&keys); let premint = PreMintSecrets::random(keyset_id, amount, split_target)?; diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 9711ebdaa..c81fdd5bb 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -92,7 +92,7 @@ impl MintLightning for LNbits { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -129,7 +129,7 @@ impl MintLightning for LNbits { match check { Ok(state) => { if state { - Some((msg, (receiver, lnbits_api, cancel_token, is_active))) + Some(((msg, Amount::ZERO), (receiver, lnbits_api, cancel_token, is_active))) } else { None } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 2890ee785..65788cce8 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -99,7 +99,7 @@ impl MintLightning for Lnd { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let mut client = fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file) .await @@ -141,7 +141,7 @@ impl MintLightning for Lnd { match msg { Ok(Some(msg)) => { if msg.state == 1 { - Some((hex::encode(msg.r_hash), (stream, cancel_token, is_active))) + Some(((hex::encode(msg.r_hash), Amount::ZERO), (stream, cancel_token, is_active))) } else { None } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index b836bb1bb..714b17206 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -148,6 +148,19 @@ async fn main() -> anyhow::Result<()> { CurrencyUnit::Sat, PaymentMethod::Bolt11, mint_melt_limits, + cln.clone(), + ); + + let ln_key = LnKey { + unit: CurrencyUnit::Sat, + method: PaymentMethod::Bolt12, + }; + ln_backends.insert(ln_key, cln.clone()); + + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt12, + mint_melt_limits, cln, ) } diff --git a/crates/cdk-mintd/src/mint.rs b/crates/cdk-mintd/src/mint.rs index 542f380fd..d4eb1ad05 100644 --- a/crates/cdk-mintd/src/mint.rs +++ b/crates/cdk-mintd/src/mint.rs @@ -183,6 +183,11 @@ impl MintBuilder { ln.insert(ln_key, ln_backend); + let mut supported_units = self.supported_units.clone(); + + supported_units.insert(ln_key.unit, (0, 32)); + self.supported_units = supported_units; + self.ln = Some(ln); self diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 205514758..fe999d661 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -101,7 +101,7 @@ impl MintLightning for Phoenixd { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -136,7 +136,7 @@ impl MintLightning for Phoenixd { Ok(state) => { if state.is_paid { // Yield the payment hash and continue the stream - Some((msg.payment_hash, (receiver, phoenixd_api, cancel_token, is_active))) + Some(((msg.payment_hash, Amount::ZERO), (receiver, phoenixd_api, cancel_token, is_active))) } else { // Invoice not paid yet, continue waiting // We need to continue the stream, so we return the same state diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index a6a6f7603..b3f8f5672 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -173,6 +173,9 @@ impl From for MintQuote { state: quote.state, expiry: quote.expiry, request_lookup_id: Bolt11Invoice::from_str("e.request).unwrap().to_string(), + // TODO: Create real migrations + amount_paid: Amount::ZERO, + amount_issued: Amount::ZERO, } } } diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index ee4b1910c..02961be0b 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -1298,6 +1298,9 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, expiry: row_expiry as u64, request_lookup_id, + // TODO: Get these values + amount_paid: Amount::ZERO, + amount_issued: Amount::ZERO, }) } diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 77d9b7f46..8b8e9a12d 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -89,7 +89,7 @@ impl MintLightning for Strike { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { self.strike_api .subscribe_to_invoice_webhook(self.webhook_url.clone()) .await?; @@ -129,7 +129,7 @@ impl MintLightning for Strike { match check { Ok(state) => { if state.state == InvoiceState::Paid { - Some((msg, (receiver, strike_api, cancel_token, is_active))) + Some(((msg, Amount::ZERO), (receiver, strike_api, cancel_token, is_active))) } else { None } diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 2120670bb..7ace61327 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -88,7 +88,7 @@ pub trait MintLightning { /// Returns a stream of request_lookup_id once invoices are paid async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err>; + ) -> Result + Send>>, Self::Err>; /// Is wait invoice active fn is_wait_invoice_active(&self) -> bool; diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index 1c1390f70..3ad5de648 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -109,6 +109,8 @@ impl Mint { amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + Amount::ZERO, + Amount::ZERO, ); tracing::debug!( diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index f33fb6813..334ed313e 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -102,6 +102,8 @@ impl Mint { amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + Amount::ZERO, + Amount::ZERO, ); tracing::debug!( @@ -142,6 +144,8 @@ impl Mint { paid: Some(paid), state, expiry: Some(quote.expiry), + amount_paid: quote.amount_paid, + amount_issued: quote.amount_issued, }) } @@ -194,6 +198,7 @@ impl Mint { pub async fn pay_mint_quote_for_request_id( &self, request_lookup_id: &str, + amount: Amount, ) -> Result<(), Error> { if let Ok(Some(mint_quote)) = self .localstore @@ -208,6 +213,35 @@ impl Mint { self.localstore .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) .await?; + let quote = self + .localstore + .get_mint_quote(&mint_quote.id) + .await? + .unwrap(); + + let amount_paid = quote.amount_paid + amount; + + let quote = MintQuote { + id: quote.id, + mint_url: quote.mint_url, + amount: quote.amount, + unit: quote.unit, + request: quote.request, + state: MintQuoteState::Paid, + expiry: quote.expiry, + request_lookup_id: quote.request_lookup_id, + amount_paid, + amount_issued: quote.amount_issued, + }; + + tracing::debug!( + "Quote: {}, Amount paid: {}, amount issued: {}", + quote.id, + amount_paid, + quote.amount_issued + ); + + self.localstore.add_mint_quote(quote).await?; } Ok(()) } @@ -234,6 +268,11 @@ impl Mint { .localstore .update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending) .await?; + let quote = self + .localstore + .get_mint_quote(&mint_request.quote) + .await? + .unwrap(); match state { MintQuoteState::Unpaid => { @@ -243,11 +282,21 @@ impl Mint { return Err(Error::PendingQuote); } MintQuoteState::Issued => { - return Err(Error::IssuedQuote); + if quote.amount_issued >= quote.amount_paid { + return Err(Error::IssuedQuote); + } } MintQuoteState::Paid => (), } + let amount_can_issue = quote.amount_paid - quote.amount_issued; + + let messages_amount = mint_request.total_amount().unwrap(); + + if amount_can_issue < messages_amount { + return Err(Error::IssuedQuote); + } + let blinded_messages: Vec = mint_request .outputs .iter() @@ -299,6 +348,21 @@ impl Mint { .update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued) .await?; + let mint_quote = MintQuote { + id: quote.id, + mint_url: quote.mint_url, + amount: quote.amount, + unit: quote.unit, + request: quote.request, + state: MintQuoteState::Issued, + expiry: quote.expiry, + amount_paid: quote.amount_paid, + amount_issued: quote.amount_issued + messages_amount, + request_lookup_id: quote.request_lookup_id, + }; + + self.localstore.add_mint_quote(mint_quote).await?; + Ok(nut04::MintBolt11Response { signatures: blind_signatures, }) diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 7ae784a54..033633e6d 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -210,7 +210,7 @@ impl Mint { match result { Ok(mut stream) => { while let Some(request_lookup_id) = stream.next().await { - if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await { + if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id.0, request_lookup_id.1).await { tracing::warn!("{:?}", err); } } diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 53c3b5a19..72d980e8f 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -29,6 +29,10 @@ pub struct MintQuote { pub expiry: u64, /// Value used by ln backend to look up state of request pub request_lookup_id: String, + /// Amount paid + pub amount_paid: Amount, + /// Amount issued + pub amount_issued: Amount, } impl MintQuote { @@ -40,6 +44,8 @@ impl MintQuote { amount: Amount, expiry: u64, request_lookup_id: String, + amount_paid: Amount, + amount_issued: Amount, ) -> Self { let id = Uuid::new_v4(); @@ -52,6 +58,8 @@ impl MintQuote { state: MintQuoteState::Unpaid, expiry, request_lookup_id, + amount_paid, + amount_issued, } } } diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index fd60a57ac..8180354d3 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -92,6 +92,10 @@ pub struct MintQuoteBolt11Response { pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, + /// Amount that has been paid + pub amount_paid: Amount, + /// Amount that has been issued + pub amount_issued: Amount, } // A custom deserializer is needed until all mints @@ -149,12 +153,24 @@ impl<'de> Deserialize<'de> for MintQuoteBolt11Response { .ok_or(serde::de::Error::missing_field("expiry"))? .as_u64(); + let amount_paid = value + .get("amount_paid") + .ok_or(serde::de::Error::missing_field("expiry"))? + .as_u64(); + + let amount_issued = value + .get("amount_issued") + .ok_or(serde::de::Error::missing_field("expiry"))? + .as_u64(); + Ok(Self { quote, request, paid: Some(paid), state, expiry, + amount_paid: amount_paid.unwrap_or_default().into(), + amount_issued: amount_issued.unwrap_or_default().into(), }) } } @@ -169,6 +185,8 @@ impl From for MintQuoteBolt11Response { paid: Some(paid), state: mint_quote.state, expiry: Some(mint_quote.expiry), + amount_paid: mint_quote.amount_paid, + amount_issued: mint_quote.amount_issued, } } } diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 3311886ca..33a8a0c82 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -244,10 +244,25 @@ impl Wallet { let count = count.map_or(0, |c| c + 1); + let status = self.mint_quote_state(quote_id).await?; + + println!( + "Amount paid: {}, Amount issued: {}", + status.amount_paid, status.amount_issued + ); + + let amount = status.amount_paid - status.amount_issued; + + let amount = if amount == Amount::ZERO { + quote_info.amount + } else { + amount + }; + let premint_secrets = match &spending_conditions { Some(spending_conditions) => PreMintSecrets::with_conditions( active_keyset_id, - quote_info.amount, + amount, &amount_split_target, spending_conditions, )?, @@ -255,7 +270,7 @@ impl Wallet { active_keyset_id, count, self.xpriv, - quote_info.amount, + amount, &amount_split_target, )?, }; @@ -293,7 +308,7 @@ impl Wallet { let minted_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?; // Remove filled quote from store - self.localstore.remove_mint_quote("e_info.id).await?; + //self.localstore.remove_mint_quote("e_info.id).await?; if spending_conditions.is_none() { // Update counter for keyset