Переглянути джерело

Merge branch 'main' into feature/wallet-db-transactions

C 2 місяців тому
батько
коміт
78984c9240

+ 44 - 1
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -8,7 +8,8 @@ use bip39::Mnemonic;
 use cdk::wallet::multi_mint_wallet::{
     MultiMintReceiveOptions as CdkMultiMintReceiveOptions,
     MultiMintSendOptions as CdkMultiMintSendOptions, MultiMintWallet as CdkMultiMintWallet,
-    TransferMode as CdkTransferMode, TransferResult as CdkTransferResult,
+    TokenData as CdkTokenData, TransferMode as CdkTransferMode,
+    TransferResult as CdkTransferResult,
 };
 
 use crate::error::FfiError;
@@ -199,6 +200,27 @@ impl MultiMintWallet {
         }
     }
 
+    pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Vec<KeySetInfo>, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let keysets = self.inner.get_mint_keysets(&cdk_mint_url).await?;
+
+        let keysets = keysets.into_iter().map(|k| k.into()).collect();
+
+        Ok(keysets)
+    }
+
+    /// Get token data (mint URL and proofs) from a token
+    ///
+    /// This method extracts the mint URL and proofs from a token. It will automatically
+    /// fetch the keysets from the mint if needed to properly decode the proofs.
+    ///
+    /// The mint must already be added to the wallet. If the mint is not in the wallet,
+    /// use `add_mint` first.
+    pub async fn get_token_data(&self, token: Arc<Token>) -> Result<TokenData, FfiError> {
+        let token_data = self.inner.get_token_data(&token.inner).await?;
+        Ok(token_data.into())
+    }
+
     /// Get wallet balances for all mints
     pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
         let balances = self.inner.get_balances().await?;
@@ -720,6 +742,27 @@ impl From<CdkTransferResult> for TransferResult {
     }
 }
 
