Explorar el Código

refactor: mint into mods

thesimplekid hace 5 meses
padre
commit
187664439c

+ 62 - 0
crates/cdk/src/mint/check_spendable.rs

@@ -0,0 +1,62 @@
+use std::collections::HashSet;
+
+use tracing::instrument;
+
+use crate::Error;
+
+use super::{CheckStateRequest, CheckStateResponse, Mint, ProofState, PublicKey, State};
+
+impl Mint {
+    /// Check state
+    #[instrument(skip_all)]
+    pub async fn check_state(
+        &self,
+        check_state: &CheckStateRequest,
+    ) -> Result<CheckStateResponse, Error> {
+        let states = self.localstore.get_proofs_states(&check_state.ys).await?;
+
+        let states = states
+            .iter()
+            .zip(&check_state.ys)
+            .map(|(state, y)| {
+                let state = match state {
+                    Some(state) => *state,
+                    None => State::Unspent,
+                };
+
+                ProofState {
+                    y: *y,
+                    state,
+                    witness: None,
+                }
+            })
+            .collect();
+
+        Ok(CheckStateResponse { states })
+    }
+
+    /// Check Tokens are not spent or pending
+    #[instrument(skip_all)]
+    pub async fn check_ys_spendable(
+        &self,
+        ys: &[PublicKey],
+        proof_state: State,
+    ) -> Result<(), Error> {
+        let proofs_state = self
+            .localstore
+            .update_proofs_states(ys, proof_state)
+            .await?;
+
+        let proofs_state = proofs_state.iter().flatten().collect::<HashSet<&State>>();
+
+        if proofs_state.contains(&State::Pending) {
+            return Err(Error::TokenPending);
+        }
+
+        if proofs_state.contains(&State::Spent) {
+            return Err(Error::TokenAlreadySpent);
+        }
+
+        Ok(())
+    }
+}

+ 31 - 0
crates/cdk/src/mint/info.rs

@@ -0,0 +1,31 @@
+use tracing::instrument;
+
+use crate::mint_url::MintUrl;
+
+use super::{Mint, MintInfo};
+
+impl Mint {
+    /// Set Mint Url
+    #[instrument(skip_all)]
+    pub fn set_mint_url(&mut self, mint_url: MintUrl) {
+        self.mint_url = mint_url;
+    }
+
+    /// Get Mint Url
+    #[instrument(skip_all)]
+    pub fn get_mint_url(&self) -> &MintUrl {
+        &self.mint_url
+    }
+
+    /// Set Mint Info
+    #[instrument(skip_all)]
+    pub fn set_mint_info(&mut self, mint_info: MintInfo) {
+        self.mint_info = mint_info;
+    }
+
+    /// Get Mint Info
+    #[instrument(skip_all)]
+    pub fn mint_info(&self) -> &MintInfo {
+        &self.mint_info
+    }
+}

+ 144 - 0
crates/cdk/src/mint/keysets.rs

