瀏覽代碼

Introduce unified Wallet trait with multi-method mint_quote routing

Add a new `Wallet` trait in cdk-common that provides a comprehensive interface
for all wallet operations with associated types. This enables trait-based
polymorphism for different wallet implementations, better FFI support, and
cleaner separation between interface and implementation.

The `mint_quote` method now accepts a `PaymentMethod` parameter and routes to
Bolt11, Bolt12, or Custom payment backends — matching the existing `melt_quote`
pattern.

Key changes:
- Define `Wallet` trait in cdk-common with associated types for Amount, Proofs,
  MintQuote, MeltQuote, Token, PaymentMethod, etc.
- Implement the trait for the concrete `Wallet` struct in cdk
- Route `mint_quote` by PaymentMethod (Bolt11/Bolt12/Custom)
- Add wallet_trait.rs to cdk-ffi for FFI support
- Update CLI, multi-mint wallet, examples, and integration tests
- Refactor balance, keyset, and issue modules to use the trait
Cesar Rodas 2 周之前
父節點
當前提交
f33b69f370
共有 45 個文件被更改,包括 1531 次插入427 次删除
  1. 25 82
      crates/cdk-cli/src/sub_commands/mint.rs
  2. 390 0
      crates/cdk-common/src/wallet/mod.rs
  3. 2 0
      crates/cdk-ffi/src/lib.rs
  4. 2 1
      crates/cdk-ffi/src/types/amount.rs
  5. 19 38
      crates/cdk-ffi/src/wallet.rs
  6. 505 0
      crates/cdk-ffi/src/wallet_trait.rs
  7. 1 1
      crates/cdk-integration-tests/src/init_pure_tests.rs
  8. 1 1
      crates/cdk-integration-tests/src/lib.rs
  9. 1 1
      crates/cdk-integration-tests/tests/async_melt.rs
  10. 1 1
      crates/cdk-integration-tests/tests/bolt12.rs
  11. 3 1
      crates/cdk-integration-tests/tests/fake_auth.rs
  12. 1 1
      crates/cdk-integration-tests/tests/fake_wallet.rs
  13. 1 1
      crates/cdk-integration-tests/tests/ffi_minting_integration.rs
  14. 1 1
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  15. 1 1
      crates/cdk-integration-tests/tests/integration_tests_pure.rs
  16. 1 1
      crates/cdk-integration-tests/tests/regtest.rs
  17. 1 1
      crates/cdk-integration-tests/tests/test_fees.rs
  18. 1 0
      crates/cdk-integration-tests/tests/test_swap_flow.rs
  19. 1 1
      crates/cdk-integration-tests/tests/wallet_saga.rs
  20. 1 1
      crates/cdk/README.md
  21. 1 1
      crates/cdk/examples/auth_wallet.rs
  22. 1 1
      crates/cdk/examples/bip353.rs
  23. 1 1
      crates/cdk/examples/human_readable_payment.rs
  24. 1 1
      crates/cdk/examples/melt-token.rs
  25. 1 1
      crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
  26. 1 1
      crates/cdk/examples/mint-token-bolt12-with-stream.rs
  27. 1 1
      crates/cdk/examples/mint-token-bolt12.rs
  28. 1 1
      crates/cdk/examples/mint-token.rs
  29. 1 1
      crates/cdk/examples/npubcash.rs
  30. 1 1
      crates/cdk/examples/p2pk.rs
  31. 1 1
      crates/cdk/examples/payment_request.rs
  32. 1 1
      crates/cdk/examples/proof-selection.rs
  33. 1 1
      crates/cdk/examples/receive-token.rs
  34. 1 1
      crates/cdk/examples/restore-wallet.rs
  35. 1 1
      crates/cdk/examples/wallet.rs
  36. 1 34
      crates/cdk/src/wallet/balance.rs
  37. 4 95
      crates/cdk/src/wallet/issue/mod.rs
  38. 3 49
      crates/cdk/src/wallet/keysets.rs
  39. 1 1
      crates/cdk/src/wallet/melt/custom.rs
  40. 1 1
      crates/cdk/src/wallet/melt/saga/mod.rs
  41. 8 94
      crates/cdk/src/wallet/mod.rs
  42. 1 1
      crates/cdk/src/wallet/receive/saga/mod.rs
  43. 2 2
      crates/cdk/src/wallet/send/saga/mod.rs
  44. 1 1
      crates/cdk/src/wallet/wallet_repository.rs
  45. 535 0
      crates/cdk/src/wallet/wallet_trait.rs

+ 25 - 82
crates/cdk-cli/src/sub_commands/mint.rs

@@ -5,9 +5,8 @@ use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::WalletRepository;
+use cdk::wallet::{WalletRepository, WalletTrait};
 use cdk::{Amount, StreamExt};
-use cdk_common::nut00::KnownMethod;
 use clap::Args;
 use serde::{Deserialize, Serialize};
 