+/// Data extracted from a token including mint URL, proofs, and memo
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct TokenData {
+    /// The mint URL from the token
+    pub mint_url: MintUrl,
+    /// The proofs contained in the token
+    pub proofs: Proofs,
+    /// The memo from the token, if present
+    pub memo: Option<String>,
+}
+
+impl From<CdkTokenData> for TokenData {
+    fn from(data: CdkTokenData) -> Self {
+        Self {
+            mint_url: data.mint_url.into(),
+            proofs: data.proofs.into_iter().map(|p| p.into()).collect(),
+            memo: data.memo,
+        }
+    }
+}
+
 /// Options for receiving tokens in multi-mint context
 #[derive(Debug, Clone, Default, uniffi::Record)]
 pub struct MultiMintReceiveOptions {

+ 8 - 1
crates/cdk-ffi/src/token.rs

@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
 use std::str::FromStr;
 
 use crate::error::FfiError;
-use crate::{Amount, CurrencyUnit, MintUrl, Proofs};
+use crate::{Amount, CurrencyUnit, KeySetInfo, MintUrl, Proofs};
 
 /// FFI-compatible Token
 #[derive(Debug, uniffi::Object)]
@@ -78,6 +78,13 @@ impl Token {
         Ok(proofs.into_iter().map(|p| p.into()).collect())
     }
 
+    /// Get proofs from the token
+    pub fn proofs(&self, mint_keysets: Vec<KeySetInfo>) -> Result<Proofs, FfiError> {
+        let mint_keysets: Vec<_> = mint_keysets.into_iter().map(|k| k.into()).collect();
+        let proofs = self.inner.proofs(&mint_keysets)?;
+        Ok(proofs.into_iter().map(|p| p.into()).collect())
+    }
+
     /// Convert token to raw bytes
     pub fn to_raw_bytes(&self) -> Result<Vec<u8>, FfiError> {
         Ok(self.inner.to_raw_bytes()?)

+ 4 - 0
crates/cdk/Cargo.toml

@@ -142,6 +142,10 @@ required-features = ["wallet"]
 name = "human_readable_payment"
 required-features = ["wallet", "bip353"]
 
+[[example]]
+name = "token-proofs"
+required-features = ["wallet"]
+
 [dev-dependencies]
 rand.workspace = true
 cdk-sqlite.workspace = true

+ 96 - 0
crates/cdk/examples/token-proofs.rs

@@ -0,0 +1,96 @@
+//! Example: Decoding a token and getting proofs using MultiMintWallet
+//!
+//! This example demonstrates how to:
+//! 1. Create a MultiMintWallet
+//! 2. Decode a cashu token
+//! 3. Use `get_token_data` to extract mint URL and proofs in one call
+//! 4. Alternatively, get keysets manually and extract proofs
+
+use std::str::FromStr;
+use std::sync::Arc;
+
+use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::{CurrencyUnit, Token};
+use cdk::wallet::MultiMintWallet;
+use cdk_sqlite::wallet::memory;
+use rand::random;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    // Generate a random seed for the wallet
+    let seed = random::<[u8; 64]>();
+
+    // Initialize the memory store
+    let localstore = Arc::new(memory::empty().await?);
+
+    // Create a new multi-mint wallet for satoshis
+    let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat).await?;
+
+    // Example: A cashu token string (in practice, this would come from user input)
+    let token = Token::from_str("cashuBo2FteB1odHRwczovL2Zha2UudGhlc2ltcGxla2lkLmRldmF1Y3NhdGF0gaJhaUgAlNWndMQKMmFwg6RhYRkIAGFzeEAwYjk0ZjU5ZjU0OTBkNTkzMzI4ZTIwNDllZTNlZmFjYjM5NzljZjU5NzA5ZTM3N2U5YzBmMDQyNDBmZTUyZTVhYWNYIQNGQCYyf1j996pS-LuP_7VsUE-uzRpAm-K4rZiDEFFc1GFko2FlWCBbuMkhvz39ytCzm7xPaY5vdTbqxlxTzXOsks_8S3sf1GFzWCBg22l0CXH5-QLcfJtUJZ2lfylNfC6_o9FTfKClLzthaGFyWCCP2nJ6Qzd8mwLa_85cu8TrwRIprElVgrhqJeoHJwXmSKRhYRkCAGFzeEBhNmMyODliMjMwMTdlMDhjYTFhOTc4ZjAwNGRiNjI4ZDk1NWI5ZTlmNjMwMjY0MjNjZDc4OGExNDBhOWJiYjgxYWNYIQPMXkT68L8Y0a6royMbkoUTbvxOUgsyDwvRZRNTvwUsWWFko2FlWCCj9BFXexBOrlUyUiY_1qEIEHvd1YphWA2l3YhdFwVRh2FzWCBTNgyGeXvGSFtvYKj3MnJCXA8qjI9fzZHFsIw-F_OAGmFyWCDRHiDbVysUuQZucifYx5zMvOKyVIz7zvcJcfd01FoI3KRhYQhhc3hAMWJjOWQ1MjE5ZTZhYzNjZmZhNTM0NTRkY2JjMzE1YzZjZjY5MmM5MDEzYTUzYTA1YzIzN2YwZTBiOTViZTkwMWFjWCEDXd5sxFgxYgUHctpLENYStcr50UtJ4QRojy0g7mkdvWRhZKNhZVggZzSifCUG692E2sW4L6DT_FuKwLZdUFoMnds3tQyMlAdhc1ggtIo0BS2-6arws5fJx_w0phOiCZZcHIFknlrDXSh3C0NhclggM2dDF0kQyuRoOqrOOMHFrmNnvtGiXWxuvqtD7HidR8I")?;
+
+    // Get the mint URL from the token
+    let mint_url = token.mint_url()?;
+    println!("Token mint URL: {}", mint_url);
+
+    // Get token value
+    let value = token.value()?;
+    println!("Token value: {} sats", value);
+
+    // Get token memo if present
+    if let Some(memo) = token.memo() {
+        println!("Token memo: {}", memo);
+    }
+
+    // Add the mint to our wallet so we can fetch keysets
+    wallet.add_mint(mint_url.clone()).await?;
+
+    // =========================================================================
+    // Method 1: Use get_token_data() for a simple one-call approach
+    // =========================================================================
+    println!("\n--- Using get_token_data() ---");
+
+    let token_data = wallet.get_token_data(&token).await?;
+    println!("Mint URL: {}", token_data.mint_url);
+    println!("Number of proofs: {}", token_data.proofs.len());
+
+    for (i, proof) in token_data.proofs.iter().enumerate() {
+        println!(
+            "  Proof {}: {} sats, keyset: {}",
+            i + 1,
+            proof.amount,
+            proof.keyset_id
+        );
+    }
+
+    // =========================================================================
+    // Method 2: Manual approach - get keysets first, then extract proofs
+    // =========================================================================
+    println!("\n--- Using manual keyset lookup ---");
+
+    // Get the keysets for this mint
+    let keysets = wallet.get_mint_keysets(&mint_url).await?;
+    println!("Found {} keysets for mint", keysets.len());
+
+    for keyset in &keysets {
+        println!(
+            "  - Keyset ID: {}, Unit: {:?}, Active: {}",
+            keyset.id, keyset.unit, keyset.active
+        );
+    }
+
+    // Extract proofs from the token using the keysets
+    let proofs = token.proofs(&keysets)?;
+    println!("\nToken contains {} proofs:", proofs.len());
+
+    // Calculate total amount from proofs
+    let total = proofs.total_amount()?;
+    println!("Total amount from proofs: {} sats", total);
+
+    // Verify total matches token value
+    assert_eq!(total, value, "Proof total should match token value");
+
+    println!("\nSuccessfully decoded token and extracted proofs!");
+
+    Ok(())
+}