@@ -0,0 +1,144 @@
+use std::collections::HashSet;
+
+use tracing::instrument;
+
+use crate::Error;
+
+use super::{
+    create_new_keyset, derivation_path_from_unit, CurrencyUnit, Id, KeySet, KeySetInfo,
+    KeysResponse, KeysetResponse, Mint, MintKeySet, MintKeySetInfo,
+};
+
+impl Mint {
+    /// Retrieve the public keys of the active keyset for distribution to wallet
+    /// clients
+    #[instrument(skip(self))]
+    pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
+        self.ensure_keyset_loaded(keyset_id).await?;
+        let keysets = self.keysets.read().await;
+        let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?.clone();
+        Ok(KeysResponse {
+            keysets: vec![keyset.into()],
+        })
+    }
+
+    /// Retrieve the public keys of the active keyset for distribution to wallet
+    /// clients
+    #[instrument(skip_all)]
+    pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
+        let active_keysets = self.localstore.get_active_keysets().await?;
+
+        let active_keysets: HashSet<&Id> = active_keysets.values().collect();
+
+        for id in active_keysets.iter() {
+            self.ensure_keyset_loaded(id).await?;
+        }
+
+        let keysets = self.keysets.read().await;
+        Ok(KeysResponse {
+            keysets: keysets
+                .values()
+                .filter_map(|k| match active_keysets.contains(&k.id) {
+                    true => Some(k.clone().into()),
+                    false => None,
+                })
+                .collect(),
+        })
+    }
+
+    /// Return a list of all supported keysets
+    #[instrument(skip_all)]
+    pub async fn keysets(&self) -> Result<KeysetResponse, Error> {
+        let keysets = self.localstore.get_keyset_infos().await?;
+        let active_keysets: HashSet<Id> = self
+            .localstore
+            .get_active_keysets()
+            .await?
+            .values()
+            .cloned()
+            .collect();
+
+        let keysets = keysets
+            .into_iter()
+            .map(|k| KeySetInfo {
+                id: k.id,
+                unit: k.unit,
+                active: active_keysets.contains(&k.id),
+                input_fee_ppk: k.input_fee_ppk,
+            })
+            .collect();
+
+        Ok(KeysetResponse { keysets })
+    }
+
+    /// Get keysets
+    #[instrument(skip(self))]
+    pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
+        self.ensure_keyset_loaded(id).await?;
+        let keysets = self.keysets.read().await;
+        let keyset = keysets.get(id).map(|k| k.clone().into());
+        Ok(keyset)
+    }
+
+    /// Add current keyset to inactive keysets
+    /// Generate new keyset
+    #[instrument(skip(self))]
+    pub async fn rotate_keyset(
+        &self,
+        unit: CurrencyUnit,
+        derivation_path_index: u32,
+        max_order: u8,
+        input_fee_ppk: u64,
+    ) -> Result<(), Error> {
+        let derivation_path = derivation_path_from_unit(unit, derivation_path_index);
+        let (keyset, keyset_info) = create_new_keyset(
+            &self.secp_ctx,
+            self.xpriv,
+            derivation_path,
+            Some(derivation_path_index),
+            unit,
+            max_order,
+            input_fee_ppk,
+        );
+        let id = keyset_info.id;
+        self.localstore.add_keyset_info(keyset_info).await?;
+        self.localstore.set_active_keyset(unit, id).await?;
+
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, keyset);
+
+        Ok(())
+    }
+
+    /// Ensure Keyset is loaded in mint
+    #[instrument(skip(self))]
+    pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> {
+        let keysets = self.keysets.read().await;
+        if keysets.contains_key(id) {
+            return Ok(());
+        }
+        drop(keysets);
+
+        let keyset_info = self
+            .localstore
+            .get_keyset_info(id)
+            .await?
+            .ok_or(Error::UnknownKeySet)?;
+        let id = keyset_info.id;
+        let mut keysets = self.keysets.write().await;
+        keysets.insert(id, self.generate_keyset(keyset_info));
+        Ok(())
+    }
+
+    /// Generate [`MintKeySet`] from [`MintKeySetInfo`]
+    #[instrument(skip_all)]
+    pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
+        MintKeySet::generate_from_xpriv(
+            &self.secp_ctx,
+            self.xpriv,
+            keyset_info.max_order,
+            keyset_info.unit,
+            keyset_info.derivation_path,
+        )
+    }
+}

+ 708 - 0
crates/cdk/src/mint/melt.rs