@@ -52,86 +51,30 @@ pub async fn mint(
     let payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
 
     let quote = match &sub_command_args.quote_id {
-        None => match payment_method {
-            PaymentMethod::Known(KnownMethod::Bolt11) => {
-                let amount = sub_command_args
-                    .amount
-                    .ok_or(anyhow!("Amount must be defined"))?;
-                let quote = wallet
-                    .mint_quote(
-                        PaymentMethod::BOLT11,
-                        Some(Amount::from(amount)),
-                        description,
-                        None,
-                    )
-                    .await?;
-
-                println!(
-                    "Quote: id={}, state={}, amount={}, expiry={}",
-                    quote.id,
-                    quote.state,
-                    quote.amount.map_or("none".to_string(), |a| a.to_string()),
-                    quote.expiry
-                );
-
-                println!("Please pay: {}", quote.request);
-
-                quote
-            }
-            PaymentMethod::Known(KnownMethod::Bolt12) => {
-                let amount = sub_command_args.amount;
-                println!(
-                    "Single use: {}",
-                    sub_command_args
-                        .single_use
-                        .map_or("none".to_string(), |b| b.to_string())
-                );
-                let quote = wallet
-                    .mint_quote(
-                        payment_method.clone(),
-                        amount.map(|a| a.into()),
-                        description,
-                        None,
-                    )
-                    .await?;
-
-                println!(
-                    "Quote: id={}, state={}, amount={}, expiry={}",
-                    quote.id,
-                    quote.state,
-                    quote.amount.map_or("none".to_string(), |a| a.to_string()),
-                    quote.expiry
-                );
-
-                println!("Please pay: {}", quote.request);
-
-                quote
-            }
-            _ => {
-                let amount = sub_command_args.amount;
-                println!(
-                    "Single use: {}",
-                    sub_command_args
-                        .single_use
-                        .map_or("none".to_string(), |b| b.to_string())
-                );
-                let quote = wallet
-                    .mint_quote(payment_method.clone(), amount.map(|a| a.into()), None, None)
-                    .await?;
-
-                println!(
-                    "Quote: id={}, state={}, amount={}, expiry={}",
-                    quote.id,
-                    quote.state,
-                    quote.amount.map_or("none".to_string(), |a| a.to_string()),
-                    quote.expiry
-                );
-
-                println!("Please pay: {}", quote.request);
-
-                quote
-            }
-        },
+        None => {
+            let amount = sub_command_args.amount.map(Amount::from);
+
+            let quote = WalletTrait::mint_quote(
+                wallet.as_ref(),
+                payment_method.clone(),
+                amount,
+                description,
+                None,
+            )
+            .await?;
+
+            println!(
+                "Quote: id={}, state={}, amount={}, expiry={}",
+                quote.id,
+                quote.state,
+                quote.amount.map_or("none".to_string(), |a| a.to_string()),
+                quote.expiry
+            );
+
+            println!("Please pay: {}", quote.request);
+
+            quote
+        }
         Some(quote_id) => wallet
             .localstore
             .get_mint_quote(quote_id)

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

@@ -673,3 +673,393 @@ mod tests {
         assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
     }
 }
+
+/// Abstract wallet interface for Cashu protocol operations.
+///
+/// This trait defines the complete set of operations a Cashu wallet must support,
+/// using associated types to remain implementation-agnostic. It enables:
+///
+/// - **Polymorphism**: program against the interface rather than a concrete wallet
+/// - **FFI support**: wrap the trait with foreign-function-friendly types
+/// - **Testability**: mock wallet behavior in tests without a real mint
+///
+/// A wallet is bound to a single mint URL and currency unit. For multi-mint
+/// scenarios, see `MultiMintWallet` which manages a collection of per-mint wallets.
+///
+/// # Lifecycle
+///
+/// A typical usage flow:
+/// 1. **Mint** — request a quote, pay the invoice, mint proofs
+/// 2. **Send** — select proofs and produce a token for the recipient
+/// 3. **Receive** — swap incoming proofs into the local wallet
+/// 4. **Melt** — redeem proofs by paying a Lightning invoice
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+pub trait Wallet: Send + Sync {
+    // --- Associated types ---
+
+    /// Numeric amount (e.g. `cdk_common::Amount`)
+    type Amount: Clone + Send + Sync;
+    /// Ordered collection of ecash proofs
+    type Proofs: Clone + Send + Sync;
+    /// Single ecash proof
+    type Proof: Clone + Send + Sync;
+    /// Mint-issued quote describing how to fund the wallet (NUT-04 / NUT-23 / NUT-25)
+    type MintQuote: Clone + Send + Sync;
+    /// Mint-issued quote describing a Lightning payment to execute (NUT-05 / NUT-24)
+    type MeltQuote: Clone + Send + Sync;
+    /// Outcome of a confirmed melt, including preimage and fee details
+    type MeltResult: Clone + Send + Sync;
+    /// Serialisable ecash token (V3 / V4)
+    type Token: Clone + Send + Sync;
+    /// Currency unit for this wallet's keyset (e.g. `sat`, `usd`)
+    type CurrencyUnit: Clone + Send + Sync;
+    /// Parsed and validated mint URL
+    type MintUrl: Clone + Send + Sync;
+    /// Mint self-description returned by `GET /v1/info`
+    type MintInfo: Clone + Send + Sync;
+    /// Keyset metadata (id, unit, fee rate, active flag, …)
+    type KeySetInfo: Clone + Send + Sync;
+    /// Error type returned by all fallible operations
+    type Error: Send + Sync + 'static;
+    /// Configuration for [`send`](Self::send) (memo, amount split target, P2PK, …)
+    type SendOptions: Clone + Send + Sync;
+    /// Configuration for [`receive`](Self::receive) (P2PK pre-image, …)
+    type ReceiveOptions: Clone + Send + Sync;
+    /// P2PK / HTLC spending conditions attached to outputs
+    type SpendingConditions: Clone + Send + Sync;
+    /// Strategy for splitting proof amounts (e.g. powers-of-two, custom targets)
+    type SplitTarget: Clone + Send + Sync + Default;
+    /// Payment protocol selector (Bolt11, Bolt12, or a custom method name)
+    type PaymentMethod: Clone + Send + Sync;
+    /// Per-method melt options (MPP, amountless invoices, …)
+    type MeltOptions: Clone + Send + Sync;
+    /// Summary returned by [`restore`](Self::restore) (proofs recovered, amount, …)
+    type Restored: Clone + Send + Sync;
+    /// Persistent record of a wallet operation (mint, melt, send, receive, …)
+    type Transaction: Clone + Send + Sync;
+    /// Unique identifier for a [`Transaction`](Self::Transaction)
+    type TransactionId: Clone + Send + Sync;
+    /// Incoming vs outgoing filter for [`list_transactions`](Self::list_transactions)
+    type TransactionDirection: Clone + Send + Sync;
+    /// NUT-18 payment request
+    type PaymentRequest: Clone + Send + Sync;
+    /// Handle to an active WebSocket subscription; dropping it unsubscribes
+    type Subscription: Send + Sync;
+    /// Parameters passed to [`subscribe`](Self::subscribe) to filter events
+    type SubscribeParams: Clone + Send + Sync;
+
+    // --- Identity ---
+
+    /// Return the mint URL this wallet is bound to.
+    fn mint_url(&self) -> Self::MintUrl;
+
+    /// Return the currency unit this wallet operates in.
+    fn unit(&self) -> Self::CurrencyUnit;
+
+    // --- Balance ---
+
+    /// Return the sum of all `Unspent` proof amounts for this mint and unit.
+    async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
+
+    /// Return the sum of all `Pending` proof amounts (proofs involved in
+    /// in-flight operations that have not yet settled).
+    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
+
+    /// Return the sum of all `Reserved` proof amounts (proofs locked to
+    /// a send that has not been claimed or revoked).
+    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
+
+    // --- Mint info ---
+
+    /// Fetch mint info by calling `GET /v1/info` on the mint.
+    ///
+    /// Always makes a network request and updates the local cache.
+    /// Returns `None` if the mint does not expose info.
+    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
+
+    /// Return cached mint info, re-fetching from the mint only when the
+    /// cache TTL has expired.
+    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
+
+    /// Return the active keyset that has the lowest input fee per proof (`input_fee_ppk`).
+    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
+
+    /// Fetch the latest keysets from the mint, store them locally, and return
+    /// the full list of keysets for this wallet's unit.
+    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
+
+    // --- Minting ---
+
+    /// Request a mint quote for the given payment method.
+    ///
+    /// The mint returns an invoice (or equivalent payment request) that, once
+    /// paid, allows the caller to mint ecash proofs of the quoted amount.
+    ///
+    /// # Arguments
+    /// * `method` — payment protocol to use (Bolt11, Bolt12, or custom)
+    /// * `amount` — requested amount; **required** for Bolt11 and Custom,
+    ///   optional for Bolt12 (the payer chooses the amount)
+    /// * `description` — optional memo embedded in the invoice; only honoured
+    ///   when the mint advertises description support for the method
+    /// * `extra` — optional JSON string with method-specific fields (used by
+    ///   custom payment methods)
+    async fn mint_quote(
+        &self,
+        method: Self::PaymentMethod,
+        amount: Option<Self::Amount>,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<Self::MintQuote, Self::Error>;
+
+    /// Re-fetch the current state of a mint quote from the mint.
+    ///
+    /// Use this to poll whether the underlying invoice has been paid.
+    /// The returned quote reflects the latest `state` and `amount_paid`.
+    async fn refresh_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error>;
+
+    // --- Melting ---
+
+    /// Request a melt quote to pay an external invoice with ecash.
+    ///
+    /// The mint estimates the amount of ecash (including fees) needed to
+    /// settle the given payment request.
+    ///
+    /// # Arguments
+    /// * `method` — payment protocol to use (Bolt11, Bolt12, or custom)
+    /// * `request` — the payment request string (e.g. a BOLT-11 invoice or
+    ///   BOLT-12 offer)
+    /// * `options` — method-specific options (MPP, amountless invoice amount, …)
+    /// * `extra` — optional JSON string with custom-method-specific fields
+    async fn melt_quote(
+        &self,
+        method: Self::PaymentMethod,
+        request: String,
+        options: Option<Self::MeltOptions>,
+        extra: Option<String>,
+    ) -> Result<Self::MeltQuote, Self::Error>;
+
+    // --- Sending ---
+
+    /// Select proofs for the given `amount`, optionally swap for exact
+    /// change, and produce an ecash token to hand to the recipient.
+    async fn send(
+        &self,
+        amount: Self::Amount,
+        options: Self::SendOptions,
+    ) -> Result<Self::Token, Self::Error>;
+
+    /// Return the IDs of all in-flight send operations whose proofs are
+    /// currently in the `Reserved` state.
+    async fn get_pending_sends(&self) -> Result<Vec<String>, Self::Error>;
+
+    /// Cancel a pending send and return the reserved proofs to `Unspent`.
+    ///
+    /// Returns the total amount of proofs reclaimed.
+    async fn revoke_send(&self, operation_id: &str) -> Result<Self::Amount, Self::Error>;
+
+    /// Check whether the recipient has already swapped the proofs from
+    /// a pending send. Returns `true` if the proofs have been spent.
+    async fn check_send_status(&self, operation_id: &str) -> Result<bool, Self::Error>;
+
+    // --- Receiving ---
+
+    /// Decode an ecash token string, swap the proofs into this wallet,
+    /// and return the received amount.
+    async fn receive(
+        &self,
+        encoded_token: &str,
+        options: Self::ReceiveOptions,
+    ) -> Result<Self::Amount, Self::Error>;
+
+    /// Swap raw proofs into this wallet (e.g. proofs obtained out-of-band).
+    ///
+    /// `memo` and `token` are optional metadata stored alongside the
+    /// resulting transaction record.
+    async fn receive_proofs(
+        &self,
+        proofs: Self::Proofs,
+        options: Self::ReceiveOptions,
+        memo: Option<String>,
+        token: Option<String>,
+    ) -> Result<Self::Amount, Self::Error>;
+
+    // --- Swapping ---
+
+    /// Swap `input_proofs` at the mint, optionally changing denominations
+    /// or attaching spending conditions to the new outputs.
+    ///
+    /// Returns `None` when no change proofs are produced (all value went
+    /// to conditioned outputs).
+    async fn swap(
+        &self,
+        amount: Option<Self::Amount>,
+        amount_split_target: Self::SplitTarget,
+        input_proofs: Self::Proofs,
+        spending_conditions: Option<Self::SpendingConditions>,
+        include_fees: bool,
+    ) -> Result<Option<Self::Proofs>, Self::Error>;
+
+    // --- Proofs ---
+
+    /// Return all proofs in the `Unspent` state.
+    async fn get_unspent_proofs(&self) -> Result<Self::Proofs, Self::Error>;
+
+    /// Return all proofs in the `Pending` state (involved in an in-flight
+    /// mint, melt, or swap).
+    async fn get_pending_proofs(&self) -> Result<Self::Proofs, Self::Error>;
+
+    /// Return all proofs in the `Reserved` state (locked to an unclaimed send).
+    async fn get_reserved_proofs(&self) -> Result<Self::Proofs, Self::Error>;
+
+    /// Return all proofs in the `PendingSpent` state (sent to the mint
+    /// but not yet confirmed spent).
+    async fn get_pending_spent_proofs(&self) -> Result<Self::Proofs, Self::Error>;
+
+    /// Query the mint for the current state of every pending proof,
+    /// reclaim any that are still unspent, and return the total
+    /// amount reclaimed.
+    async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
+
+    /// Ask the mint which of the given `proofs` have been spent.
+    ///
+    /// Returns a `Vec<bool>` aligned with the input: `true` = spent.
+    async fn check_proofs_spent(&self, proofs: Self::Proofs) -> Result<Vec<bool>, Self::Error>;
+
+    /// Swap the given proofs back into the wallet, discarding any that
+    /// the mint reports as already spent.
+    async fn reclaim_unspent(&self, proofs: Self::Proofs) -> Result<(), Self::Error>;
+
+    // --- Transactions ---
+
+    /// List recorded transactions, optionally filtered by direction.
+    ///
+    /// Pass `None` to return both incoming and outgoing transactions.
+    async fn list_transactions(
+        &self,
+        direction: Option<Self::TransactionDirection>,
+    ) -> Result<Vec<Self::Transaction>, Self::Error>;
+
+    /// Look up a single transaction by its ID.
+    ///
+    /// Returns `None` if no transaction with that ID exists.
+    async fn get_transaction(
+        &self,
+        id: Self::TransactionId,
+    ) -> Result<Option<Self::Transaction>, Self::Error>;
+
+    /// Return the proofs that were involved in the given transaction.
+    async fn get_proofs_for_transaction(
+        &self,
+        id: Self::TransactionId,
+    ) -> Result<Self::Proofs, Self::Error>;
+
+    /// Revert a transaction by returning its proofs to the `Unspent` state.
+    ///
+    /// This is only valid for transactions whose proofs have **not** been
+    /// spent at the mint.
+    async fn revert_transaction(&self, id: Self::TransactionId) -> Result<(), Self::Error>;
+
+    // --- Token verification ---
+
+    /// Verify the DLEQ (Discrete-Log Equality) proofs on every proof
+    /// inside the given token, ensuring they were signed by the mint.
+    async fn verify_token_dleq(&self, token: &Self::Token) -> Result<(), Self::Error>;
+
+    // --- Wallet recovery ---
+
+    /// Deterministically re-derive all secrets from the wallet seed and
+    /// recover any proofs that the mint still considers unspent.
+    async fn restore(&self) -> Result<Self::Restored, Self::Error>;
+
+    // --- Keysets & fees ---
+
+    /// Return the `input_fee_ppk` (parts per thousand) for the given keyset.
+    async fn get_keyset_fees(&self, keyset_id: &str) -> Result<u64, Self::Error>;
+
+    /// Calculate the total fee for spending `proof_count` proofs from the
+    /// given keyset.
+    async fn calculate_fee(
+        &self,
+        proof_count: u64,
+        keyset_id: &str,
+    ) -> Result<Self::Amount, Self::Error>;
+
+    // --- Subscriptions ---
+
+    /// Open a WebSocket subscription to the mint for real-time state
+    /// updates (e.g. quote state changes).
+    ///
+    /// The returned handle stays subscribed until it is dropped.
+    async fn subscribe(
+        &self,
+        params: Self::SubscribeParams,
+    ) -> Result<Self::Subscription, Self::Error>;
+
+    // --- Payment requests ---
+
+    /// Fulfil a NUT-18 payment request by sending ecash to the payee
+    /// via the transport specified in the request.
+    ///
+    /// `custom_amount` overrides the amount in the request when set.
+    async fn pay_request(
+        &self,
+        request: Self::PaymentRequest,
+        custom_amount: Option<Self::Amount>,
+    ) -> Result<(), Self::Error>;
+
+    // --- BIP-353 / Lightning Address ---
+
+    /// Resolve a BIP-353 human-readable address to a BOLT-12 offer and
+    /// return a melt quote for the given `amount` (in millisatoshis).
+    ///
+    /// Not available on `wasm32` targets (requires DNS resolution).
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_bip353_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error>;
+
+    /// Resolve a Lightning Address (LNURL-pay) and return a melt quote
+    /// for the given `amount` (in millisatoshis).
+    ///
+    /// Not available on `wasm32` targets (requires HTTPS callback).
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_lightning_address_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error>;
+
+    /// Resolve a human-readable address (tries BIP-353 first, then
+    /// Lightning Address) and return a melt quote for the given `amount`
+    /// (in millisatoshis).
+    ///
+    /// Not available on `wasm32` targets.
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_human_readable_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error>;
+
+    // --- Auth ---
+
+    /// Store a Clear Auth Token (CAT) for authenticated mint access.
+    async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
+
+    /// Store an OAuth2 refresh token for authenticated mint access.
+    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
+
+    /// Use the stored refresh token to obtain a new access token from the
+    /// mint's OIDC provider.
+    async fn refresh_access_token(&self) -> Result<(), Self::Error>;
+
+    /// Mint blind-auth proofs that can be presented to the mint to
+    /// authenticate future requests.
+    async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Self::Proofs, Self::Error>;
+
+    /// Return all unspent blind-auth proofs.
+    async fn get_unspent_auth_proofs(&self) -> Result<Self::Proofs, Self::Error>;
+}

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

@@ -18,7 +18,9 @@ pub mod token;
 pub mod types;
 pub mod wallet;
 pub mod wallet_repository;
+mod wallet_trait;
 
+pub use cdk_common::wallet::Wallet as WalletTrait;
 pub use database::*;
 pub use error::*;
 pub use logging::*;

+ 2 - 1
crates/cdk-ffi/src/types/amount.rs

@@ -129,9 +129,10 @@ impl From<CurrencyUnit> for CdkCurrencyUnit {
 }
 
 /// FFI-compatible SplitTarget
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, uniffi::Enum)]
 pub enum SplitTarget {
     /// Default target; least amount of proofs
+    #[default]
     None,
     /// Target amount for wallet to have most proofs that add up to value
     Value { amount: Amount },

+ 19 - 38
crates/cdk-ffi/src/wallet.rs

@@ -4,7 +4,7 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use bip39::Mnemonic;
-use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
+use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder, WalletTrait};
 
 use crate::error::FfiError;
 use crate::token::Token;
