Browse Source

Add composable wallet trait system

Introduce a modular trait system for wallet implementations that enables
flexible composition of wallet capabilities. This allows different wallet
implementations to share a common interface while supporting selective trait
implementation.

The trait hierarchy consists of:
- WalletTypes: Base trait defining associated types (Amount, Proofs, etc.)
- WalletBalance: Balance query operations
- WalletMintInfo: Mint information and keyset operations
- WalletMint: Minting operations (payment -> tokens)
- WalletMelt: Melting operations (tokens -> payment)
- WalletReceive: Token receiving
- WalletProofs: Proof state management
- Wallet: Super-trait combining all capabilities with blanket impl

Implements all traits for both cdk::Wallet and cdk-ffi::Wallet, enabling
generic code that works with any wallet implementation.
Cesar Rodas 3 days ago
parent
commit
497aa6d413

+ 2 - 0
crates/cdk-common/src/wallet.rs → crates/cdk-common/src/wallet/mod.rs

@@ -1,5 +1,7 @@
 //! Wallet Types
 
+pub mod traits;
+
 use std::collections::HashMap;
 use std::fmt;
 use std::str::FromStr;

+ 27 - 0
crates/cdk-common/src/wallet/traits/balance.rs

@@ -0,0 +1,27 @@
+//! WalletBalance - Balance operations trait
+
+use super::WalletTypes;
+
+/// Trait for wallet balance operations
+///
+/// Provides methods to query the wallet's balance in different states:
+/// - Total unspent balance available for spending
+/// - Pending balance (awaiting confirmation)
+/// - Reserved balance (locked for specific operations)
+#[async_trait::async_trait]
+pub trait WalletBalance: WalletTypes {
+    /// Get the total unspent balance
+    ///
+    /// Returns the sum of all unspent proofs available for spending.
+    async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
+
+    /// Get the total pending balance
+    ///
+    /// Returns the sum of all proofs in pending state (awaiting confirmation).
+    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
+
+    /// Get the total reserved balance
+    ///
+    /// Returns the sum of all proofs reserved for specific operations.
+    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
+}

+ 33 - 0
crates/cdk-common/src/wallet/traits/melt.rs

@@ -0,0 +1,33 @@
+//! WalletMelt - Melting operations trait
+
+use super::WalletTypes;
+
+/// Trait for melting operations
+///
+/// Provides methods for creating melt quotes and melting tokens.
+/// Melting is the process of converting Cashu tokens back to external
+/// payment (e.g., paying a Lightning invoice).
+#[async_trait::async_trait]
+pub trait WalletMelt: WalletTypes {
+    /// The result type returned from a melt operation
+    type MeltResult: Clone + Send + Sync;
+
+    /// Create a melt quote
+    ///
+    /// Requests a quote from the mint for melting tokens to pay a request.
+    /// The quote includes the amount required and any fees.
+    ///
+    /// # Arguments
+    ///
+    /// * `request` - The payment request to pay (e.g., Lightning invoice)
+    async fn melt_quote(&self, request: String) -> Result<Self::MeltQuote, Self::Error>;
+
+    /// Melt tokens to pay a quote
+    ///
+    /// Uses proofs from the wallet to pay the melt quote's payment request.
+    ///
+    /// # Arguments
+    ///
+    /// * `quote_id` - The ID of the melt quote to pay
+    async fn melt(&self, quote_id: &str) -> Result<Self::MeltResult, Self::Error>;
+}

+ 37 - 0
crates/cdk-common/src/wallet/traits/mint.rs

@@ -0,0 +1,37 @@
+//! WalletMint - Minting operations trait
+
+use super::WalletTypes;
+
+/// Trait for minting operations
+///
+/// Provides methods for creating mint quotes and minting tokens.
+/// Minting is the process of converting external payment (e.g., Lightning)
+/// into Cashu tokens.
+#[async_trait::async_trait]
+pub trait WalletMint: WalletTypes {
+    /// Create a mint quote
+    ///
+    /// Requests a quote from the mint for minting a specific amount.
+    /// The quote includes a payment request (e.g., Lightning invoice)
+    /// that must be paid before tokens can be minted.
+    ///
+    /// # Arguments
+    ///
+    /// * `amount` - The amount to mint
+    /// * `description` - Optional description for the quote
+    async fn mint_quote(
+        &self,
+        amount: Self::Amount,
+        description: Option<String>,
+    ) -> Result<Self::MintQuote, Self::Error>;
+
+    /// Mint tokens for a paid quote
+    ///
+    /// After the payment request from a mint quote has been paid,
+    /// this method exchanges the quote for Cashu proofs.
+    ///
+    /// # Arguments
+    ///
+    /// * `quote_id` - The ID of the paid quote
+    async fn mint(&self, quote_id: &str) -> Result<Self::Proofs, Self::Error>;
+}