@@ -0,0 +1,708 @@
+use std::collections::HashSet;
+use std::str::FromStr;
+
+use anyhow::bail;
+use lightning_invoice::Bolt11Invoice;
+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::nut11::enforce_sig_flag;
+use crate::nuts::nut11::EnforceSigFlag;
+use crate::{
+    cdk_lightning::to_unit, mint::SigFlag, nuts::Id, nuts::MeltQuoteState, types::LnKey,
+    util::unix_time, Amount, Error,
+};
+
+use super::nut05::MeltBolt11Response;
+use super::{
+    CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    Mint, PaymentMethod, PublicKey, State,
+};
+
+impl Mint {
+    fn check_melt_request_acceptable(
+        &self,
+        amount: Amount,
+        unit: CurrencyUnit,
+        method: PaymentMethod,
+    ) -> Result<(), Error> {
+        let nut05 = &self.mint_info.nuts.nut05;
+
+        if nut05.disabled {
+            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);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Get melt bolt11 quote
+    #[instrument(skip_all)]
+    pub async fn get_melt_bolt11_quote(
+        &self,
+        melt_request: &MeltQuoteBolt11Request,
+    ) -> Result<MeltQuoteBolt11Response, Error> {
+        let MeltQuoteBolt11Request {
+            request,
+            unit,
+            options: _,
+        } = melt_request;
+
+        let amount = match melt_request.options {
+            Some(mpp_amount) => mpp_amount.amount,
+            None => {
+                let amount_msat = request
+                    .amount_milli_satoshis()
+                    .ok_or(Error::InvoiceAmountUndefined)?;
+
+                to_unit(amount_msat, &CurrencyUnit::Msat, unit)
+                    .map_err(|_err| Error::UnsupportedUnit)?
+            }
+        };
+
+        self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt11)?;
+
+        let ln = self
+            .ln
+            .get(&LnKey::new(*unit, PaymentMethod::Bolt11))
+            .ok_or_else(|| {
+                tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
+
+                Error::UnitUnsupported
+            })?;
+
+        let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| {
+            tracing::error!(
+                "Could not get payment quote for mint quote, {} bolt11, {}",
+                unit,
+                err
+            );
+
+            Error::UnitUnsupported
+        })?;
+
+        let quote = MeltQuote::new(
+            request.to_string(),
+            *unit,
+            payment_quote.amount,
+            payment_quote.fee,
+            unix_time() + self.quote_ttl.melt_ttl,
+            payment_quote.request_lookup_id.clone(),
+        );
+
+        tracing::debug!(
+            "New melt quote {} for {} {} with request id {}",
+            quote.id,
+            amount,
+            unit,
+            payment_quote.request_lookup_id
+        );
+
+        self.localstore.add_melt_quote(quote.clone()).await?;
+
+        Ok(quote.into())
+    }
+
+    /// Check melt quote status
+    #[instrument(skip(self))]
+    pub async fn check_melt_quote(&self, quote_id: &str) -> Result<MeltQuoteBolt11Response, Error> {
+        let quote = self
+            .localstore
+            .get_melt_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        let blind_signatures = self
+            .localstore
+            .get_blind_signatures_for_quote(quote_id)
+            .await?;
+
+        let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
+
+        Ok(MeltQuoteBolt11Response {
+            quote: quote.id,
+            paid: Some(quote.state == MeltQuoteState::Paid),
+            state: quote.state,
+            expiry: quote.expiry,
+            amount: quote.amount,
+            fee_reserve: quote.fee_reserve,
+            payment_preimage: quote.payment_preimage,
+            change,
+        })
+    }
+
+    /// Update melt quote
+    #[instrument(skip_all)]
+    pub async fn update_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
+        self.localstore.add_melt_quote(quote).await?;
+        Ok(())
+    }
+
+    /// Get melt quotes
+    #[instrument(skip_all)]
+    pub async fn melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> {
+        let quotes = self.localstore.get_melt_quotes().await?;
+        Ok(quotes)
+    }
+
+    /// Remove melt quote
+    #[instrument(skip(self))]
+    pub async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
+        self.localstore.remove_melt_quote(quote_id).await?;
+
+        Ok(())
+    }
+
+    /// Check melt has expected fees
+    #[instrument(skip_all)]
+    pub async fn check_melt_expected_ln_fees(
+        &self,
+        melt_quote: &MeltQuote,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<Option<Amount>, Error> {
+        let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
+
+        let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
+            .expect("Quote unit is checked above that it can convert to msat");
+
+        let invoice_amount_msats: Amount = invoice
+            .amount_milli_satoshis()
+            .ok_or(Error::InvoiceAmountUndefined)?
+            .into();
+
+        let partial_amount = match invoice_amount_msats > quote_msats {
+            true => {
+                let partial_msats = invoice_amount_msats - quote_msats;
+
+                Some(
+                    to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
+                        .map_err(|_| Error::UnitUnsupported)?,
+                )
+            }
+            false => None,
+        };
+
+        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)?,
+        };
+
+        let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
+            tracing::error!("Proof inputs in melt quote overflowed");
+            Error::AmountOverflow
+        })?;
+
+        if amount_to_pay + melt_quote.fee_reserve > inputs_amount_quote_unit {
+            tracing::debug!(
+                "Not enough inputs provided: {} msats needed {} msats",
+                inputs_amount_quote_unit,
+                amount_to_pay
+            );
+
+            return Err(Error::TransactionUnbalanced(
+                inputs_amount_quote_unit.into(),
+                amount_to_pay.into(),
+                melt_quote.fee_reserve.into(),
+            ));
+        }
+
+        Ok(partial_amount)
+    }
+
+    /// Verify melt request is valid
+    #[instrument(skip_all)]
+    pub async fn verify_melt_request(
+        &self,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<MeltQuote, Error> {
+        let state = self
+            .localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Pending)
+            .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);
+            }
+        }
+
+        let ys = melt_request
+            .inputs
+            .iter()
+            .map(|p| hash_to_curve(&p.secret.to_bytes()))
+            .collect::<Result<Vec<PublicKey>, _>>()?;
+
+        // Ensure proofs are unique and not being double spent
+        if melt_request.inputs.len() != ys.iter().collect::<HashSet<_>>().len() {
+            return Err(Error::DuplicateProofs);
+        }
+
+        self.localstore
+            .add_proofs(
+                melt_request.inputs.clone(),
+                Some(melt_request.quote.clone()),
+            )
+            .await?;
+        self.check_ys_spendable(&ys, State::Pending).await?;
+
+        for proof in &melt_request.inputs {
+            self.verify_proof(proof).await?;
+        }
+
+        let quote = self
+            .localstore
+            .get_melt_quote(&melt_request.quote)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        let proofs_total = melt_request.proofs_amount()?;
+
+        let fee = self.get_proofs_fee(&melt_request.inputs).await?;
+
+        let required_total = quote.amount + quote.fee_reserve + fee;
+
+        // Check that the inputs proofs are greater then total.
+        // Transaction does not need to be balanced as wallet may not want change.
+        if proofs_total < required_total {
+            tracing::info!(
+                "Swap request unbalanced: {}, outputs {}, fee {}",
+                proofs_total,
+                quote.amount,
+                fee
+            );
+            return Err(Error::TransactionUnbalanced(
+                proofs_total.into(),
+                quote.amount.into(),
+                (fee + quote.fee_reserve).into(),
+            ));
+        }
+
+        let input_keyset_ids: HashSet<Id> =
+            melt_request.inputs.iter().map(|p| p.keyset_id).collect();
+
+        let mut keyset_units = HashSet::with_capacity(input_keyset_ids.capacity());
+
+        for id in input_keyset_ids {
+            let keyset = self
+                .localstore
+                .get_keyset_info(&id)
+                .await?
+                .ok_or(Error::UnknownKeySet)?;
+            keyset_units.insert(keyset.unit);
+        }
+
+        let EnforceSigFlag { sig_flag, .. } = enforce_sig_flag(melt_request.inputs.clone());
+
+        if sig_flag.eq(&SigFlag::SigAll) {
+            return Err(Error::SigAllUsedInMelt);
+        }
+
+        if let Some(outputs) = &melt_request.outputs {
+            let output_keysets_ids: HashSet<Id> = outputs.iter().map(|b| b.keyset_id).collect();
+            for id in output_keysets_ids {
+                let keyset = self
+                    .localstore
+                    .get_keyset_info(&id)
+                    .await?
+                    .ok_or(Error::UnknownKeySet)?;
+
+                // Get the active keyset for the unit
+                let active_keyset_id = self
+                    .localstore
+                    .get_active_keyset_id(&keyset.unit)
+                    .await?
+                    .ok_or(Error::InactiveKeyset)?;
+
+                // Check output is for current active keyset
+                if id.ne(&active_keyset_id) {
+                    return Err(Error::InactiveKeyset);
+                }
+                keyset_units.insert(keyset.unit);
+            }
+        }
+
+        // Check that all input and output proofs are the same unit
+        if keyset_units.len().gt(&1) {
+            return Err(Error::MultipleUnits);
+        }
+
+        tracing::debug!("Verified melt quote: {}", melt_request.quote);
+        Ok(quote)
+    }
+
+    /// Process unpaid melt request
+    /// In the event that a melt request fails and the lighthing payment is not
+    /// made The [`Proofs`] should be returned to an unspent state and the
+    /// 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::<Result<Vec<PublicKey>, _>>()?;
+
+        self.localstore
+            .update_proofs_states(&input_ys, State::Unspent)
+            .await?;
+
+        self.localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Unpaid)
+            .await?;
+
+        Ok(())
+    }
+
+    /// Melt Bolt11
+    #[instrument(skip_all)]
+    pub async fn melt_bolt11(
+        &self,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<MeltBolt11Response, Error> {
+        use std::sync::Arc;
+        async fn check_payment_state(
+            ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+            melt_quote: &MeltQuote,
+        ) -> anyhow::Result<PayInvoiceResponse> {
+            match ln
+                .check_outgoing_payment(&melt_quote.request_lookup_id)
+                .await
+            {
+                Ok(response) => Ok(response),
+                Err(check_err) => {
+                    // If we cannot check the status of the payment we keep the proofs stuck as pending.
+                    tracing::error!(
+                        "Could not check the status of payment for {},. Proofs stuck as pending",
+                        melt_quote.id
+                    );
+                    tracing::error!("Checking payment error: {}", check_err);
+                    bail!("Could not check payment status")
+                }
+            }
+        }
+
+        let quote = match self.verify_melt_request(melt_request).await {
+            Ok(quote) => quote,
+            Err(err) => {
+                tracing::debug!("Error attempting to verify melt quote: {}", err);
+
+                if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                    tracing::error!(
+                        "Could not reset melt quote {} state: {}",
+                        melt_request.quote,
+                        err
+                    );
+                }
+                return Err(err);
+            }
+        };
+
+        let settled_internally_amount =
+            match self.handle_internal_melt_mint(&quote, melt_request).await {
+                Ok(amount) => amount,
+                Err(err) => {
+                    tracing::error!("Attempting to settle internally failed");
+                    if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                        tracing::error!(
+                            "Could not reset melt quote {} state: {}",
+                            melt_request.quote,
+                            err
+                        );
+                    }
+                    return Err(err);
+                }
+            };
+
+        let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
+            Some(amount_spent) => (None, amount_spent),
+            None => {
+                // If the quote unit is SAT or MSAT we can check that the expected fees are
+                // provided. We also check if the quote is less then the invoice
+                // amount in the case that it is a mmp However, if the quote is not
+                // of a bitcoin unit we cannot do these checks as the mint
+                // is unaware of a conversion rate. In this case it is assumed that the quote is
+                // correct and the mint should pay the full invoice amount if inputs
+                // > `then quote.amount` are included. This is checked in the
+                // `verify_melt` method.
+                let partial_amount = match quote.unit {
+                    CurrencyUnit::Sat | CurrencyUnit::Msat => {
+                        match self.check_melt_expected_ln_fees(&quote, melt_request).await {
+                            Ok(amount) => amount,
+                            Err(err) => {
+                                tracing::error!("Fee is not expected: {}", err);
+                                if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                                    tracing::error!("Could not reset melt quote state: {}", err);
+                                }
+                                return Err(Error::Internal);
+                            }
+                        }
+                    }
+                    _ => None,
+                };
+                let ln = match self.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
+                    Some(ln) => ln,
+                    None => {
+                        tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
+                        if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                            tracing::error!("Could not reset melt quote state: {}", err);
+                        }
+
+                        return Err(Error::UnitUnsupported);
+                    }
+                };
+
+                let pre = match ln
+                    .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
+                    .await
+                {
+                    Ok(pay)
+                        if pay.status == MeltQuoteState::Unknown
+                            || pay.status == MeltQuoteState::Failed =>
+                    {
+                        let check_response = check_payment_state(Arc::clone(ln), &quote)
+                            .await
+                            .map_err(|_| Error::Internal)?;
+
+                        if check_response.status == MeltQuoteState::Paid {
+                            tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
+
+                            return Err(Error::Internal);
+                        }
+
+                        check_response
+                    }
+                    Ok(pay) => pay,
+                    Err(err) => {
+                        // If the error is that the invoice was already paid we do not want to hold
+                        // hold the proofs as pending to we reset them  and return an error.
+                        if matches!(err, cdk_lightning::Error::InvoiceAlreadyPaid) {
+                            tracing::debug!("Invoice already paid, resetting melt quote");
+                            if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                                tracing::error!("Could not reset melt quote state: {}", err);
+                            }
+                            return Err(Error::RequestAlreadyPaid);
+                        }
+
+                        tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
+
+                        let check_response = check_payment_state(Arc::clone(ln), &quote)
+                            .await
+                            .map_err(|_| Error::Internal)?;
+                        // If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
+                        if check_response.status == MeltQuoteState::Paid {
+                            tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
+
+                            return Err(Error::Internal);
+                        }
+                        check_response
+                    }
+                };
+
+                match pre.status {
+                    MeltQuoteState::Paid => (),
+                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
+                        tracing::info!(
+                            "Lightning payment for quote {} failed.",
+                            melt_request.quote
+                        );
+                        if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                            tracing::error!("Could not reset melt quote state: {}", err);
+                        }
+                        return Err(Error::PaymentFailed);
+                    }
+                    MeltQuoteState::Pending => {
+                        tracing::warn!(
+                            "LN payment pending, proofs are stuck as pending for quote: {}",
+                            melt_request.quote
+                        );
+                        return Err(Error::PendingQuote);
+                    }
+                }
+
+                // Convert from unit of backend to quote unit
+                // Note: this should never fail since these conversions happen earlier and would fail there.
+                // Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
+                let amount_spent =
+                    to_unit(pre.total_spent, &pre.unit, &quote.unit).unwrap_or_default();
+
+                let payment_lookup_id = pre.payment_lookup_id;
+
+                if payment_lookup_id != quote.request_lookup_id {
+                    tracing::info!(
+                        "Payment lookup id changed post payment from {} to {}",
+                        quote.request_lookup_id,
+                        payment_lookup_id
+                    );
+
+                    let mut melt_quote = quote;
+                    melt_quote.request_lookup_id = payment_lookup_id;
+
+                    if let Err(err) = self.localstore.add_melt_quote(melt_quote).await {
+                        tracing::warn!("Could not update payment lookup id: {}", err);
+                    }
+                }
+
+                (pre.payment_preimage, amount_spent)
+            }
+        };
+
+        // If we made it here the payment has been made.
+        // We process the melt burning the inputs and returning change
+        let res = self
+            .process_melt_request(melt_request, preimage, amount_spent_quote_unit)
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not process melt request: {}", err);
+                err
+            })?;
+
+        Ok(res.into())
+    }
+
+    /// Process melt request marking [`Proofs`] as spent
+    /// The melt request must be verifyed using [`Self::verify_melt_request`]
+    /// before calling [`Self::process_melt_request`]
+    #[instrument(skip_all)]
+    pub async fn process_melt_request(
+        &self,
+        melt_request: &MeltBolt11Request,
+        payment_preimage: Option<String>,
+        total_spent: Amount,
+    ) -> Result<MeltQuoteBolt11Response, Error> {
+        tracing::debug!("Processing melt quote: {}", melt_request.quote);
+
+        let quote = self
+            .localstore
+            .get_melt_quote(&melt_request.quote)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        let input_ys = melt_request
+            .inputs
+            .iter()
+            .map(|p| hash_to_curve(&p.secret.to_bytes()))
+            .collect::<Result<Vec<PublicKey>, _>>()?;
+
+        self.localstore
+            .update_proofs_states(&input_ys, State::Spent)
+            .await?;
+
+        self.localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Paid)
+            .await?;
+
+        let mut change = None;
+
+        // Check if there is change to return
+        if melt_request.proofs_amount()? > total_spent {
+            // Check if wallet provided change outputs
+            if let Some(outputs) = melt_request.outputs.clone() {
+                let blinded_messages: Vec<PublicKey> =
+                    outputs.iter().map(|b| b.blinded_secret).collect();
+
+                if self
+                    .localstore
+                    .get_blind_signatures(&blinded_messages)
+                    .await?
+                    .iter()
+                    .flatten()
+                    .next()
+                    .is_some()
+                {
+                    tracing::info!("Output has already been signed");
+
+                    return Err(Error::BlindedMessageAlreadySigned);
+                }
+
+                let change_target = melt_request.proofs_amount()? - total_spent;
+                let mut amounts = change_target.split();
+                let mut change_sigs = Vec::with_capacity(amounts.len());
+
+                if outputs.len().lt(&amounts.len()) {
+                    tracing::debug!(
+                        "Providing change requires {} blinded messages, but only {} provided",
+                        amounts.len(),
+                        outputs.len()
+                    );
+
+                    // In the case that not enough outputs are provided to return all change
+                    // Reverse sort the amounts so that the most amount of change possible is
+                    // returned. The rest is burnt
+                    amounts.sort_by(|a, b| b.cmp(a));
+                }
+
+                let mut outputs = outputs;
+
+                for (amount, blinded_message) in amounts.iter().zip(&mut outputs) {
+                    blinded_message.amount = *amount;
+
+                    let blinded_signature = self.blind_sign(blinded_message).await?;
+                    change_sigs.push(blinded_signature)
+                }
+
+                self.localstore
+                    .add_blind_signatures(
+                        &outputs[0..change_sigs.len()]
+                            .iter()
+                            .map(|o| o.blinded_secret)
+                            .collect::<Vec<PublicKey>>(),
+                        &change_sigs,
+                        Some(quote.id.clone()),
+                    )
+                    .await?;
+
+                change = Some(change_sigs);
+            }
+        }
+
+        Ok(MeltQuoteBolt11Response {
+            amount: quote.amount,
+            paid: Some(true),
+            payment_preimage,
+            change,
+            quote: quote.id,
+            fee_reserve: quote.fee_reserve,
+            state: MeltQuoteState::Paid,
+            expiry: quote.expiry,
+        })
+    }
+}