@@ -23,6 +23,11 @@ impl Wallet {
     pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
         Self { inner }
     }
+
+    /// Get reference to the inner CDK wallet
+    pub(crate) fn inner(&self) -> &Arc<CdkWallet> {
+        &self.inner
+    }
 }
 
 #[uniffi::export(async_runtime = "tokio")]
@@ -93,37 +98,9 @@ impl Wallet {
         self.inner.set_metadata_cache_ttl(ttl);
     }
 
-    /// Get total balance
-    pub async fn total_balance(&self) -> Result<Amount, FfiError> {
-        let balance = self.inner.total_balance().await?;
-        Ok(balance.into())
-    }
-
-    /// Get total pending balance
-    pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
-        let balance = self.inner.total_pending_balance().await?;
-        Ok(balance.into())
-    }
-
-    /// Get total reserved balance
-    pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
-        let balance = self.inner.total_reserved_balance().await?;
-        Ok(balance.into())
-    }
-
-    /// Get mint info from mint
-    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
-        let info = self.inner.fetch_mint_info().await?;
-        Ok(info.map(Into::into))
-    }
-
-    /// Load mint info
-    ///
-    /// This will get mint info from cache if it is fresh
-    pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
-        let info = self.inner.load_mint_info().await?;
-        Ok(info.into())
-    }
+    // total_balance(), total_pending_balance(), total_reserved_balance(),
+    // fetch_mint_info(), load_mint_info() are now available via wallet traits
+    // (WalletBalance, WalletMintInfo) in wallet_traits.rs
 
     /// Receive tokens
     pub async fn receive(
@@ -216,10 +193,14 @@ impl Wallet {
         description: Option<String>,
         extra: Option<String>,
     ) -> Result<MintQuote, FfiError> {
-        let quote = self
-            .inner
-            .mint_quote(payment_method, amount.map(Into::into), description, extra)
-            .await?;
+        let quote = <CdkWallet as WalletTrait>::mint_quote(
+            self.inner.as_ref(),
+            payment_method.into(),
+            amount.map(Into::into),
+            description,
+            extra,
+        )
+        .await?;
         Ok(quote.into())
     }
 
@@ -481,13 +462,13 @@ impl Wallet {
 
     /// Refresh keysets from the mint
     pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
-        let keysets = self.inner.refresh_keysets().await?;
+        let keysets = <CdkWallet as WalletTrait>::refresh_keysets(self.inner.as_ref()).await?;
         Ok(keysets.into_iter().map(Into::into).collect())
     }
 
     /// Get the active keyset for the wallet's unit
     pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
-        let keyset = self.inner.get_active_keyset().await?;
+        let keyset = <CdkWallet as WalletTrait>::get_active_keyset(self.inner.as_ref()).await?;
         Ok(keyset.into())
     }
 

+ 505 - 0
crates/cdk-ffi/src/wallet_trait.rs