+ 34 - 0
crates/cdk-common/src/wallet/traits/mint_info.rs

@@ -0,0 +1,34 @@
+//! WalletMintInfo - Mint information and keyset operations trait
+
+use super::WalletTypes;
+
+/// Trait for mint information and keyset operations
+///
+/// Provides methods to query and manage mint metadata including:
+/// - Fetching fresh mint information from the server
+/// - Loading cached mint information
+/// - Managing keysets
+#[async_trait::async_trait]
+pub trait WalletMintInfo: WalletTypes {
+    /// Fetch mint information from the mint server
+    ///
+    /// This always makes a network request to get fresh mint info.
+    /// Returns `None` if the mint does not provide info.
+    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
+
+    /// Load mint information from cache or fetch if needed
+    ///
+    /// This may use cached data if available and fresh, otherwise
+    /// fetches from the mint server.
+    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
+
+    /// Get the active keyset for the wallet's unit
+    ///
+    /// Returns the currently active keyset with the lowest fees.
+    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
+
+    /// Refresh keysets from the mint
+    ///
+    /// Forces a fresh fetch of keyset information from the mint server.
+    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
+}

+ 97 - 0
crates/cdk-common/src/wallet/traits/mod.rs

@@ -0,0 +1,97 @@
+//! Wallet Traits
+//!
+//! This module defines a composable trait system for wallet implementations.
+//! Each trait focuses on a specific capability, allowing flexible and
+//! modular wallet implementations.
+//!
+//! # Trait Hierarchy
+//!
+//! - [`WalletTypes`] - Base trait defining associated types (required by all others)
+//! - [`WalletBalance`] - Balance query operations
+//! - [`WalletMintInfo`] - Mint information and keyset operations
+//! - [`WalletMint`] - Minting operations (convert payment to tokens)
+//! - [`WalletMelt`] - Melting operations (convert tokens to payment)
+//! - [`WalletReceive`] - Receiving tokens
+//! - [`WalletProofs`] - Proof management
+//!
+//! # Super-Trait
+//!
+//! The [`Wallet`] trait combines all capabilities into a single trait,
+//! with a blanket implementation for any type that implements all component traits.
+//!
+//! # Example
+//!
+//! ```ignore
+//! // Function that only needs balance and receive capabilities
+//! async fn check_and_receive<W: WalletBalance + WalletReceive>(
+//!     wallet: &W,
+//!     token: &str,
+//! ) -> Result<W::Amount, W::Error> {
+//!     let before = wallet.total_balance().await?;
+//!     let received = wallet.receive(token).await?;
+//!     Ok(received)
+//! }
+//! ```
+
+mod balance;
+mod melt;
+mod mint;
+mod mint_info;
+mod proofs;
+mod receive;
+mod types;
+
+pub use balance::WalletBalance;
+pub use melt::WalletMelt;
+pub use mint::WalletMint;
+pub use mint_info::WalletMintInfo;
+pub use proofs::WalletProofs;
+pub use receive::WalletReceive;
+pub use types::WalletTypes;
+
+/// Complete wallet trait - composition of all wallet capabilities
+///
+/// This trait combines all wallet capability traits into a single super-trait.
+/// Any type that implements all the component traits automatically implements
+/// `Wallet` through a blanket implementation.
+///
+/// Use this trait when you need the full wallet functionality. For more
+/// targeted use cases, prefer using individual traits or combinations of them.
+///
+/// # Example
+///
+/// ```ignore
+/// use cdk_common::wallet::traits::Wallet;
+///
+/// async fn full_wallet_operation<W: Wallet>(wallet: &W) -> Result<(), W::Error> {
+///     let info = wallet.load_mint_info().await?;
+///     let balance = wallet.total_balance().await?;
+///     // ... use full wallet functionality
+///     Ok(())
+/// }
+/// ```
+pub trait Wallet:
+    WalletTypes
+    + WalletBalance
+    + WalletMintInfo
+    + WalletMint
+    + WalletMelt
+    + WalletReceive
+    + WalletProofs
+{
+}
+
+/// Blanket implementation for Wallet
+///
+/// Any type that implements all the component traits automatically
+/// implements the Wallet super-trait.
+impl<T> Wallet for T where
+    T: WalletTypes
+        + WalletBalance
+        + WalletMintInfo
+        + WalletMint
+        + WalletMelt
+        + WalletReceive
+        + WalletProofs
+{
+}