+ 306 - 0
crates/cdk/src/mint/mint_nut04.rs

@@ -0,0 +1,306 @@
+use tracing::instrument;
+
+use crate::{nuts::MintQuoteState, types::LnKey, util::unix_time, Amount, Error};
+
+use super::{
+    nut04, CurrencyUnit, Mint, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response,
+    PaymentMethod, PublicKey,
+};
+
+impl Mint {
+    /// Checks that minting is enabled, request is supported unit and within range
+    fn check_mint_request_acceptable(
+        &self,
+        amount: Amount,
+        unit: CurrencyUnit,
+    ) -> Result<(), Error> {
+        let nut04 = &self.mint_info.nuts.nut04;
+
+        if nut04.disabled {
+            return Err(Error::MintingDisabled);
+        }
+
+        match nut04.get_settings(&unit, &PaymentMethod::Bolt11) {
+            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);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Create new mint bolt11 quote
+    #[instrument(skip_all)]
+    pub async fn get_mint_bolt11_quote(
+        &self,
+        mint_quote_request: MintQuoteBolt11Request,
+    ) -> Result<MintQuoteBolt11Response, Error> {
+        let MintQuoteBolt11Request {
+            amount,
+            unit,
+            description,
+        } = mint_quote_request;
+
+        self.check_mint_request_acceptable(amount, unit)?;
+
+        let ln = self
+            .ln
+            .get(&LnKey::new(unit, PaymentMethod::Bolt11))
+            .ok_or_else(|| {
+                tracing::info!("Bolt11 mint request for unsupported unit");
+
+                Error::UnitUnsupported
+            })?;
+
+        let quote_expiry = unix_time() + self.quote_ttl.mint_ttl;
+
+        if description.is_some() && !ln.get_settings().invoice_description {
+            tracing::error!("Backend does not support invoice description");
+            return Err(Error::InvoiceDescriptionUnsupported);
+        }
+
+        let create_invoice_response = ln
+            .create_invoice(
+                amount,
+                &unit,
+                description.unwrap_or("".to_string()),
+                quote_expiry,
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not create invoice: {}", err);
+                Error::InvalidPaymentRequest
+            })?;
+
+        let quote = MintQuote::new(
+            self.mint_url.clone(),
+            create_invoice_response.request.to_string(),
+            unit,
+            amount,
+            create_invoice_response.expiry.unwrap_or(0),
+            create_invoice_response.request_lookup_id.clone(),
+        );
+
+        tracing::debug!(
+            "New mint quote {} for {} {} with request id {}",
+            quote.id,
+            amount,
+            unit,
+            create_invoice_response.request_lookup_id,
+        );
+
+        self.localstore.add_mint_quote(quote.clone()).await?;
+
+        Ok(quote.into())
+    }
+
+    /// Check mint quote
+    #[instrument(skip(self))]
+    pub async fn check_mint_quote(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
+        let quote = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        let paid = quote.state == MintQuoteState::Paid;
+
+        // Since the pending state is not part of the NUT it should not be part of the
+        // response. In practice the wallet should not be checking the state of
+        // a quote while waiting for the mint response.
+        let state = match quote.state {
+            MintQuoteState::Pending => MintQuoteState::Paid,
+            s => s,
+        };
+
+        Ok(MintQuoteBolt11Response {
+            quote: quote.id,
+            request: quote.request,
+            paid: Some(paid),
+            state,
+            expiry: Some(quote.expiry),
+        })
+    }
+
+    /// Update mint quote
+    #[instrument(skip_all)]
+    pub async fn update_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
+        self.localstore.add_mint_quote(quote).await?;
+        Ok(())
+    }
+
+    /// Get mint quotes
+    #[instrument(skip_all)]
+    pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let quotes = self.localstore.get_mint_quotes().await?;
+        Ok(quotes)
+    }
+
+    /// Get pending mint quotes
+    #[instrument(skip_all)]
+    pub async fn get_pending_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let mint_quotes = self.localstore.get_mint_quotes().await?;
+
+        Ok(mint_quotes
+            .into_iter()
+            .filter(|p| p.state == MintQuoteState::Pending)
+            .collect())
+    }
+
+    /// Get pending mint quotes
+    #[instrument(skip_all)]
+    pub async fn get_unpaid_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let mint_quotes = self.localstore.get_mint_quotes().await?;
+
+        Ok(mint_quotes
+            .into_iter()
+            .filter(|p| p.state == MintQuoteState::Unpaid)
+            .collect())
+    }
+
+    /// Remove mint quote
+    #[instrument(skip_all)]
+    pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
+        self.localstore.remove_mint_quote(quote_id).await?;
+
+        Ok(())
+    }
+
+    /// Flag mint quote as paid
+    #[instrument(skip_all)]
+    pub async fn pay_mint_quote_for_request_id(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<(), Error> {
+        if let Ok(Some(mint_quote)) = self
+            .localstore
+            .get_mint_quote_by_request_lookup_id(request_lookup_id)
+            .await
+        {
+            tracing::debug!(
+                "Quote {} paid by lookup id {}",
+                mint_quote.id,
+                request_lookup_id
+            );
+            self.localstore
+                .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid)
+                .await?;
+        }
+        Ok(())
+    }
+
+    /// Process mint request
+    #[instrument(skip_all)]
+    pub async fn process_mint_request(
+        &self,
+        mint_request: nut04::MintBolt11Request,
+    ) -> Result<nut04::MintBolt11Response, Error> {
+        // Check quote is known and not expired
+        match self.localstore.get_mint_quote(&mint_request.quote).await? {
+            Some(quote) => {
+                if quote.expiry < unix_time() {
+                    return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
+                }
+            }
+            None => {
+                return Err(Error::UnknownQuote);
+            }
+        }
+
+        let state = self
+            .localstore
+            .update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending)
+            .await?;
+
+        match state {
+            MintQuoteState::Unpaid => {
+                return Err(Error::UnpaidQuote);
+            }
+            MintQuoteState::Pending => {
+                return Err(Error::PendingQuote);
+            }
+            MintQuoteState::Issued => {
+                return Err(Error::IssuedQuote);
+            }
+            MintQuoteState::Paid => (),
+        }
+
+        let blinded_messages: Vec<PublicKey> = mint_request
+            .outputs
+            .iter()
+            .map(|b| b.blinded_secret)
+            .collect();
+
+        if self
+            .localstore
+            .get_blind_signatures(&blinded_messages)
+            .await?
+            .iter()
+            .flatten()
+            .next()
+            .is_some()
+        {
+            tracing::info!("Output has already been signed",);
+            tracing::info!(
+                "Mint {} did not succeed returning quote to Paid state",
+                mint_request.quote
+            );
+
+            self.localstore
+                .update_mint_quote_state(&mint_request.quote, MintQuoteState::Paid)
+                .await
+                .unwrap();
+            return Err(Error::BlindedMessageAlreadySigned);
+        }
+
+        let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
+
+        for blinded_message in mint_request.outputs.iter() {
+            let blind_signature = self.blind_sign(blinded_message).await?;
+            blind_signatures.push(blind_signature);
+        }
+
+        self.localstore
+            .add_blind_signatures(
+                &mint_request
+                    .outputs
+                    .iter()
+                    .map(|p| p.blinded_secret)
+                    .collect::<Vec<PublicKey>>(),
+                &blind_signatures,
+                Some(mint_request.quote.clone()),
+            )
+            .await?;
+
+        self.localstore
+            .update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued)
+            .await?;
+
+        Ok(nut04::MintBolt11Response {
+            signatures: blind_signatures,
+        })
+    }
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 9 - 1340
crates/cdk/src/mint/mod.rs