@@ -0,0 +1,505 @@
+//! Wallet trait implementations for the FFI [`Wallet`].
+//!
+//! Each method delegates to the CDK wallet's trait implementation,
+//! converting types between FFI and CDK representations.
+
+use std::sync::Arc;
+
+use cdk_common::wallet::Wallet as CdkWalletTrait;
+
+use crate::error::FfiError;
+use crate::types::*;
+use crate::wallet::Wallet;
+
+#[async_trait::async_trait]
+impl CdkWalletTrait for Wallet {
+    type Amount = Amount;
+    type Proofs = Proofs;
+    type Proof = Proof;
+    type MintQuote = MintQuote;
+    type MeltQuote = MeltQuote;
+    type MeltResult = FinalizedMelt;
+    type Token = String;
+    type CurrencyUnit = CurrencyUnit;
+    type MintUrl = MintUrl;
+    type MintInfo = MintInfo;
+    type KeySetInfo = KeySetInfo;
+    type Error = FfiError;
+    type SendOptions = SendOptions;
+    type ReceiveOptions = ReceiveOptions;
+    type SpendingConditions = SpendingConditions;
+    type SplitTarget = SplitTarget;
+    type PaymentMethod = PaymentMethod;
+    type MeltOptions = MeltOptions;
+    type Restored = Restored;
+    type Transaction = Transaction;
+    type TransactionId = TransactionId;
+    type TransactionDirection = TransactionDirection;
+    type PaymentRequest = Arc<crate::types::payment_request::PaymentRequest>;
+    type Subscription = Arc<ActiveSubscription>;
+    type SubscribeParams = SubscribeParams;
+
+    fn mint_url(&self) -> Self::MintUrl {
+        <cdk::wallet::Wallet as CdkWalletTrait>::mint_url(self.inner().as_ref()).into()
+    }
+
+    fn unit(&self) -> Self::CurrencyUnit {
+        <cdk::wallet::Wallet as CdkWalletTrait>::unit(self.inner().as_ref()).into()
+    }
+
+    async fn total_balance(&self) -> Result<Self::Amount, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::total_balance(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::total_pending_balance(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::total_reserved_balance(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error> {
+        let info =
+            <cdk::wallet::Wallet as CdkWalletTrait>::fetch_mint_info(self.inner().as_ref()).await?;
+        Ok(info.map(Into::into))
+    }
+
+    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::load_mint_info(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_active_keyset(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
+        let keysets =
+            <cdk::wallet::Wallet as CdkWalletTrait>::refresh_keysets(self.inner().as_ref()).await?;
+        Ok(keysets.into_iter().map(Into::into).collect())
+    }
+
+    async fn mint_quote(
+        &self,
+        method: Self::PaymentMethod,
+        amount: Option<Self::Amount>,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<Self::MintQuote, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::mint_quote(
+            self.inner().as_ref(),
+            method.into(),
+            amount.map(Into::into),
+            description,
+            extra,
+        )
+        .await?
+        .into())
+    }
+
+    async fn refresh_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::refresh_mint_quote(
+            self.inner().as_ref(),
+            quote_id,
+        )
+        .await?
+        .into())
+    }
+
+    async fn melt_quote(
+        &self,
+        method: Self::PaymentMethod,
+        request: String,
+        options: Option<Self::MeltOptions>,
+        extra: Option<String>,
+    ) -> Result<Self::MeltQuote, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::melt_quote(
+            self.inner().as_ref(),
+            method.into(),
+            request,
+            options.map(Into::into),
+            extra,
+        )
+        .await?
+        .into())
+    }
+
+    async fn send(
+        &self,
+        amount: Self::Amount,
+        options: Self::SendOptions,
+    ) -> Result<Self::Token, Self::Error> {
+        let token = <cdk::wallet::Wallet as CdkWalletTrait>::send(
+            self.inner().as_ref(),
+            amount.into(),
+            options.into(),
+        )
+        .await?;
+        Ok(token.to_string())
+    }
+
+    async fn get_pending_sends(&self) -> Result<Vec<String>, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_pending_sends(self.inner().as_ref())
+                .await?,
+        )
+    }
+
+    async fn revoke_send(&self, operation_id: &str) -> Result<Self::Amount, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::revoke_send(
+            self.inner().as_ref(),
+            operation_id,
+        )
+        .await?
+        .into())
+    }
+
+    async fn check_send_status(&self, operation_id: &str) -> Result<bool, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::check_send_status(
+            self.inner().as_ref(),
+            operation_id,
+        )
+        .await?)
+    }
+
+    async fn receive(
+        &self,
+        encoded_token: &str,
+        options: Self::ReceiveOptions,
+    ) -> Result<Self::Amount, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::receive(
+            self.inner().as_ref(),
+            encoded_token,
+            options.into(),
+        )
+        .await?
+        .into())
+    }
+
+    async fn receive_proofs(
+        &self,
+        proofs: Self::Proofs,
+        options: Self::ReceiveOptions,
+        memo: Option<String>,
+        token: Option<String>,
+    ) -> Result<Self::Amount, Self::Error> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::receive_proofs(
+            self.inner().as_ref(),
+            cdk_proofs,
+            options.into(),
+            memo,
+            token,
+        )
+        .await?
+        .into())
+    }
+
+    async fn swap(
+        &self,
+        amount: Option<Self::Amount>,
+        amount_split_target: Self::SplitTarget,
+        input_proofs: Self::Proofs,
+        spending_conditions: Option<Self::SpendingConditions>,
+        include_fees: bool,
+    ) -> Result<Option<Self::Proofs>, Self::Error> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            input_proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let result = <cdk::wallet::Wallet as CdkWalletTrait>::swap(
+            self.inner().as_ref(),
+            amount.map(Into::into),
+            amount_split_target.into(),
+            cdk_proofs,
+            conditions,
+            include_fees,
+        )
+        .await?;
+
+        Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
+    }
+
+    async fn get_unspent_proofs(&self) -> Result<Self::Proofs, Self::Error> {
+        let proofs =
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_unspent_proofs(self.inner().as_ref())
+                .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_pending_proofs(&self) -> Result<Self::Proofs, Self::Error> {
+        let proofs =
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_pending_proofs(self.inner().as_ref())
+                .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_reserved_proofs(&self) -> Result<Self::Proofs, Self::Error> {
+        let proofs =
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_reserved_proofs(self.inner().as_ref())
+                .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_pending_spent_proofs(&self) -> Result<Self::Proofs, Self::Error> {
+        let proofs = <cdk::wallet::Wallet as CdkWalletTrait>::get_pending_spent_proofs(
+            self.inner().as_ref(),
+        )
+        .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::check_all_pending_proofs(
+                self.inner().as_ref(),
+            )
+            .await?
+            .into(),
+        )
+    }
+
+    async fn check_proofs_spent(&self, proofs: Self::Proofs) -> Result<Vec<bool>, Self::Error> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::check_proofs_spent(
+            self.inner().as_ref(),
+            cdk_proofs,
+        )
+        .await?)
+    }
+
+    async fn reclaim_unspent(&self, proofs: Self::Proofs) -> Result<(), Self::Error> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::reclaim_unspent(
+            self.inner().as_ref(),
+            cdk_proofs,
+        )
+        .await?)
+    }
+
+    async fn list_transactions(
+        &self,
+        direction: Option<Self::TransactionDirection>,
+    ) -> Result<Vec<Self::Transaction>, Self::Error> {
+        let cdk_direction = direction.map(Into::into);
+        let transactions = <cdk::wallet::Wallet as CdkWalletTrait>::list_transactions(
+            self.inner().as_ref(),
+            cdk_direction,
+        )
+        .await?;
+        Ok(transactions.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_transaction(
+        &self,
+        id: Self::TransactionId,
+    ) -> Result<Option<Self::Transaction>, Self::Error> {
+        let cdk_id = id.try_into()?;
+        let transaction =
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_transaction(self.inner().as_ref(), cdk_id)
+                .await?;
+        Ok(transaction.map(Into::into))
+    }
+
+    async fn get_proofs_for_transaction(
+        &self,
+        id: Self::TransactionId,
+    ) -> Result<Self::Proofs, Self::Error> {
+        let cdk_id = id.try_into()?;
+        let proofs = <cdk::wallet::Wallet as CdkWalletTrait>::get_proofs_for_transaction(
+            self.inner().as_ref(),
+            cdk_id,
+        )
+        .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn revert_transaction(&self, id: Self::TransactionId) -> Result<(), Self::Error> {
+        let cdk_id = id.try_into()?;
+        <cdk::wallet::Wallet as CdkWalletTrait>::revert_transaction(self.inner().as_ref(), cdk_id)
+            .await?;
+        Ok(())
+    }
+
+    async fn verify_token_dleq(&self, token: &String) -> Result<(), Self::Error> {
+        let cdk_token: cdk::nuts::Token = token.parse().map_err(FfiError::internal)?;
+        <cdk::wallet::Wallet as CdkWalletTrait>::verify_token_dleq(
+            self.inner().as_ref(),
+            &cdk_token,
+        )
+        .await?;
+        Ok(())
+    }
+
+    async fn restore(&self) -> Result<Self::Restored, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::restore(self.inner().as_ref())
+                .await?
+                .into(),
+        )
+    }
+
+    async fn get_keyset_fees(&self, keyset_id: &str) -> Result<u64, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::get_keyset_fees(
+            self.inner().as_ref(),
+            keyset_id,
+        )
+        .await?)
+    }
+
+    async fn calculate_fee(
+        &self,
+        proof_count: u64,
+        keyset_id: &str,
+    ) -> Result<Self::Amount, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::calculate_fee(
+            self.inner().as_ref(),
+            proof_count,
+            keyset_id,
+        )
+        .await?
+        .into())
+    }
+
+    async fn subscribe(
+        &self,
+        params: Self::SubscribeParams,
+    ) -> Result<Self::Subscription, Self::Error> {
+        let cdk_params: cdk::nuts::nut17::Params<Arc<String>> = params.clone().into();
+        let sub_id = cdk_params.id.to_string();
+        let active_sub =
+            <cdk::wallet::Wallet as CdkWalletTrait>::subscribe(self.inner().as_ref(), cdk_params)
+                .await?;
+        Ok(Arc::new(ActiveSubscription::new(active_sub, sub_id)))
+    }
+
+    async fn pay_request(
+        &self,
+        request: Self::PaymentRequest,
+        custom_amount: Option<Self::Amount>,
+    ) -> Result<(), Self::Error> {
+        <cdk::wallet::Wallet as CdkWalletTrait>::pay_request(
+            self.inner().as_ref(),
+            request.inner().clone(),
+            custom_amount.map(Into::into),
+        )
+        .await?;
+        Ok(())
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_bip353_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error> {
+        Ok(<cdk::wallet::Wallet as CdkWalletTrait>::melt_bip353_quote(
+            self.inner().as_ref(),
+            address,
+            amount.into(),
+        )
+        .await?
+        .into())
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_lightning_address_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::melt_lightning_address_quote(
+                self.inner().as_ref(),
+                address,
+                amount.into(),
+            )
+            .await?
+            .into(),
+        )
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_human_readable_quote(
+        &self,
+        address: &str,
+        amount: Self::Amount,
+    ) -> Result<Self::MeltQuote, Self::Error> {
+        Ok(
+            <cdk::wallet::Wallet as CdkWalletTrait>::melt_human_readable_quote(
+                self.inner().as_ref(),
+                address,
+                amount.into(),
+            )
+            .await?
+            .into(),
+        )
+    }
+
+    async fn set_cat(&self, cat: String) -> Result<(), Self::Error> {
+        <cdk::wallet::Wallet as CdkWalletTrait>::set_cat(self.inner().as_ref(), cat).await?;
+        Ok(())
+    }
+
+    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error> {
+        <cdk::wallet::Wallet as CdkWalletTrait>::set_refresh_token(
+            self.inner().as_ref(),
+            refresh_token,
+        )
+        .await?;
+        Ok(())
+    }
+
+    async fn refresh_access_token(&self) -> Result<(), Self::Error> {
+        <cdk::wallet::Wallet as CdkWalletTrait>::refresh_access_token(self.inner().as_ref())
+            .await?;
+        Ok(())
+    }
+
+    async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Self::Proofs, Self::Error> {
+        let proofs = <cdk::wallet::Wallet as CdkWalletTrait>::mint_blind_auth(
+            self.inner().as_ref(),
+            amount.into(),
+        )
+        .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_unspent_auth_proofs(&self) -> Result<Self::Proofs, Self::Error> {
+        let proofs =
+            <cdk::wallet::Wallet as CdkWalletTrait>::get_unspent_auth_proofs(self.inner().as_ref())
+                .await?;
+        Ok(proofs.into_iter().map(Into::into).collect())
+    }
+}

+ 1 - 1
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -27,7 +27,7 @@ use cdk::nuts::{
 };
 use cdk::types::{FeeReserve, QuoteTTL};
 use cdk::util::unix_time;
-use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
+use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder, WalletTrait};
 use cdk::{Amount, Error, Mint, StreamExt};
 use cdk_fake_wallet::FakeWallet;
 use tokio::sync::RwLock;

+ 1 - 1
crates/cdk-integration-tests/src/lib.rs

@@ -26,7 +26,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::{
     MeltQuoteBolt11Response, MeltRequest, MintRequest, MintResponse, PreMintSecrets, Proofs,
 };
-use cdk::wallet::{HttpClient, MintConnector, MintQuote};
+use cdk::wallet::{HttpClient, MintConnector, MintQuote, WalletTrait};
 use cdk::{StreamExt, Wallet};
 use cdk_fake_wallet::create_fake_invoice;
 use init_regtest::{get_lnd_dir, LND_RPC_ADDR};