+ 161 - 1
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -9,10 +9,10 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use anyhow::Result;
-use cdk_common::database;
 use cdk_common::database::WalletDatabase;
 use cdk_common::task::spawn;
 use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection, TransactionId};
+use cdk_common::{database, KeySetInfo};
 use tokio::sync::RwLock;
 use tracing::instrument;
 use zeroize::Zeroize;
@@ -61,6 +61,17 @@ pub struct TransferResult {
     pub target_balance_after: Amount,
 }
 
+/// Data extracted from a token including mint URL, proofs, and memo
+#[derive(Debug, Clone)]
+pub struct TokenData {
+    /// The mint URL from the token
+    pub mint_url: MintUrl,
+    /// The proofs contained in the token
+    pub proofs: Proofs,
+    /// The memo from the token, if present
+    pub memo: Option<String>,
+}
+
 /// Configuration for individual wallets within MultiMintWallet
 #[derive(Clone, Default, Debug)]
 pub struct WalletConfig {
@@ -520,6 +531,66 @@ impl MultiMintWallet {
         &self.unit
     }
 
+    /// Get keysets for a mint url
+    pub async fn get_mint_keysets(&self, mint_url: &MintUrl) -> Result<Vec<KeySetInfo>, Error> {
+        let wallets = self.wallets.read().await;
+        let target_wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        target_wallet.get_mint_keysets().await
+    }
+
+    /// Get token data (mint URL and proofs) from a token
+    ///
+    /// This method extracts the mint URL and proofs from a token. It will automatically
+    /// fetch the keysets from the mint if needed to properly decode the proofs.
+    ///
+    /// The mint must already be added to the wallet. If the mint is not in the wallet,
+    /// use `add_mint` first or set `allow_untrusted` in receive options.
+    ///
+    /// # Arguments
+    ///
+    /// * `token` - The token to extract data from
+    ///
+    /// # Returns
+    ///
+    /// A `TokenData` struct containing the mint URL and proofs
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use cdk::wallet::MultiMintWallet;
+    /// # use cdk::nuts::Token;
+    /// # use std::str::FromStr;
+    /// # async fn example(wallet: &MultiMintWallet) -> Result<(), Box<dyn std::error::Error>> {
+    /// let token = Token::from_str("cashuA...")?;
+    /// let token_data = wallet.get_token_data(&token).await?;
+    /// println!("Mint: {}", token_data.mint_url);
+    /// println!("Proofs: {} total", token_data.proofs.len());
+    /// # Ok(())
+    /// # }
+    /// ```
+    #[instrument(skip(self, token))]
+    pub async fn get_token_data(&self, token: &Token) -> Result<TokenData, Error> {
+        let mint_url = token.mint_url()?;
+
+        // Get the keysets for this mint
+        let keysets = self.get_mint_keysets(&mint_url).await?;
+
+        // Extract proofs using the keysets
+        let proofs = token.proofs(&keysets)?;
+
+        // Get the memo
+        let memo = token.memo().clone();
+
+        Ok(TokenData {
+            mint_url,
+            proofs,
+            memo,
+        })
+    }
+
     /// Get wallet balances for all mints
     #[instrument(skip(self))]
     pub async fn get_balances(&self) -> Result<BTreeMap<MintUrl, Amount>, Error> {
@@ -2037,4 +2108,93 @@ mod tests {
         assert_eq!(options.allowed_mints, vec![mint1, mint2]);
         assert_eq!(options.excluded_mints, vec![mint3]);
     }
+
+    #[tokio::test]
+    async fn test_get_mint_keysets_unknown_mint() {
+        use std::str::FromStr;
+
+        let multi_wallet = create_test_multi_wallet().await;
+        let mint_url = MintUrl::from_str("https://unknown-mint.example.com").unwrap();
+
+        // Should error when trying to get keysets for a mint that hasn't been added
+        let result = multi_wallet.get_mint_keysets(&mint_url).await;
+        assert!(result.is_err());
+
+        match result {
+            Err(Error::UnknownMint { mint_url: url }) => {
+                assert!(url.contains("unknown-mint.example.com"));
+            }
+            _ => panic!("Expected UnknownMint error"),
+        }
+    }
+
+    #[tokio::test]
+    async fn test_multi_mint_receive_options() {
+        use std::str::FromStr;
+
+        let mint_url = MintUrl::from_str("https://trusted.mint.example.com").unwrap();
+
+        // Test default options
+        let default_opts = MultiMintReceiveOptions::default();
+        assert!(!default_opts.allow_untrusted);
+        assert!(default_opts.transfer_to_mint.is_none());
+
+        // Test builder pattern
+        let opts = MultiMintReceiveOptions::new()
+            .allow_untrusted(true)
+            .transfer_to_mint(Some(mint_url.clone()));
+
+        assert!(opts.allow_untrusted);
+        assert_eq!(opts.transfer_to_mint, Some(mint_url));
+    }
+
+    #[tokio::test]
+    async fn test_get_token_data_unknown_mint() {
+        use std::str::FromStr;
+
+        let multi_wallet = create_test_multi_wallet().await;
+
+        // Create a token from a mint that isn't in the wallet
+        // This is a valid token structure pointing to an unknown mint
+        let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
+        let token = Token::from_str(token_str).unwrap();
+
+        // Should error because the mint (localhost:3338) hasn't been added
+        let result = multi_wallet.get_token_data(&token).await;
+        assert!(result.is_err());
+
+        match result {
+            Err(Error::UnknownMint { mint_url }) => {
+                assert!(mint_url.contains("localhost:3338"));
+            }
+            _ => panic!("Expected UnknownMint error"),
+        }
+    }
+
+    #[test]
+    fn test_token_data_struct() {
+        use std::str::FromStr;
+
+        let mint_url = MintUrl::from_str("https://example.mint.com").unwrap();
+        let proofs = vec![];
+        let memo = Some("Test memo".to_string());
+
+        let token_data = TokenData {
+            mint_url: mint_url.clone(),
+            proofs: proofs.clone(),
+            memo: memo.clone(),
+        };
+
+        assert_eq!(token_data.mint_url, mint_url);
+        assert_eq!(token_data.proofs.len(), 0);
+        assert_eq!(token_data.memo, memo);
+
+        // Test with no memo
+        let token_data_no_memo = TokenData {
+            mint_url: mint_url.clone(),
+            proofs: vec![],
+            memo: None,
+        };
+        assert!(token_data_no_memo.memo.is_none());
+    }
 }