Skip to content
57 changes: 35 additions & 22 deletions crates/cdk-common/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use bitcoin::hashes::{sha256, Hash, HashEngine};
use cashu::amount::{FeeAndAmounts, KeysetFeeAndAmounts, SplitTarget};
use cashu::nuts::nut07::ProofState;
use cashu::nuts::nut18::PaymentRequest;
use cashu::nuts::{AuthProof, Keys};
use cashu::nuts::AuthProof;
use cashu::util::hex;
use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -773,6 +773,24 @@ pub enum KeysetFilter {
All,
}

/// Policy controlling how keysets are loaded.
///
/// Determines the data-fetching strategy for keyset queries:
/// memory cache, local database, and/or network.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum KeysetLoadPolicy {
/// Use in-memory cache and local database only. Never contacts the network.
/// Returns an error if neither cache nor database has data.
CacheOnly,
/// Check cache first (respects TTL). Falls back to database, then network
/// only when data is stale or absent. This is the default.
#[default]
CacheThenNetwork,
/// Always fetch fresh data from the mint over the network.
/// Updates cache and database with the result.
Refresh,
}

/// Unified wallet trait providing a common interface for wallet operations.
///
/// This trait abstracts over different wallet implementations (CDK wallet, FFI
Expand Down Expand Up @@ -842,28 +860,23 @@ pub trait Wallet: Send + Sync {
/// Load mint info (from cache if fresh, otherwise fetches)
async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;

/// Refresh keysets from the mint (always fetches fresh data)
async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;

/// Get the active keyset with lowest fees
async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;

/// Load keys for a specific keyset from cache or mint
async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Self::Error>;

/// Get keysets for this wallet's unit, filtered by active/all
async fn get_mint_keysets(
&self,
filter: KeysetFilter,
) -> Result<Vec<Self::KeySetInfo>, Self::Error>;

/// Load active keysets (alias for get_mint_keysets with Active filter)
async fn load_mint_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
self.get_mint_keysets(KeysetFilter::Active).await
}
/// Get all keysets for this wallet's unit.
///
/// The `policy` parameter controls the fetching strategy:
/// - [`CacheOnly`](KeysetLoadPolicy::CacheOnly) — in-memory cache + local DB, no network
/// - [`CacheThenNetwork`](KeysetLoadPolicy::CacheThenNetwork) — cache first, network if stale (default)
/// - [`Refresh`](KeysetLoadPolicy::Refresh) — always fetch from the mint
async fn keysets(&self, policy: KeysetLoadPolicy)
-> Result<Vec<Self::KeySetInfo>, Self::Error>;

/// Get the active keyset with the lowest fees.
///
/// Filters the output of [`keysets()`](Self::keysets) for active keysets
/// and returns the one with the minimum `input_fee_ppk`.
async fn active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;

/// Fetch the active keyset with lowest fees
async fn fetch_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
/// Get a single keyset by ID.
async fn keyset(&self, keyset_id: Id) -> Result<Self::KeySetInfo, Self::Error>;

/// Get fees and available amounts for all keysets
async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
Expand Down
18 changes: 0 additions & 18 deletions crates/cdk-ffi/src/types/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,6 @@ pub fn encode_key_set_info(info: KeySetInfo) -> Result<String, FfiError> {
Ok(serde_json::to_string(&info)?)
}

/// FFI-compatible KeysetFilter
#[derive(Debug, Clone, Copy, uniffi::Enum)]
pub enum KeysetFilter {
/// Only return active keysets
Active,
/// Return all keysets (active and inactive)
All,
}

impl From<KeysetFilter> for cdk_common::wallet::KeysetFilter {
fn from(filter: KeysetFilter) -> Self {
match filter {
KeysetFilter::Active => cdk_common::wallet::KeysetFilter::Active,
KeysetFilter::All => cdk_common::wallet::KeysetFilter::All,
}
}
}

/// FFI-compatible PublicKey
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
#[serde(transparent)]
Expand Down
38 changes: 38 additions & 0 deletions crates/cdk-ffi/src/types/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ impl From<cdk::wallet::SendKind> for SendKind {
}
}

/// Policy controlling how keysets are loaded
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum, Default,
)]
pub enum KeysetLoadPolicy {
/// Use in-memory cache and local database only. Never contacts the network.
CacheOnly,
/// Check cache first (respects TTL). Falls back to database, then network.
#[default]
CacheThenNetwork,
/// Always fetch fresh data from the mint over the network.
Refresh,
}

impl From<KeysetLoadPolicy> for cdk_common::wallet::KeysetLoadPolicy {
fn from(policy: KeysetLoadPolicy) -> Self {
match policy {
KeysetLoadPolicy::CacheOnly => cdk_common::wallet::KeysetLoadPolicy::CacheOnly,
KeysetLoadPolicy::CacheThenNetwork => {
cdk_common::wallet::KeysetLoadPolicy::CacheThenNetwork
}
KeysetLoadPolicy::Refresh => cdk_common::wallet::KeysetLoadPolicy::Refresh,
}
}
}

impl From<cdk_common::wallet::KeysetLoadPolicy> for KeysetLoadPolicy {
fn from(policy: cdk_common::wallet::KeysetLoadPolicy) -> Self {
match policy {
cdk_common::wallet::KeysetLoadPolicy::CacheOnly => KeysetLoadPolicy::CacheOnly,
cdk_common::wallet::KeysetLoadPolicy::CacheThenNetwork => {
KeysetLoadPolicy::CacheThenNetwork
}
cdk_common::wallet::KeysetLoadPolicy::Refresh => KeysetLoadPolicy::Refresh,
}
}
}