+ 33 - 0
crates/cdk-common/src/wallet/traits/proofs.rs

@@ -0,0 +1,33 @@
+//! WalletProofs - Proof management trait
+
+use super::WalletTypes;
+
+/// Trait for proof management operations
+///
+/// Provides methods for checking and managing the state of proofs.
+#[async_trait::async_trait]
+pub trait WalletProofs: WalletTypes {
+    /// Check if proofs are spent
+    ///
+    /// Queries the mint to check the state of the provided proofs.
+    /// Returns a boolean for each proof indicating if it has been spent.
+    ///
+    /// # Arguments
+    ///
+    /// * `proofs` - The proofs to check
+    ///
+    /// # Returns
+    ///
+    /// A vector of booleans, where `true` indicates the proof is spent
+    async fn check_proofs_spent(&self, proofs: Self::Proofs) -> Result<Vec<bool>, Self::Error>;
+
+    /// Reclaim unspent proofs
+    ///
+    /// Checks the provided proofs with the mint and reclaims any that
+    /// are still unspent by swapping them for fresh proofs.
+    ///
+    /// # Arguments
+    ///
+    /// * `proofs` - The proofs to reclaim
+    async fn reclaim_unspent(&self, proofs: Self::Proofs) -> Result<(), Self::Error>;
+}

+ 25 - 0
crates/cdk-common/src/wallet/traits/receive.rs

@@ -0,0 +1,25 @@
+//! WalletReceive - Token receiving trait
+
+use super::WalletTypes;
+
+/// Trait for receiving tokens
+///
+/// Provides methods for receiving Cashu tokens into the wallet.
+/// Receiving involves validating and swapping incoming tokens for
+/// fresh proofs controlled by this wallet.
+#[async_trait::async_trait]
+pub trait WalletReceive: WalletTypes {
+    /// Receive tokens from an encoded token string
+    ///
+    /// Parses the encoded token, validates the proofs, and swaps them
+    /// for fresh proofs in the wallet.
+    ///
+    /// # Arguments
+    ///
+    /// * `encoded_token` - The base64-encoded Cashu token string
+    ///
+    /// # Returns
+    ///
+    /// The amount received after any fees
+    async fn receive(&self, encoded_token: &str) -> Result<Self::Amount, Self::Error>;
+}

+ 51 - 0
crates/cdk-common/src/wallet/traits/types.rs

@@ -0,0 +1,51 @@
+//! WalletTypes - Base trait defining all wallet associated types
+
+/// Base trait defining all wallet associated types
+///
+/// This trait provides the foundation for all wallet implementations by defining
+/// the associated types used throughout the wallet trait system. All other wallet
+/// traits require `WalletTypes` as a supertrait.
+///
+/// # Associated Types
+///
+/// - `Amount`: The amount type used for token values
+/// - `Proofs`: Collection of cryptographic proofs
+/// - `Proof`: A single cryptographic proof
+/// - `MintQuote`: Quote information for minting operations
+/// - `MeltQuote`: Quote information for melting operations
+/// - `Token`: Cashu token representation
+/// - `CurrencyUnit`: Currency unit (e.g., sat, msat)
+/// - `MintUrl`: URL of the mint
+/// - `MintInfo`: Information about the mint
+/// - `KeySetInfo`: Keyset metadata
+/// - `Error`: Error type for wallet operations
+pub trait WalletTypes: Send + Sync {
+    /// Amount type for token values
+    type Amount: Clone + Send + Sync;
+    /// Collection of proofs type
+    type Proofs: Clone + Send + Sync;
+    /// Single proof type
+    type Proof: Clone + Send + Sync;
+    /// Mint quote type
+    type MintQuote: Clone + Send + Sync;
+    /// Melt quote type
+    type MeltQuote: Clone + Send + Sync;
+    /// Token type
+    type Token: Clone + Send + Sync;
+    /// Currency unit type
+    type CurrencyUnit: Clone + Send + Sync;
+    /// Mint URL type
+    type MintUrl: Clone + Send + Sync;
+    /// Mint info type
+    type MintInfo: Clone + Send + Sync;
+    /// Keyset info type
+    type KeySetInfo: Clone + Send + Sync;
+    /// Error type for wallet operations
+    type Error: std::error::Error + Send + Sync + 'static;
+
+    /// Get the mint URL
+    fn mint_url(&self) -> Self::MintUrl;
+
+    /// Get the currency unit
+    fn unit(&self) -> Self::CurrencyUnit;
+}