+ 1 - 1
crates/cdk-integration-tests/tests/async_melt.rs

@@ -15,7 +15,7 @@ use bip39::Mnemonic;
 use cashu::PaymentMethod;
 use cdk::amount::SplitTarget;
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, State};
-use cdk::wallet::{MeltOutcome, Wallet};
+use cdk::wallet::{MeltOutcome, Wallet, WalletTrait};
 use cdk::StreamExt;
 use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
 use cdk_sqlite::wallet::memory;

+ 1 - 1
crates/cdk-integration-tests/tests/bolt12.rs

@@ -11,7 +11,7 @@ use cashu::nut23::Amountless;
 use cashu::{
     Amount, CurrencyUnit, MintRequest, MintUrl, PaymentMethod, PreMintSecrets, ProofsMethods,
 };
-use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletBuilder};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletBuilder, WalletTrait};
 use cdk_integration_tests::get_mint_url_from_env;
 use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
 use cdk_sqlite::wallet::memory;

+ 3 - 1
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -12,7 +12,9 @@ use cdk::nuts::{
     MeltQuoteState, MeltRequest, MintQuoteBolt11Request, MintRequest, PaymentMethod,
     RestoreRequest, State, SwapRequest,
 };
-use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
+use cdk::wallet::{
+    AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder, WalletTrait,
+};
 use cdk::{Error, OidcClient};
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_http_client::HttpClient as CommonHttpClient;

+ 1 - 1
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -26,7 +26,7 @@ use cdk::nuts::{
     SecretKey, State, SwapRequest,
 };
 use cdk::wallet::types::TransactionDirection;
-use cdk::wallet::{HttpClient, MintConnector, Wallet};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletTrait};
 use cdk::StreamExt;
 use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
 use cdk_sqlite::wallet::memory;

+ 1 - 1
crates/cdk-integration-tests/tests/ffi_minting_integration.rs

@@ -20,7 +20,7 @@ use bip39::Mnemonic;
 use cdk_ffi::sqlite::WalletSqliteDatabase;
 use cdk_ffi::types::{encode_mint_quote, Amount, CurrencyUnit, QuoteState, SplitTarget};
 use cdk_ffi::wallet::Wallet as FfiWallet;
-use cdk_ffi::{PaymentMethod, WalletConfig};
+use cdk_ffi::{PaymentMethod, WalletConfig, WalletTrait};
 use cdk_integration_tests::{get_mint_url_from_env, pay_if_regtest};
 use lightning_invoice::Bolt11Invoice;
 use tokio::time::timeout;

+ 1 - 1
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -23,7 +23,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::{KnownMethod, ProofsMethods};
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, PaymentMethod, State};
-use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletRepositoryBuilder};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletRepositoryBuilder, WalletTrait};
 use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;
 use futures::{SinkExt, StreamExt};

+ 1 - 1
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -27,7 +27,7 @@ use cdk::mint::Mint;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::subscription::Params;
 use cdk::wallet::types::{TransactionDirection, TransactionId};
-use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions};
+use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions, WalletTrait};
 use cdk::Amount;
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_integration_tests::init_pure_tests::*;

+ 1 - 1
crates/cdk-integration-tests/tests/regtest.rs

@@ -24,7 +24,7 @@ use cdk::nuts::{
     CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState, MintRequest, Mpp,
     NotificationPayload, PaymentMethod, PreMintSecrets,
 };
-use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription, WalletTrait};
 use cdk_integration_tests::{
     attempt_manual_mint, get_mint_url_from_env, get_second_mint_url_from_env, get_test_client,
 };

+ 1 - 1
crates/cdk-integration-tests/tests/test_fees.rs

@@ -5,7 +5,7 @@ use bip39::Mnemonic;
 use cashu::{Bolt11Invoice, PaymentMethod, ProofsMethods};
 use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::{ReceiveOptions, SendKind, SendOptions, Wallet};
+use cdk::wallet::{ReceiveOptions, SendKind, SendOptions, Wallet, WalletTrait};
 use cdk_integration_tests::init_regtest::get_temp_dir;
 use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;

+ 1 - 0
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -20,6 +20,7 @@ use cashu::{
 };
 use cdk::mint::Mint;
 use cdk::nuts::nut00::ProofsMethods;
+use cdk::wallet::WalletTrait;
 use cdk::Amount;
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_integration_tests::init_pure_tests::*;

+ 1 - 1
crates/cdk-integration-tests/tests/wallet_saga.rs

@@ -11,7 +11,7 @@
 use anyhow::Result;
 use cashu::{MeltQuoteState, PaymentMethod};
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::wallet::SendOptions;
+use cdk::wallet::{SendOptions, WalletTrait};
 use cdk::Amount;
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_integration_tests::init_pure_tests::*;

+ 1 - 1
crates/cdk/README.md

@@ -55,7 +55,7 @@ use cdk::amount::SplitTarget;
 use cdk_sqlite::wallet::memory;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
 #[cfg(feature = "wallet")]
