Explorar el Código

Melt external (#1357)

Allows melting proofs that are not stored in the wallet's database,
enabling external proof handling similar to receive_proofs.
tsk hace 2 meses
padre
commit
90b0b907a4

+ 33 - 0
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -442,6 +442,39 @@ impl MultiMintWallet {
         Ok(melted.into())
     }
 
+    /// Melt specific proofs from a specific mint
+    ///
+    /// This method allows melting proofs that may not be in the wallet's database,
+    /// similar to how `receive_proofs` handles external proofs. The proofs will be
+    /// added to the database and used for the melt operation.
+    ///
+    /// # Arguments
+    ///
+    /// * `mint_url` - The mint to use for the melt operation
+    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
+    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
+    ///
+    /// # Returns
+    ///
+    /// A `Melted` result containing the payment details and any change proofs
+    pub async fn melt_proofs(
+        &self,
+        mint_url: MintUrl,
+        quote_id: String,
+        proofs: Proofs,
+    ) -> Result<Melted, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        let melted = self
+            .inner
+            .melt_proofs(&cdk_mint_url, &quote_id, cdk_proofs)
+            .await?;
+        Ok(melted.into())
+    }
+
     /// Check melt quote status
     pub async fn check_melt_quote(
         &self,

+ 23 - 0
crates/cdk-ffi/src/wallet.rs

@@ -225,6 +225,29 @@ impl Wallet {
         Ok(melted.into())
     }
 
+    /// Melt specific proofs
+    ///
+    /// This method allows melting proofs that may not be in the wallet's database,
+    /// similar to how `receive_proofs` handles external proofs. The proofs will be
+    /// added to the database and used for the melt operation.
+    ///
+    /// # Arguments
+    ///
+    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
+    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
+    ///
+    /// # Returns
+    ///
+    /// A `Melted` result containing the payment details and any change proofs
+    pub async fn melt_proofs(&self, quote_id: String, proofs: Proofs) -> Result<Melted, FfiError> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        let melted = self.inner.melt_proofs(&quote_id, cdk_proofs).await?;
+        Ok(melted.into())
+    }
+
     /// Get a quote for a bolt12 mint
     pub async fn mint_bolt12_quote(
         &self,

+ 89 - 0
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -1541,3 +1541,92 @@ async fn test_wallet_proof_recovery_after_failed_swap() {
         "Proofs should have been swapped to new ones"
     );
 }
+
+/// Tests that melt_proofs works correctly with proofs that are not already in the wallet's database.
+/// This is similar to the receive flow where proofs come from an external source.
+///
+/// Flow:
+/// 1. Wallet A mints proofs (proofs ARE in Wallet A's database)
+/// 2. Wallet B creates a melt quote
+/// 3. Wallet B calls melt_proofs with proofs from Wallet A (proofs are NOT in Wallet B's database)
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_melt_proofs_external() {
+    // Create sender wallet (Wallet A) and mint some proofs
+    let wallet_sender = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await.unwrap()),
+        Mnemonic::generate(12).unwrap().to_seed_normalized(""),
+        None,
+    )
+    .expect("failed to create sender wallet");
+
+    let mint_quote = wallet_sender.mint_quote(100.into(), None).await.unwrap();
+
+    let mut proof_streams =
+        wallet_sender.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
+
+    let proofs = proof_streams
+        .next()
+        .await
+        .expect("payment")
+        .expect("no error");
+
+    assert_eq!(proofs.total_amount().unwrap(), Amount::from(100));
+
+    // Create receiver/melter wallet (Wallet B) with a separate database
+    // These proofs are NOT in Wallet B's database
+    let wallet_melter = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await.unwrap()),
+        Mnemonic::generate(12).unwrap().to_seed_normalized(""),
+        None,
+    )
+    .expect("failed to create melter wallet");
+
+    // Verify proofs are not in the melter wallet's database
+    let melter_proofs = wallet_melter.get_unspent_proofs().await.unwrap();
+    assert!(
+        melter_proofs.is_empty(),
+        "Melter wallet should have no proofs initially"
+    );
+
+    // Create a fake invoice for melting
+    let fake_description = FakeInvoiceDescription::default();
+    let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap());
+
+    // Wallet B creates a melt quote
+    let melt_quote = wallet_melter
+        .melt_quote(invoice.to_string(), None)
+        .await
+        .unwrap();
+
+    // Wallet B calls melt_proofs with external proofs (from Wallet A)
+    // These proofs are NOT in wallet_melter's database
+    let melted = wallet_melter
+        .melt_proofs(&melt_quote.id, proofs.clone())
+        .await
+        .unwrap();
+
+    // Verify the melt succeeded
+    assert_eq!(melted.amount, Amount::from(9));
+    assert_eq!(melted.fee_paid, 1.into());
+
+    // Verify change was returned (100 input - 9 melt amount = 91 change, minus fee reserve)
+    assert!(melted.change.is_some());
+    let change_amount = melted.change.unwrap().total_amount().unwrap();
+    assert!(change_amount > Amount::ZERO, "Should have received change");
+
+    // Verify the melter wallet now has the change proofs
+    let melter_balance = wallet_melter.total_balance().await.unwrap();
+    assert_eq!(melter_balance, change_amount);
+
+    // Verify a transaction was recorded
+    let transactions = wallet_melter
+        .list_transactions(Some(TransactionDirection::Outgoing))
+        .await
+        .unwrap();
+    assert_eq!(transactions.len(), 1);
+    assert_eq!(transactions[0].amount, Amount::from(9));
+}

+ 7 - 4
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -154,10 +154,13 @@ impl Wallet {
             return Err(Error::InsufficientFunds);
         }
 
-        let ys = proofs.ys()?;
-        self.localstore
-            .update_proofs_state(ys, State::Pending)
-            .await?;
+        // Since the proofs may be external (not in our database), add them first
+        let proofs_info = proofs
+            .clone()
+            .into_iter()
+            .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
+            .collect::<Result<Vec<ProofInfo>, _>>()?;
+        self.localstore.update_proofs(proofs_info, vec![]).await?;
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
 

+ 30 - 0
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -1343,6 +1343,36 @@ impl MultiMintWallet {
         wallet.melt(quote_id).await
     }
 
+    /// Melt specific proofs from a specific mint using a quote ID
+    ///
+    /// This method allows melting proofs that may not be in the wallet's database,
+    /// similar to how `receive_proofs` handles external proofs. The proofs will be
+    /// added to the database and used for the melt operation.
+    ///
+    /// # Arguments
+    ///
+    /// * `mint_url` - The mint to use for the melt operation
+    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
+    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
+    ///
+    /// # Returns
+    ///
+    /// A `Melted` result containing the payment details and any change proofs
+    #[instrument(skip(self, proofs))]
+    pub async fn melt_proofs(
+        &self,
+        mint_url: &MintUrl,
+        quote_id: &str,
+        proofs: Proofs,
+    ) -> Result<Melted, Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.melt_proofs(quote_id, proofs).await
+    }
+
     /// Check a specific melt quote status
     #[instrument(skip(self))]
     pub async fn check_melt_quote(