+ 1 - 0
crates/cdk-ffi/src/lib.rs

@@ -18,6 +18,7 @@ pub mod sqlite;
 pub mod token;
 pub mod types;
 pub mod wallet;
+mod wallet_traits;
 
 pub use database::*;
 pub use error::*;

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

@@ -7,7 +7,7 @@ use crate::error::FfiError;
 use crate::{Amount, CurrencyUnit, KeySetInfo, MintUrl, Proofs};
 
 /// FFI-compatible Token
-#[derive(Debug, uniffi::Object)]
+#[derive(Debug, Clone, uniffi::Object)]
 pub struct Token {
     pub(crate) inner: cdk::nuts::Token,
 }

+ 124 - 0
crates/cdk-ffi/src/wallet_traits.rs

@@ -0,0 +1,124 @@
+//! Wallet trait implementations for FFI Wallet
+//!
+//! This module implements the wallet traits from `cdk_common::wallet::traits`
+//! for the FFI Wallet struct.
+
+use std::str::FromStr;
+
+use cdk_common::wallet::traits::{
+    WalletBalance, WalletMelt, WalletMint, WalletMintInfo, WalletProofs, WalletReceive, WalletTypes,
+};
+
+use crate::error::FfiError;
+use crate::token::Token;
+use crate::types::{
+    Amount, CurrencyUnit, KeySetInfo, MeltQuote, Melted, MintInfo, MintQuote, MintUrl, Proof,
+    Proofs,
+};
+use crate::wallet::Wallet;
+
+impl WalletTypes for Wallet {
+    type Amount = Amount;
+    type Proofs = Proofs;
+    type Proof = Proof;
+    type MintQuote = MintQuote;
+    type MeltQuote = MeltQuote;
+    type Token = Token;
+    type CurrencyUnit = CurrencyUnit;
+    type MintUrl = MintUrl;
+    type MintInfo = MintInfo;
+    type KeySetInfo = KeySetInfo;
+    type Error = FfiError;
+
+    fn mint_url(&self) -> Self::MintUrl {
+        self.mint_url()
+    }
+
+    fn unit(&self) -> Self::CurrencyUnit {
+        self.unit()
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletBalance for Wallet {
+    async fn total_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_balance().await
+    }
+
+    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_pending_balance().await
+    }
+
+    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_reserved_balance().await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMintInfo for Wallet {
+    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error> {
+        self.fetch_mint_info().await
+    }
+
+    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error> {
+        self.load_mint_info().await
+    }
+
+    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
+        self.get_active_keyset().await
+    }
+
+    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
+        self.refresh_keysets().await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMint for Wallet {
+    async fn mint_quote(
+        &self,
+        amount: Self::Amount,
+        description: Option<String>,
+    ) -> Result<Self::MintQuote, Self::Error> {
+        self.mint_quote(amount, description).await
+    }
+
+    async fn mint(&self, quote_id: &str) -> Result<Self::Proofs, Self::Error> {
+        self.mint(quote_id.to_string(), crate::types::SplitTarget::None, None)
+            .await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMelt for Wallet {
+    type MeltResult = Melted;
+
+    async fn melt_quote(&self, request: String) -> Result<Self::MeltQuote, Self::Error> {
+        self.melt_quote(request, None).await
+    }
+
+    async fn melt(&self, quote_id: &str) -> Result<Self::MeltResult, Self::Error> {
+        self.melt(quote_id.to_string()).await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletReceive for Wallet {
+    async fn receive(&self, encoded_token: &str) -> Result<Self::Amount, Self::Error> {
+        // Parse the token string into a Token
+        let token = std::sync::Arc::new(Token::from_str(encoded_token)?);
+        self.receive(token, crate::types::ReceiveOptions::default())
+            .await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletProofs for Wallet {
+    async fn check_proofs_spent(&self, proofs: Self::Proofs) -> Result<Vec<bool>, Self::Error> {
+        self.check_proofs_spent(proofs).await
+    }
+
+    async fn reclaim_unspent(&self, proofs: Self::Proofs) -> Result<(), Self::Error> {
+        self.reclaim_unspent(proofs).await
+    }
+}

+ 1 - 0
crates/cdk/src/wallet/mod.rs

@@ -61,6 +61,7 @@ mod send;
 mod streams;
 pub mod subscription;
 mod swap;
+mod traits;
 mod transactions;
 pub mod util;
 

+ 121 - 0
crates/cdk/src/wallet/traits.rs

@@ -0,0 +1,121 @@
+//! Wallet trait implementations
+//!
+//! This module implements the wallet traits from `cdk_common::wallet::traits`
+//! for the CDK Wallet struct.
+
+use cdk_common::wallet::traits::{
+    WalletBalance, WalletMelt, WalletMint, WalletMintInfo, WalletProofs, WalletReceive, WalletTypes,
+};
+
+use crate::amount::SplitTarget;
+use crate::mint_url::MintUrl;
+use crate::nuts::{CurrencyUnit, KeySetInfo, MintInfo, Proof, Proofs, State};
+use crate::types::Melted;
+use crate::wallet::{MeltQuote, MintQuote, ReceiveOptions};
+use crate::{Amount, Error, Wallet};
+
+impl WalletTypes for Wallet {
+    type Amount = Amount;
+    type Proofs = Proofs;
+    type Proof = Proof;
+    type MintQuote = MintQuote;
+    type MeltQuote = MeltQuote;
+    type Token = crate::nuts::Token;
+    type CurrencyUnit = CurrencyUnit;
+    type MintUrl = MintUrl;
+    type MintInfo = MintInfo;
+    type KeySetInfo = KeySetInfo;
+    type Error = Error;
+
+    fn mint_url(&self) -> Self::MintUrl {
+        self.mint_url.clone()
+    }
+
+    fn unit(&self) -> Self::CurrencyUnit {
+        self.unit.clone()
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletBalance for Wallet {
+    async fn total_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_balance().await
+    }
+
+    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_pending_balance().await
+    }
+
+    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error> {
+        self.total_reserved_balance().await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMintInfo for Wallet {
+    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error> {
+        self.fetch_mint_info().await
+    }
+
+    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error> {
+        self.load_mint_info().await
+    }
+
+    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
+        self.get_active_keyset().await
+    }
+
+    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
+        self.refresh_keysets().await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMint for Wallet {
+    async fn mint_quote(
+        &self,
+        amount: Self::Amount,
+        description: Option<String>,
+    ) -> Result<Self::MintQuote, Self::Error> {
+        self.mint_quote(amount, description).await
+    }
+
+    async fn mint(&self, quote_id: &str) -> Result<Self::Proofs, Self::Error> {
+        self.mint(quote_id, SplitTarget::default(), None).await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletMelt for Wallet {
+    type MeltResult = Melted;
+
+    async fn melt_quote(&self, request: String) -> Result<Self::MeltQuote, Self::Error> {
+        self.melt_quote(request, None).await
+    }
+
+    async fn melt(&self, quote_id: &str) -> Result<Self::MeltResult, Self::Error> {
+        self.melt(quote_id).await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletReceive for Wallet {
+    async fn receive(&self, encoded_token: &str) -> Result<Self::Amount, Self::Error> {
+        self.receive(encoded_token, ReceiveOptions::default()).await
+    }
+}
+
+#[async_trait::async_trait]
+impl WalletProofs for Wallet {
+    async fn check_proofs_spent(&self, proofs: Self::Proofs) -> Result<Vec<bool>, Self::Error> {
+        let proof_states = self.check_proofs_spent(proofs).await?;
+        Ok(proof_states
+            .into_iter()
+            .map(|ps| matches!(ps.state, State::Spent | State::PendingSpent))
+            .collect())
+    }
+
+    async fn reclaim_unspent(&self, proofs: Self::Proofs) -> Result<(), Self::Error> {
+        self.reclaim_unspent(proofs).await
+    }
+}