-use cdk::wallet::{RecoveryReport, SendOptions, Wallet};
+use cdk::wallet::{RecoveryReport, SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use rand::random;
 use tokio::time::sleep;

+ 1 - 1
crates/cdk/examples/auth_wallet.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{SendOptions, Wallet};
+use cdk::wallet::{SendOptions, Wallet, WalletTrait};
 use cdk::{Amount, OidcClient};
 use cdk_common::amount::SplitTarget;
 use cdk_common::{MintInfo, ProofsMethods};

+ 1 - 1
crates/cdk/examples/bip353.rs

@@ -27,7 +27,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::Wallet;
+use cdk::wallet::{Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/human_readable_payment.rs

@@ -37,7 +37,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::Wallet;
+use cdk::wallet::{Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/melt-token.rs

@@ -8,7 +8,7 @@ use bitcoin::hex::prelude::FromHex;
 use bitcoin::secp256k1::Secp256k1;
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, PaymentMethod, SecretKey};
-use cdk::wallet::{MeltOutcome, Wallet};
+use cdk::wallet::{MeltOutcome, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};

+ 1 - 1
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs

@@ -7,7 +7,7 @@ use std::time::Duration;
 use cdk::error::Error;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::{BaseHttpClient, HttpTransport, SendOptions, WalletBuilder};
+use cdk::wallet::{BaseHttpClient, HttpTransport, SendOptions, WalletBuilder, WalletTrait};
 use cdk::{Amount, StreamExt};
 use cdk_common::mint_url::MintUrl;
 use cdk_common::{AuthToken, PaymentMethod};

+ 1 - 1
crates/cdk/examples/mint-token-bolt12-with-stream.rs

@@ -5,7 +5,7 @@ use std::sync::Arc;
 use cdk::error::Error;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{SendOptions, Wallet};
+use cdk::wallet::{SendOptions, Wallet, WalletTrait};
 use cdk::{Amount, StreamExt};
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/mint-token-bolt12.rs

@@ -6,7 +6,7 @@ use std::time::Duration;
 use cdk::error::Error;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{SendOptions, Wallet};
+use cdk::wallet::{SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/mint-token.rs

@@ -6,7 +6,7 @@ use std::time::Duration;
 use cdk::error::Error;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{SendOptions, Wallet};
+use cdk::wallet::{SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/npubcash.rs

@@ -20,7 +20,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::Wallet;
+use cdk::wallet::{Wallet, WalletTrait};
 use cdk::StreamExt;
 use cdk_sqlite::wallet::memory;
 use nostr_sdk::{Keys, ToBech32};

+ 1 - 1
crates/cdk/examples/p2pk.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, PaymentMethod, SecretKey, SpendingConditions};
-use cdk::wallet::{ReceiveOptions, SendOptions, Wallet};
+use cdk::wallet::{ReceiveOptions, SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/payment_request.rs

@@ -30,7 +30,7 @@ use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::KnownMethod;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
 use cdk::wallet::payment_request::CreateRequestParams;
-use cdk::wallet::WalletRepositoryBuilder;
+use cdk::wallet::{WalletRepositoryBuilder, WalletTrait};
 use cdk_sqlite::wallet::memory;
 use rand::random;
 

+ 1 - 1
crates/cdk/examples/proof-selection.rs

@@ -6,7 +6,7 @@ use std::time::Duration;
 
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::Wallet;
+use cdk::wallet::{Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_common::nut02::KeySetInfosMethods;
 use cdk_sqlite::wallet::memory;

+ 1 - 1
crates/cdk/examples/receive-token.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{ReceiveOptions, SendOptions, Wallet};
+use cdk::wallet::{ReceiveOptions, SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/restore-wallet.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::Wallet;
+use cdk::wallet::{Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 1
crates/cdk/examples/wallet.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::{RecoveryReport, SendOptions, Wallet};
+use cdk::wallet::{RecoveryReport, SendOptions, Wallet, WalletTrait};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::random;

+ 1 - 34
crates/cdk/src/wallet/balance.rs

@@ -1,34 +1 @@
-use tracing::instrument;
-
-use crate::nuts::nut00::ProofsMethods;
-use crate::nuts::State;
-use crate::{Amount, Error, Wallet};
-
-impl Wallet {
-    /// Total unspent balance of wallet
-    #[instrument(skip(self))]
-    pub async fn total_balance(&self) -> Result<Amount, Error> {
-        // Use the efficient balance query instead of fetching all proofs
-        let balance = self
-            .localstore
-            .get_balance(
-                Some(self.mint_url.clone()),
-                Some(self.unit.clone()),
-                Some(vec![State::Unspent]),
-            )
-            .await?;
-        Ok(Amount::from(balance))
-    }
-
-    /// Total pending balance
-    #[instrument(skip(self))]
-    pub async fn total_pending_balance(&self) -> Result<Amount, Error> {
-        Ok(self.get_pending_proofs().await?.total_amount()?)
-    }
-
-    /// Total reserved balance
-    #[instrument(skip(self))]
-    pub async fn total_reserved_balance(&self) -> Result<Amount, Error> {
-        Ok(self.get_reserved_proofs().await?.total_amount()?)
-    }
-}
+// Balance methods are now implemented via the WalletBalance trait in traits.rs

+ 4 - 95
crates/cdk/src/wallet/issue/mod.rs

@@ -5,112 +5,21 @@
 pub(crate) mod saga;
 
 use cdk_common::nut00::KnownMethod;
-use cdk_common::nut04::MintMethodOptions;
-use cdk_common::nut25::MintQuoteBolt12Request;
+use cdk_common::wallet::Wallet as WalletTrait;
 use cdk_common::PaymentMethod;
 pub(crate) use saga::MintSaga;
 use tracing::instrument;
 
 use crate::amount::SplitTarget;
-use crate::nuts::{
-    MintQuoteBolt11Request, MintQuoteCustomRequest, Proofs, SecretKey, SpendingConditions,
-};
+use crate::nuts::{Proofs, SpendingConditions};
 use crate::util::unix_time;
 use crate::wallet::recovery::RecoveryAction;
 use crate::wallet::{MintQuote, MintQuoteState};
 use crate::{Amount, Error, Wallet};
 
 impl Wallet {
-    /// Mint Quote
-    #[instrument(skip(self, method))]
-    pub async fn mint_quote<T>(
-        &self,
-        method: T,
-        amount: Option<Amount>,
-        description: Option<String>,
-        extra: Option<String>,
-    ) -> Result<MintQuote, Error>
-    where
-        T: Into<PaymentMethod>,
-    {
-        let mint_info = self.load_mint_info().await?;
-        let mint_url = self.mint_url.clone();
-        let unit = self.unit.clone();
-
-        let method: PaymentMethod = method.into();
-
-        // Check settings and description support
-        if description.is_some() {
-            let settings = mint_info
-                .nuts
-                .nut04
-                .get_settings(&unit, &method)
-                .ok_or(Error::UnsupportedUnit)?;
-
-            match settings.options {
-                Some(MintMethodOptions::Bolt11 { description }) if description => (),
-                _ => return Err(Error::InvoiceDescriptionUnsupported),
-            }
-        }
-
-        self.refresh_keysets().await?;
-
-        let secret_key = SecretKey::generate();
-
-        let (quote_id, request_str, expiry) = match &method {
-            PaymentMethod::Known(KnownMethod::Bolt11) => {
-                let amount = amount.ok_or(Error::AmountUndefined)?;
-                let request = MintQuoteBolt11Request {
-                    amount,
-                    unit: unit.clone(),
-                    description,
-                    pubkey: Some(secret_key.public_key()),
-                };
-
-                let response = self.client.post_mint_quote(request).await?;
-                (response.quote, response.request, response.expiry)
-            }
-            PaymentMethod::Known(KnownMethod::Bolt12) => {
-                let request = MintQuoteBolt12Request {
-                    amount,
-                    unit: unit.clone(),
-                    description,
-                    pubkey: secret_key.public_key(),
-                };
-
-                let response = self.client.post_mint_bolt12_quote(request).await?;
-                (response.quote, response.request, response.expiry)
-            }
-            PaymentMethod::Custom(_) => {
-                let amount = amount.ok_or(Error::AmountUndefined)?;
-                let request = MintQuoteCustomRequest {
-                    amount,
-                    unit: unit.clone(),
-                    description,
-                    pubkey: Some(secret_key.public_key()),
-                    extra: serde_json::from_str(&extra.unwrap_or_default())?,
-                };
-
-                let response = self.client.post_mint_custom_quote(&method, request).await?;
-                (response.quote, response.request, response.expiry)
-            }
-        };
-
-        let quote = MintQuote::new(
-            quote_id,
-            mint_url,
-            method.clone(),
-            amount,
-            unit,
-            request_str,
-            expiry.unwrap_or(0),
-            Some(secret_key),
-        );
-
-        self.localstore.add_mint_quote(quote.clone()).await?;
-
-        Ok(quote)
-    }
+    // mint_quote() is now implemented via the Wallet trait in wallet_trait.rs
+    // It routes to Bolt11/Bolt12/Custom based on the PaymentMethod parameter.
 
     /// Checks the state of a mint quote with the mint
     async fn check_state(&self, mint_quote: &mut MintQuote) -> Result<(), Error> {

+ 3 - 49
crates/cdk/src/wallet/keysets.rs

@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
 use cdk_common::amount::{FeeAndAmounts, KeysetFeeAndAmounts};
-use cdk_common::nut02::{KeySetInfos, KeySetInfosMethods};
+use cdk_common::nut02::KeySetInfosMethods;
 use tracing::instrument;
 
 use crate::nuts::{Id, KeySetInfo, Keys};
@@ -64,36 +64,7 @@ impl Wallet {
         }
     }
 
-    /// Refresh keysets by fetching the latest from mint - always fetches fresh data
-    ///
-    /// Forces a fresh fetch of keyset information from the mint server,
-    /// updating the metadata cache and database. Use this when you need
-    /// the most up-to-date keyset information.
-    #[instrument(skip(self))]
-    pub async fn refresh_keysets(&self) -> Result<KeySetInfos, Error> {
-        tracing::debug!("Refreshing keysets from mint");
-
-        let keysets = self
-            .metadata_cache
-            .load_from_mint(&self.localstore, &self.client)
-            .await?
-            .keysets
-            .iter()
-            .filter_map(|(_, keyset)| {
-                if keyset.unit == self.unit && keyset.active {
-                    Some((*keyset.clone()).clone())
-                } else {
-                    None
-                }
-            })
-            .collect::<Vec<_>>();
-
-        if !keysets.is_empty() {
-            Ok(keysets)
-        } else {
-            Err(Error::UnknownKeySet)
-        }
-    }
+    // refresh_keysets() is now implemented via the WalletMintInfo trait in traits.rs
 
     /// Get the active keyset with the lowest fees - fetches fresh data from mint
     ///
@@ -110,24 +81,7 @@ impl Wallet {
             .ok_or(Error::NoActiveKeyset)
     }
 
-    /// Get the active keyset with the lowest fees from cache
-    ///
-    /// Returns the active keyset with minimum input fees from the metadata cache.
-    /// Uses cached data if available, fetches from mint if cache not populated.
-    #[instrument(skip(self))]
-    pub async fn get_active_keyset(&self) -> Result<KeySetInfo, Error> {
-        self.metadata_cache
-            .load(&self.localstore, &self.client, {
-                let ttl = self.metadata_cache_ttl.read();
-                *ttl
-            })
-            .await?
-            .active_keysets
-            .iter()
-            .min_by_key(|k| k.input_fee_ppk)
-            .map(|ks| (**ks).clone())
-            .ok_or(Error::NoActiveKeyset)
-    }
+    // get_active_keyset() is now implemented via the WalletMintInfo trait in traits.rs
 
     /// Get keyset fees and amounts for all keysets from metadata cache
     ///

+ 1 - 1
crates/cdk/src/wallet/melt/custom.rs

@@ -1,4 +1,4 @@
-use cdk_common::wallet::MeltQuote;
+use cdk_common::wallet::{MeltQuote, Wallet as WalletTrait};
 use cdk_common::PaymentMethod;
 use tracing::instrument;
 

+ 1 - 1
crates/cdk/src/wallet/melt/saga/mod.rs

@@ -38,7 +38,7 @@ use cdk_common::amount::SplitTarget;
 use cdk_common::dhke::construct_proofs;
 use cdk_common::wallet::{
     MeltOperationData, MeltQuote, MeltSagaState, OperationData, ProofInfo, Transaction,
-    TransactionDirection, WalletSaga, WalletSagaState,
+    TransactionDirection, Wallet as WalletTrait, WalletSaga, WalletSagaState,
 };
 use cdk_common::MeltQuoteState;
 use tracing::instrument;

+ 8 - 94
crates/cdk/src/wallet/mod.rs

@@ -28,12 +28,11 @@ use crate::mint_url::MintUrl;
 use crate::nuts::nut00::token::Token;
 use crate::nuts::nut17::Kind;
 use crate::nuts::{
-    nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proofs,
-    RestoreRequest, SpendingConditions, State,
+    nut10, CurrencyUnit, Id, Keys, MintQuoteState, PreMintSecrets, Proofs, RestoreRequest,
+    SpendingConditions, State,
 };
-use crate::util::unix_time;
 use crate::wallet::mint_metadata_cache::MintMetadataCache;
-use crate::{Amount, OidcClient};
+use crate::Amount;
 
 mod auth;
 #[cfg(feature = "nostr")]
@@ -64,10 +63,12 @@ pub mod test_utils;
 mod transactions;
 pub mod util;
 pub mod wallet_repository;
+mod wallet_trait;
 
 pub use auth::{AuthMintConnector, AuthWallet};
 pub use builder::WalletBuilder;
 pub use cdk_common::wallet as types;
+pub use cdk_common::wallet::Wallet as WalletTrait;
 pub use melt::{MeltConfirmOptions, MeltOutcome, PendingMelt, PreparedMelt};
 pub use mint_connector::transport::Transport as HttpTransport;
 pub use mint_connector::{
@@ -194,7 +195,7 @@ impl From<WalletSubscription> for WalletParams {
 }
 
 /// Amount that are recovered during restore operation
-#[derive(Debug, Hash, PartialEq, Eq, Default)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
 pub struct Restored {
     /// Amount in the restore that has already been spent
     pub spent: Amount,
@@ -329,95 +330,8 @@ impl Wallet {
         Ok(())
     }
 
-    /// Query mint for current mint information
-    #[instrument(skip(self))]
-    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
-        let mint_info = self
-            .metadata_cache
-            .load_from_mint(&self.localstore, &self.client)
-            .await?
-            .mint_info
-            .clone();
-
-        // If mint provides time make sure it is accurate
-        if let Some(mint_unix_time) = mint_info.time {
-            let current_unix_time = unix_time();
-            if current_unix_time.abs_diff(mint_unix_time) > 30 {
-                tracing::warn!(
-                    "Mint time does match wallet time. Mint: {}, Wallet: {}",
-                    mint_unix_time,
-                    current_unix_time
-                );
-                return Err(Error::MintTimeExceedsTolerance);
-            }
-        }
-
-        // Create or update auth wallet
-        {
-            let mut auth_wallet = self.auth_wallet.write().await;
-            match &*auth_wallet {
-                Some(auth_wallet) => {
-                    let mut protected_endpoints = auth_wallet.protected_endpoints.write().await;
-                    *protected_endpoints = mint_info.protected_endpoints();
-
-                    if let Some(oidc_client) = mint_info
-                        .openid_discovery()
-                        .map(|url| OidcClient::new(url, None))
-                    {
-                        auth_wallet.set_oidc_client(Some(oidc_client)).await;
-                    }
-                }
-                None => {
-                    tracing::info!("Mint has auth enabled creating auth wallet");
-
-                    let oidc_client = mint_info
-                        .openid_discovery()
-                        .map(|url| OidcClient::new(url, None));
-                    let new_auth_wallet = AuthWallet::new(
-                        self.mint_url.clone(),
-                        None,
-                        self.localstore.clone(),
-                        self.metadata_cache.clone(),
-                        mint_info.protected_endpoints(),
-                        oidc_client,
-                    );
-                    *auth_wallet = Some(new_auth_wallet.clone());
-
-                    self.client
-                        .set_auth_wallet(Some(new_auth_wallet.clone()))
-                        .await;
-
-                    if let Err(e) = new_auth_wallet.refresh_keysets().await {
-                        tracing::error!("Could not fetch auth keysets: {}", e);
-                    }
-                }
-            }
-        }
-
-        tracing::trace!("Mint info updated for {}", self.mint_url);
-
-        Ok(Some(mint_info))
-    }
-
-    /// Load mint info from cache
-    ///
-    /// This is a helper function that loads the mint info from the metadata cache
-    /// using the configured TTL. Unlike `fetch_mint_info()`, this does not make
-    /// a network call if the cache is fresh.
-    #[instrument(skip(self))]
-    pub async fn load_mint_info(&self) -> Result<MintInfo, Error> {
-        let mint_info = self
-            .metadata_cache
-            .load(&self.localstore, &self.client, {
-                let ttl = self.metadata_cache_ttl.read();
-                *ttl
-            })
-            .await?
-            .mint_info
-            .clone();
-
-        Ok(mint_info)
-    }
+    // fetch_mint_info() and load_mint_info() are now implemented via the
+    // WalletMintInfo trait in traits.rs
 
     /// Get amounts needed to refill proof state
     #[instrument(skip(self))]

+ 1 - 1
crates/cdk/src/wallet/receive/saga/mod.rs

@@ -39,7 +39,7 @@ use bitcoin::XOnlyPublicKey;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{
     OperationData, ProofInfo, ReceiveOperationData, ReceiveSagaState, Transaction,
-    TransactionDirection, WalletSaga, WalletSagaState,
+    TransactionDirection, Wallet as WalletTrait, WalletSaga, WalletSagaState,
 };
 use tracing::instrument;
 

+ 2 - 2
crates/cdk/src/wallet/send/saga/mod.rs

@@ -66,8 +66,8 @@ use std::collections::HashMap;
 use cdk_common::nut02::KeySetInfosMethods;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{
-    OperationData, SendOperationData, SendSagaState, Transaction, TransactionDirection, WalletSaga,
-    WalletSagaState,
+    OperationData, SendOperationData, SendSagaState, Transaction, TransactionDirection,
+    Wallet as WalletTrait, WalletSaga, WalletSagaState,
 };
 use cdk_common::Id;
 use tracing::instrument;

+ 1 - 1
crates/cdk/src/wallet/wallet_repository.rs

@@ -9,7 +9,7 @@ use std::sync::Arc;
 
 use cdk_common::database;
 use cdk_common::database::WalletDatabase;
-use cdk_common::wallet::WalletKey;
+use cdk_common::wallet::{Wallet as WalletTrait, WalletKey};
 use tokio::sync::RwLock;
 use tracing::instrument;
 use zeroize::Zeroize;

+ 535 - 0
crates/cdk/src/wallet/wallet_trait.rs

@@ -0,0 +1,535 @@
+//! Wallet trait implementations for [`Wallet`].
+//!
+//! Implements the unified wallet trait defined in `cdk_common::wallet`.
+
+use std::str::FromStr;
+
+use cdk_common::nut00::KnownMethod;
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::wallet::{Transaction, TransactionDirection, TransactionId, Wallet as WalletTrait};
+use cdk_common::CurrencyUnit;
+
+use crate::amount::SplitTarget;
+use crate::mint_url::MintUrl;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    Id, KeySetInfo, MeltOptions, MintInfo, MintQuoteBolt11Request, MintQuoteBolt12Request,
+    MintQuoteCustomRequest, PaymentMethod, PaymentRequest, Proof, Proofs, SecretKey,
+    SpendingConditions, State, Token,
+};
+use crate::types::FinalizedMelt;
+use crate::util::unix_time;
+use crate::wallet::receive::ReceiveOptions;
+use crate::wallet::send::SendOptions;
+use crate::wallet::subscription::ActiveSubscription;
+use crate::wallet::{MeltQuote, MintQuote, Restored, Wallet};
+use crate::{Amount, Error, OidcClient};
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl WalletTrait for Wallet {
+    type Amount = Amount;
+    type Proofs = Proofs;
+    type Proof = Proof;
+    type MintQuote = MintQuote;
+    type MeltQuote = MeltQuote;
+    type MeltResult = FinalizedMelt;
+    type Token = Token;
+    type CurrencyUnit = CurrencyUnit;
+    type MintUrl = MintUrl;
+    type MintInfo = MintInfo;
+    type KeySetInfo = KeySetInfo;
+    type Error = Error;
+    type SendOptions = SendOptions;
+    type ReceiveOptions = ReceiveOptions;
+    type SpendingConditions = SpendingConditions;
+    type SplitTarget = SplitTarget;
+    type PaymentMethod = PaymentMethod;
+    type MeltOptions = MeltOptions;
+    type Restored = Restored;
+    type Transaction = Transaction;
+    type TransactionId = TransactionId;
+    type TransactionDirection = TransactionDirection;
+    type PaymentRequest = PaymentRequest;
+    type Subscription = ActiveSubscription;
+    type SubscribeParams = cdk_common::subscription::WalletParams;
+
+    fn mint_url(&self) -> Self::MintUrl {
+        self.mint_url.clone()
+    }
+
+    fn unit(&self) -> Self::CurrencyUnit {
+        self.unit.clone()
+    }
+
+    async fn total_balance(&self) -> Result<Amount, Error> {
+        let balance = self
+            .localstore
+            .get_balance(
+                Some(self.mint_url.clone()),
+                Some(self.unit.clone()),
+                Some(vec![State::Unspent]),
+            )
+            .await?;
+        Ok(Amount::from(balance))
+    }
+
+    async fn total_pending_balance(&self) -> Result<Amount, Error> {
+        Ok(self.get_pending_proofs().await?.total_amount()?)
+    }
+
+    async fn total_reserved_balance(&self) -> Result<Amount, Error> {
+        Ok(self.get_reserved_proofs().await?.total_amount()?)
+    }
+
+    async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
+        let mint_info = self
+            .metadata_cache
+            .load_from_mint(&self.localstore, &self.client)
+            .await?
+            .mint_info
+            .clone();
+
+        // If mint provides time make sure it is accurate
+        if let Some(mint_unix_time) = mint_info.time {
+            let current_unix_time = unix_time();
+            if current_unix_time.abs_diff(mint_unix_time) > 30 {
+                tracing::warn!(
+                    "Mint time does match wallet time. Mint: {}, Wallet: {}",
+                    mint_unix_time,
+                    current_unix_time
+                );
+                return Err(Error::MintTimeExceedsTolerance);
+            }
+        }
+
+        // Create or update auth wallet
+        {
+            use crate::wallet::auth::AuthWallet;
+
+            let mut auth_wallet = self.auth_wallet.write().await;
+            match &*auth_wallet {
+                Some(auth_wallet) => {
+                    let mut protected_endpoints = auth_wallet.protected_endpoints.write().await;
+                    *protected_endpoints = mint_info.protected_endpoints();
+
+                    if let Some(oidc_client) = mint_info
+                        .openid_discovery()
+                        .map(|url| OidcClient::new(url, None))
+                    {
+                        auth_wallet.set_oidc_client(Some(oidc_client)).await;
+                    }
+                }
+                None => {
+                    tracing::info!("Mint has auth enabled creating auth wallet");
+
+                    let oidc_client = mint_info
+                        .openid_discovery()
+                        .map(|url| OidcClient::new(url, None));
+                    let new_auth_wallet = AuthWallet::new(
+                        self.mint_url.clone(),
+                        None,
+                        self.localstore.clone(),
+                        self.metadata_cache.clone(),
+                        mint_info.protected_endpoints(),
+                        oidc_client,
+                    );
+                    *auth_wallet = Some(new_auth_wallet.clone());
+
+                    self.client
+                        .set_auth_wallet(Some(new_auth_wallet.clone()))
+                        .await;
+
+                    if let Err(e) = new_auth_wallet.refresh_keysets().await {
+                        tracing::error!("Could not fetch auth keysets: {}", e);
+                    }
+                }
+            }
+        }
+
+        tracing::trace!("Mint info updated for {}", self.mint_url);
+
+        Ok(Some(mint_info))
+    }
+
+    async fn load_mint_info(&self) -> Result<MintInfo, Error> {
+        let mint_info = self
+            .metadata_cache
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.read();
+                *ttl
+            })
+            .await?
+            .mint_info
+            .clone();
+
+        Ok(mint_info)
+    }
+
+    async fn get_active_keyset(&self) -> Result<KeySetInfo, Error> {
+        self.metadata_cache
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.read();
+                *ttl
+            })
+            .await?
+            .active_keysets
+            .iter()
+            .min_by_key(|k| k.input_fee_ppk)
+            .map(|ks| (**ks).clone())
+            .ok_or(Error::NoActiveKeyset)
+    }
+
+    async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+        tracing::debug!("Refreshing keysets from mint");
+
+        let keysets = self
+            .metadata_cache
+            .load_from_mint(&self.localstore, &self.client)
+            .await?
+            .keysets
+            .iter()
+            .filter_map(|(_, keyset)| {
+                if keyset.unit == self.unit && keyset.active {
+                    Some((*keyset.clone()).clone())
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+
+        if !keysets.is_empty() {
+            Ok(keysets)
+        } else {
+            Err(Error::UnknownKeySet)
+        }
+    }
+
+    async fn mint_quote(
+        &self,
+        method: PaymentMethod,
+        amount: Option<Amount>,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        let mint_info = WalletTrait::load_mint_info(self).await?;
+        let mint_url = self.mint_url.clone();
+        let unit = self.unit.clone();
+
+        // Check settings and description support
+        if description.is_some() {
+            let settings = mint_info
+                .nuts
+                .nut04
+                .get_settings(&unit, &method)
+                .ok_or(Error::UnsupportedUnit)?;
+
+            match settings.options {
+                Some(MintMethodOptions::Bolt11 {
+                    description: true, ..
+                }) => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
+        }
+
+        WalletTrait::refresh_keysets(self).await?;
+
+        let secret_key = SecretKey::generate();
+
+        match method {
+            PaymentMethod::Known(KnownMethod::Bolt11) => {
+                let bolt11_amount =
+                    amount.ok_or(Error::Custom("Amount is required for Bolt11".to_string()))?;
+
+                let request = MintQuoteBolt11Request {
+                    amount: bolt11_amount,
+                    unit: unit.clone(),
+                    description,
+                    pubkey: Some(secret_key.public_key()),
+                };
+
+                let response = self.client.post_mint_quote(request).await?;
+
+                let quote = MintQuote::new(
+                    response.quote,
+                    mint_url,
+                    method,
+                    Some(bolt11_amount),
+                    unit,
+                    response.request,
+                    response.expiry.unwrap_or(0),
+                    Some(secret_key),
+                );
+
+                self.localstore.add_mint_quote(quote.clone()).await?;
+                Ok(quote)
+            }
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
+                let request = MintQuoteBolt12Request {
+                    amount,
+                    unit: unit.clone(),
+                    description,
+                    pubkey: secret_key.public_key(),
+                };
+
+                let response = self.client.post_mint_bolt12_quote(request).await?;
+
+                let quote = MintQuote::new(
+                    response.quote,
+                    mint_url,
+                    method,
+                    response.amount,
+                    unit,
+                    response.request,
+                    response.expiry.unwrap_or(0),
+                    Some(secret_key),
+                );
+
+                self.localstore.add_mint_quote(quote.clone()).await?;
+                Ok(quote)
+            }
+            PaymentMethod::Custom(_) => {
+                let custom_amount =
+                    amount.ok_or(Error::Custom("Amount is required for Custom".to_string()))?;
+
+                let extra_json = extra
+                    .and_then(|s| serde_json::from_str(&s).ok())
+                    .unwrap_or(serde_json::Value::Null);
+
+                let request = MintQuoteCustomRequest {
+                    amount: custom_amount,
+                    unit: unit.clone(),
+                    description,
+                    pubkey: Some(secret_key.public_key()),
+                    extra: extra_json,
+                };
+
+                let response = self.client.post_mint_custom_quote(&method, request).await?;
+
+                let quote = MintQuote::new(
+                    response.quote,
+                    mint_url,
+                    method,
+                    response.amount,
+                    unit,
+                    response.request,
+                    response.expiry.unwrap_or(0),
+                    Some(secret_key),
+                );
+
+                self.localstore.add_mint_quote(quote.clone()).await?;
+                Ok(quote)
+            }
+        }
+    }
+
+    async fn refresh_mint_quote(&self, quote_id: &str) -> Result<MintQuote, Error> {
+        Wallet::refresh_mint_quote_status(self, quote_id).await
+    }
+
+    async fn melt_quote(
+        &self,
+        method: PaymentMethod,
+        request: String,
+        options: Option<MeltOptions>,
+        extra: Option<String>,
+    ) -> Result<MeltQuote, Error> {
+        Wallet::melt_quote::<PaymentMethod, _>(self, method, request, options, extra).await
+    }
+
+    async fn send(&self, amount: Amount, options: SendOptions) -> Result<Token, Error> {
+        let memo = options.memo.clone();
+        let prepared = self.prepare_send(amount, options).await?;
+        prepared.confirm(memo).await
+    }
+
+    async fn get_pending_sends(&self) -> Result<Vec<String>, Error> {
+        let uuids = Wallet::get_pending_sends(self).await?;
+        Ok(uuids.into_iter().map(|id| id.to_string()).collect())
+    }
+
+    async fn revoke_send(&self, operation_id: &str) -> Result<Amount, Error> {
+        let uuid = uuid::Uuid::parse_str(operation_id)
+            .map_err(|e| Error::Custom(format!("Invalid operation ID: {}", e)))?;
+        Wallet::revoke_send(self, uuid).await
+    }
+
+    async fn check_send_status(&self, operation_id: &str) -> Result<bool, Error> {
+        let uuid = uuid::Uuid::parse_str(operation_id)
+            .map_err(|e| Error::Custom(format!("Invalid operation ID: {}", e)))?;
+        Wallet::check_send_status(self, uuid).await
+    }
+
+    async fn receive(&self, encoded_token: &str, options: ReceiveOptions) -> Result<Amount, Error> {
+        Wallet::receive(self, encoded_token, options).await
+    }
+
+    async fn receive_proofs(
+        &self,
+        proofs: Proofs,
+        options: ReceiveOptions,
+        memo: Option<String>,
+        token: Option<String>,
+    ) -> Result<Amount, Error> {
+        Wallet::receive_proofs(self, proofs, options, memo, token).await
+    }
+
+    async fn swap(
+        &self,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        input_proofs: Proofs,
+        spending_conditions: Option<SpendingConditions>,
+        include_fees: bool,
+    ) -> Result<Option<Proofs>, Error> {
+        Wallet::swap(
+            self,
+            amount,
+            amount_split_target,
+            input_proofs,
+            spending_conditions,
+            include_fees,
+        )
+        .await
+    }
+
+    async fn get_unspent_proofs(&self) -> Result<Proofs, Error> {
+        Wallet::get_unspent_proofs(self).await
+    }
+
+    async fn get_pending_proofs(&self) -> Result<Proofs, Error> {
+        Wallet::get_pending_proofs(self).await
+    }
+
+    async fn get_reserved_proofs(&self) -> Result<Proofs, Error> {
+        Wallet::get_reserved_proofs(self).await
+    }
+
+    async fn get_pending_spent_proofs(&self) -> Result<Proofs, Error> {
+        Wallet::get_pending_spent_proofs(self).await
+    }
+
+    async fn check_all_pending_proofs(&self) -> Result<Amount, Error> {
+        Wallet::check_all_pending_proofs(self).await
+    }
+
+    async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, Error> {
+        let states = Wallet::check_proofs_spent(self, proofs).await?;
+        Ok(states
+            .into_iter()
+            .map(|ps| matches!(ps.state, State::Spent | State::PendingSpent))
+            .collect())
+    }
+
+    async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> {
+        let states = Wallet::check_proofs_spent(self, proofs.clone()).await?;
+        let unspent_proofs: Proofs = proofs
+            .into_iter()
+            .zip(states.iter())
+            .filter(|(_, ps)| !matches!(ps.state, State::Spent | State::PendingSpent))
+            .map(|(p, _)| p)
+            .collect();
+
+        if !unspent_proofs.is_empty() {
+            self.swap(None, SplitTarget::default(), unspent_proofs, None, false)
+                .await?;
+        }
+        Ok(())
+    }
+
+    async fn list_transactions(
+        &self,
+        direction: Option<TransactionDirection>,
+    ) -> Result<Vec<Transaction>, Error> {
+        Wallet::list_transactions(self, direction).await
+    }
+
+    async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Error> {
+        Wallet::get_transaction(self, id).await
+    }
+
+    async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Error> {
+        Wallet::get_proofs_for_transaction(self, id).await
+    }
+
+    async fn revert_transaction(&self, id: TransactionId) -> Result<(), Error> {
+        Wallet::revert_transaction(self, id).await
+    }
+
+    async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
+        Wallet::verify_token_dleq(self, token).await
+    }
+
+    async fn restore(&self) -> Result<Restored, Error> {
+        Wallet::restore(self).await
+    }
+
+    async fn get_keyset_fees(&self, keyset_id: &str) -> Result<u64, Error> {
+        let id = Id::from_str(keyset_id)?;
+        Ok(self.get_keyset_fees_and_amounts_by_id(id).await?.fee())
+    }
+
+    async fn calculate_fee(&self, proof_count: u64, keyset_id: &str) -> Result<Amount, Error> {
+        let id = Id::from_str(keyset_id)?;
+        Wallet::get_keyset_count_fee(self, &id, proof_count).await
+    }
+
+    async fn subscribe(
+        &self,
+        params: cdk_common::subscription::WalletParams,
+    ) -> Result<ActiveSubscription, Error> {
+        Wallet::subscribe(self, params).await
+    }
+
+    async fn pay_request(
+        &self,
+        request: PaymentRequest,
+        custom_amount: Option<Amount>,
+    ) -> Result<(), Error> {
+        Wallet::pay_request(self, request, custom_amount).await
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_bip353_quote(&self, address: &str, amount: Amount) -> Result<MeltQuote, Error> {
+        Wallet::melt_bip353_quote(self, address, amount).await
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_lightning_address_quote(
+        &self,
+        address: &str,
+        amount: Amount,
+    ) -> Result<MeltQuote, Error> {
+        Wallet::melt_lightning_address_quote(self, address, amount).await
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    async fn melt_human_readable_quote(
+        &self,
+        address: &str,
+        amount: Amount,
+    ) -> Result<MeltQuote, Error> {
+        Wallet::melt_human_readable_quote(self, address, amount).await
+    }
+
+    async fn set_cat(&self, cat: String) -> Result<(), Error> {
+        Wallet::set_cat(self, cat).await
+    }
+
+    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Error> {
+        Wallet::set_refresh_token(self, refresh_token).await
+    }
+
+    async fn refresh_access_token(&self) -> Result<(), Error> {
+        Wallet::refresh_access_token(self).await
+    }
+
+    async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, Error> {
+        Wallet::mint_blind_auth(self, amount).await
+    }
+
+    async fn get_unspent_auth_proofs(&self) -> Result<Proofs, Error> {
+        let auth_proofs = Wallet::get_unspent_auth_proofs(self).await?;
+        Ok(auth_proofs.into_iter().map(|ap| ap.into()).collect())
+    }
+}