/// FFI-compatible P2PK locked proof send mode
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum, Default,
Expand Down
51 changes: 17 additions & 34 deletions crates/cdk-ffi/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,50 +603,33 @@ impl Wallet {
)))
}

/// Refresh keysets from the mint
pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
let keysets = self.inner.refresh_keysets().await?;
/// Get all keysets for this wallet's unit
pub async fn keysets(
&self,
policy: Option<crate::types::wallet::KeysetLoadPolicy>,
) -> Result<Vec<KeySet>, FfiError> {
let cdk_policy: cdk_common::wallet::KeysetLoadPolicy = policy.unwrap_or_default().into();
let keysets = self.inner.keysets(cdk_policy).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?;
/// Get the active keyset with the lowest fees
pub async fn active_keyset(&self) -> Result<KeySet, FfiError> {
let keyset = self.inner.active_keyset().await?;
Ok(keyset.into())
}

/// Get fees for a specific keyset ID
pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
/// Get a single keyset by ID
pub async fn keyset(&self, keyset_id: String) -> Result<KeySet, FfiError> {
let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
Ok(self.inner.get_keyset_fees_by_id(id).await?)
let keyset = self.inner.keyset(id).await?;
Ok(keyset.into())
}

/// Load keys for a specific keyset
pub async fn load_keyset_keys(&self, keyset_id: String) -> Result<Keys, FfiError> {
/// Get fees for a specific keyset ID
pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
let keys = self.inner.load_keyset_keys(id).await?;
Ok(keys.into())
}

/// Get keysets for this wallet's unit with filter
pub async fn get_mint_keysets(
&self,
filter: KeysetFilter,
) -> Result<Vec<KeySetInfo>, FfiError> {
let keysets = self.inner.get_mint_keysets(filter.into()).await?;
Ok(keysets.into_iter().map(Into::into).collect())
}

/// Load active keysets
pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
let keysets = self.inner.load_mint_keysets().await?;
Ok(keysets.into_iter().map(Into::into).collect())
}

/// Fetch active keyset with lowest fees
pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
let keyset = self.inner.fetch_active_keyset().await?;
Ok(keyset.into())
Ok(self.inner.get_keyset_fees_by_id(id).await?)
}

/// Get fees and amounts for all keysets
Expand Down
32 changes: 11 additions & 21 deletions crates/cdk-ffi/src/wallet_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl WalletTraitDef for Wallet {
type MintUrl = MintUrl;
type CurrencyUnit = CurrencyUnit;
type MintInfo = MintInfo;
type KeySetInfo = KeySetInfo;
type KeySetInfo = KeySet;
type MintQuote = MintQuote;
type MeltQuote = MeltQuote;
type PaymentMethod = PaymentMethod;
Expand Down Expand Up @@ -68,34 +68,24 @@ impl WalletTraitDef for Wallet {
Ok(info.into())
}

async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
let keysets = WalletTraitDef::refresh_keysets(self.inner().as_ref()).await?;
async fn keysets(
&self,
policy: cdk_common::wallet::KeysetLoadPolicy,
) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
let keysets = WalletTraitDef::keysets(self.inner().as_ref(), policy).await?;
Ok(keysets.into_iter().map(Into::into).collect())
}

async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
let keyset = WalletTraitDef::get_active_keyset(self.inner().as_ref()).await?;
async fn active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
let keyset = WalletTraitDef::active_keyset(self.inner().as_ref()).await?;
Ok(keyset.into())
}

async fn load_keyset_keys(
async fn keyset(
&self,
keyset_id: cdk_common::nuts::Id,
) -> Result<cdk_common::nuts::Keys, Self::Error> {
let keys = WalletTraitDef::load_keyset_keys(self.inner().as_ref(), keyset_id).await?;
Ok(keys)
}

async fn get_mint_keysets(
&self,
filter: cdk_common::wallet::KeysetFilter,
) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
let keysets = WalletTraitDef::get_mint_keysets(self.inner().as_ref(), filter).await?;
Ok(keysets.into_iter().map(Into::into).collect())
}

async fn fetch_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error> {
let keyset = WalletTraitDef::fetch_active_keyset(self.inner().as_ref()).await?;
) -> Result<Self::KeySetInfo, Self::Error> {
let keyset = WalletTraitDef::keyset(self.inner().as_ref(), keyset_id).await?;
Ok(keyset.into())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/cdk-integration-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ pub async fn attempt_manual_mint(
mint_amount: Amount,
payment_method: PaymentMethod,
) -> Result<MintResponse, cdk::Error> {
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let active_keyset_id = wallet.active_keyset().await.unwrap().id;
let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
let http_client = HttpClient::new(mint_url.parse().unwrap(), None);

Expand Down
2 changes: 1 addition & 1 deletion crates/cdk-integration-tests/tests/bolt12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
assert_eq!(state.amount_paid, Amount::ZERO);
assert_eq!(state.amount_issued, Amount::ZERO);

let active_keyset_id = wallet.fetch_active_keyset().await?.id;
let active_keyset_id = wallet.active_keyset().await?.id;

let pay_amount_msats = 10_000;

Expand Down
Loading