+ 187 - 0
crates/cdk/src/mint/swap.rs

@@ -0,0 +1,187 @@
+use std::collections::HashSet;
+
+use tracing::instrument;
+
+use crate::dhke::hash_to_curve;
+use crate::Error;
+
+use super::nut11::{enforce_sig_flag, EnforceSigFlag};
+use super::{Id, Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse};
+
+impl Mint {
+    /// Process Swap
+    #[instrument(skip_all)]
+    pub async fn process_swap_request(
+        &self,
+        swap_request: SwapRequest,
+    ) -> Result<SwapResponse, Error> {
+        let blinded_messages: Vec<PublicKey> = swap_request
+            .outputs
+            .iter()
+            .map(|b| b.blinded_secret)
+            .collect();
+
+        if self
+            .localstore
+            .get_blind_signatures(&blinded_messages)
+            .await?
+            .iter()
+            .flatten()
+            .next()
+            .is_some()
+        {
+            tracing::info!("Output has already been signed",);
+
+            return Err(Error::BlindedMessageAlreadySigned);
+        }
+
+        let proofs_total = swap_request.input_amount()?;
+
+        let output_total = swap_request.output_amount()?;
+
+        let fee = self.get_proofs_fee(&swap_request.inputs).await?;
+
+        let total_with_fee = output_total.checked_add(fee).ok_or(Error::AmountOverflow)?;
+
+        if proofs_total != total_with_fee {
+            tracing::info!(
+                "Swap request unbalanced: {}, outputs {}, fee {}",
+                proofs_total,
+                output_total,
+                fee
+            );
+            return Err(Error::TransactionUnbalanced(
+                proofs_total.into(),
+                output_total.into(),
+                fee.into(),
+            ));
+        }
+
+        let proof_count = swap_request.inputs.len();
+
+        let input_ys = swap_request
+            .inputs
+            .iter()
+            .map(|p| hash_to_curve(&p.secret.to_bytes()))
+            .collect::<Result<Vec<PublicKey>, _>>()?;
+
+        self.localstore
+            .add_proofs(swap_request.inputs.clone(), None)
+            .await?;
+        self.check_ys_spendable(&input_ys, State::Pending).await?;
+
+        // Check that there are no duplicate proofs in request
+        if input_ys
+            .iter()
+            .collect::<HashSet<&PublicKey>>()
+            .len()
+            .ne(&proof_count)
+        {
+            self.localstore
+                .update_proofs_states(&input_ys, State::Unspent)
+                .await?;
+            return Err(Error::DuplicateProofs);
+        }
+
+        for proof in &swap_request.inputs {
+            if let Err(err) = self.verify_proof(proof).await {
+                tracing::info!("Error verifying proof in swap");
+                self.localstore
+                    .update_proofs_states(&input_ys, State::Unspent)
+                    .await?;
+                return Err(err);
+            }
+        }
+
+        let input_keyset_ids: HashSet<Id> =
+            swap_request.inputs.iter().map(|p| p.keyset_id).collect();
+
+        let mut keyset_units = HashSet::with_capacity(input_keyset_ids.capacity());
+
+        for id in input_keyset_ids {
+            match self.localstore.get_keyset_info(&id).await? {
+                Some(keyset) => {
+                    keyset_units.insert(keyset.unit);
+                }
+                None => {
+                    tracing::info!("Swap request with unknown keyset in inputs");
+                    self.localstore
+                        .update_proofs_states(&input_ys, State::Unspent)
+                        .await?;
+                }
+            }
+        }
+
+        let output_keyset_ids: HashSet<Id> =
+            swap_request.outputs.iter().map(|p| p.keyset_id).collect();
+
+        for id in &output_keyset_ids {
+            match self.localstore.get_keyset_info(id).await? {
+                Some(keyset) => {
+                    keyset_units.insert(keyset.unit);
+                }
+                None => {
+                    tracing::info!("Swap request with unknown keyset in outputs");
+                    self.localstore
+                        .update_proofs_states(&input_ys, State::Unspent)
+                        .await?;
+                }
+            }
+        }
+
+        // Check that all proofs are the same unit
+        // in the future it maybe possible to support multiple units but unsupported for
+        // now
+        if keyset_units.len().gt(&1) {
+            tracing::error!("Only one unit is allowed in request: {:?}", keyset_units);
+            self.localstore
+                .update_proofs_states(&input_ys, State::Unspent)
+                .await?;
+            return Err(Error::MultipleUnits);
+        }
+
+        let EnforceSigFlag {
+            sig_flag,
+            pubkeys,
+            sigs_required,
+        } = enforce_sig_flag(swap_request.inputs.clone());
+
+        if sig_flag.eq(&SigFlag::SigAll) {
+            let pubkeys = pubkeys.into_iter().collect();
+            for blinded_message in &swap_request.outputs {
+                if let Err(err) = blinded_message.verify_p2pk(&pubkeys, sigs_required) {
+                    tracing::info!("Could not verify p2pk in swap request");
+                    self.localstore
+                        .update_proofs_states(&input_ys, State::Unspent)
+                        .await?;
+                    return Err(err.into());
+                }
+            }
+        }
+
+        let mut promises = Vec::with_capacity(swap_request.outputs.len());
+
+        for blinded_message in swap_request.outputs.iter() {
+            let blinded_signature = self.blind_sign(blinded_message).await?;
+            promises.push(blinded_signature);
+        }
+
+        self.localstore
+            .update_proofs_states(&input_ys, State::Spent)
+            .await?;
+
+        self.localstore
+            .add_blind_signatures(
+                &swap_request
+                    .outputs
+                    .iter()
+                    .map(|o| o.blinded_secret)
+                    .collect::<Vec<PublicKey>>(),
+                &promises,
+                None,
+            )
+            .await?;
+
+        Ok(SwapResponse::new(promises))
+    }
+}

+ 1 - 1
misc/fake_itests.sh

@@ -82,5 +82,5 @@ cargo test -p cdk-integration-tests --test fake_wallet
 # Capture the exit status of cargo test
 test_status=$?
 
-# Exit with the status of the tests
+# Exit with the status of the testexit $test_status
 exit $test_status

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio