cdk-ffi 0.17.5

FFI bindings for cdk wallet
Documentation
//! FFI WalletRepository bindings

use std::collections::HashMap;
use std::sync::Arc;

use bip39::Mnemonic;
use cdk::wallet::wallet_repository::{
    WalletRepository as CdkWalletRepository, WalletRepositoryBuilder,
};

use crate::error::FfiError;
use crate::types::*;

/// FFI-compatible WalletRepository
#[derive(uniffi::Object)]
pub struct WalletRepository {
    inner: Arc<CdkWalletRepository>,
}

#[uniffi::export(async_runtime = "tokio")]
impl WalletRepository {
    /// Create a new WalletRepository from locally persisted wallet state.
    ///
    /// Construction does not make network requests to configured mints.
    ///
    /// Accepts a `WalletStore` which can be:
    /// - `Sqlite { path }` — built-in Rust SQLite backend
    /// - `Postgres { url }` — built-in Rust Postgres backend
    /// - `Custom { db }` — foreign-language implementation of `WalletDatabase`
    #[uniffi::constructor]
    pub fn new(mnemonic: String, store: crate::database::WalletStore) -> Result<Self, FfiError> {
        let db = crate::database::resolve_wallet_store(store)?;

        // Parse mnemonic and generate seed without passphrase
        let m = Mnemonic::parse(&mnemonic)
            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
        let seed = m.to_seed_normalized("");

        // Convert the FFI database trait to a CDK database implementation
        let localstore = crate::database::create_cdk_database_from_ffi(db);

        let rt = crate::runtime::RuntimeGuard::new().map_err(FfiError::internal)?;
        let wallet = rt.block_on(async move {
            WalletRepositoryBuilder::new()
                .localstore(localstore)
                .seed(seed)
                .build()
                .await
        })?;

        Ok(Self {
            inner: Arc::new(wallet),
        })
    }

    /// Create a new WalletRepository with proxy configuration.
    ///
    /// Construction restores locally persisted wallet state without making
    /// network requests to configured mints. The proxy is used by subsequent
    /// mint operations.
    #[uniffi::constructor]
    pub fn new_with_proxy(
        mnemonic: String,
        store: crate::database::WalletStore,
        proxy_url: String,
    ) -> Result<Self, FfiError> {
        let db = crate::database::resolve_wallet_store(store)?;

        // Parse mnemonic and generate seed without passphrase
        let m = Mnemonic::parse(&mnemonic)
            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
        let seed = m.to_seed_normalized("");

        // Convert the FFI database trait to a CDK database implementation
        let localstore = crate::database::create_cdk_database_from_ffi(db);

        // Parse proxy URL
        let proxy_url = url::Url::parse(&proxy_url)
            .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;

        let rt = crate::runtime::RuntimeGuard::new().map_err(FfiError::internal)?;
        let wallet = rt.block_on(async move {
            WalletRepositoryBuilder::new()
                .localstore(localstore)
                .seed(seed)
                .proxy_url(proxy_url)
                .build()
                .await
        })?;

        Ok(Self {
            inner: Arc::new(wallet),
        })
    }

    /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
    ///
    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
    /// before requiring a refresh from the mint server for a specific mint.
    ///
    /// # Arguments
    ///
    /// * `mint_url` - The mint URL to set the TTL for
    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
    pub async fn set_metadata_cache_ttl_for_mint(
        &self,
        mint_url: MintUrl,
        ttl_secs: Option<u64>,
    ) -> Result<(), FfiError> {
        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
        let wallets = self.inner.get_wallets().await;

        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
            let ttl = ttl_secs.map(std::time::Duration::from_secs);
            wallet.set_metadata_cache_ttl(ttl);
            Ok(())
        } else {
            Err(FfiError::internal(format!(
                "Mint not found: {}",
                cdk_mint_url
            )))
        }
    }

    /// Set metadata cache TTL (time-to-live) in seconds for all mints
    ///
    /// Controls how long cached mint metadata is considered fresh for all mints
    /// in this WalletRepository.
    ///
    /// # Arguments
    ///
    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
        let wallets = self.inner.get_wallets().await;
        let ttl = ttl_secs.map(std::time::Duration::from_secs);

        for wallet in wallets.iter() {
            wallet.set_metadata_cache_ttl(ttl);
        }
    }

    /// Add a mint to this WalletRepository
    pub async fn create_wallet(
        &self,
        mint_url: MintUrl,
        unit: Option<CurrencyUnit>,
        target_proof_count: Option<u32>,
    ) -> Result<(), FfiError> {
        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;

        let config = target_proof_count.map(|count| {
            cdk::wallet::wallet_repository::WalletConfig::new()
                .with_target_proof_count(count as usize)
        });

        let unit_enum = unit.unwrap_or(CurrencyUnit::Sat);

        self.inner
            .create_wallet(cdk_mint_url, unit_enum.into(), config)
            .await?;

        Ok(())
    }

    /// Remove mint from WalletRepository
    pub async fn remove_wallet(
        &self,
        mint_url: MintUrl,
        currency_unit: CurrencyUnit,
    ) -> Result<(), FfiError> {
        // 1. Convert MintUrl safely without unwrap()
        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
            .try_into()
            .map_err(|_| FfiError::internal("invalid mint url"))?; // Map the error to your FfiError type

        // 2. Await the inner call and propagate its result with '?'
        self.inner
            .remove_wallet(cdk_mint_url, currency_unit.into())
            .await
            .map_err(|e| e.into()) // Ensure the inner error can convert to FfiError
    }

    /// Check if mint is in wallet
    pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
        if let Ok(cdk_mint_url) = mint_url.try_into() {
            self.inner.has_mint(&cdk_mint_url).await
        } else {
            false
        }
    }

    /// Get the NUT-27 mint backup public key as hex.
    pub fn mint_backup_public_key(&self) -> Result<String, FfiError> {
        let keys = self.inner.backup_keys()?;
        Ok(keys.public_key().to_hex())
    }

    /// Backup the current mint list to Nostr relays using NUT-27.
    pub async fn backup_mints(
        &self,
        relays: Vec<String>,
        options: BackupOptions,
    ) -> Result<BackupResult, FfiError> {
        let result = self.inner.backup_mints(relays, options.into()).await?;
        Ok(result.into())
    }

    /// Restore the mint list from Nostr relays using NUT-27.
    pub async fn restore_mints(
        &self,
        relays: Vec<String>,
        add_mints: bool,
        options: RestoreOptions,
    ) -> Result<RestoreResult, FfiError> {
        let result = self
            .inner
            .restore_mints(relays, add_mints, options.into())
            .await?;
        Ok(result.into())
    }

    /// Fetch the NUT-27 mint backup without adding mints to the repository.
    pub async fn fetch_mint_backup(
        &self,
        relays: Vec<String>,
        options: RestoreOptions,
    ) -> Result<MintBackup, FfiError> {
        let backup = self.inner.fetch_backup(relays, options.into()).await?;
        Ok(backup.into())
    }

    /// Get wallet balances for all mints
    pub async fn get_balances(&self) -> Result<HashMap<WalletKey, Amount>, FfiError> {
        let balances = self.inner.get_balances().await?;
        let mut balance_map = HashMap::new();
        for (wallet_key, amount) in balances {
            balance_map.insert(wallet_key.into(), amount.into());
        }
        Ok(balance_map)
    }

    /// Get all wallets from WalletRepository
    pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
        let wallets = self.inner.get_wallets().await;
        wallets
            .into_iter()
            .map(|w| Arc::new(crate::wallet::Wallet::from_inner(Arc::new(w))))
            .collect()
    }

    /// Get a specific wallet from WalletRepository by mint URL
    ///
    /// Returns an error if no wallet exists for the given mint URL.
    pub async fn get_wallet(
        &self,
        mint_url: MintUrl,
        unit: CurrencyUnit,
    ) -> Result<Arc<crate::wallet::Wallet>, FfiError> {
        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
        let unit_cdk: cdk::nuts::CurrencyUnit = unit.into();
        let wallet = self.inner.get_wallet(&cdk_mint_url, &unit_cdk).await?;
        Ok(Arc::new(crate::wallet::Wallet::from_inner(Arc::new(
            wallet,
        ))))
    }

    /// Get token data, including the expected redemption fee, without redeeming it.
    pub async fn get_token_data(
        &self,
        token: Arc<crate::token::Token>,
    ) -> Result<TokenData, FfiError> {
        Ok(self.inner.get_token_data(&token.inner).await?.into())
    }
}

/// Token data FFI type
///
/// Contains information extracted from a parsed token.
#[derive(Debug, Clone, uniffi::Record)]
pub struct TokenData {
    /// The mint URL from the token
    pub mint_url: MintUrl,
    /// The proofs contained in the token
    pub proofs: Vec<crate::types::Proof>,
    /// The memo from the token, if present
    pub memo: Option<String>,
    /// Value of token in smallest unit
    pub value: Amount,
    /// Currency unit
    pub unit: CurrencyUnit,
    /// Fee to redeem (None if unknown)
    pub redeem_fee: Option<Amount>,
}

impl From<cdk::wallet::TokenData> for TokenData {
    fn from(data: cdk::wallet::TokenData) -> Self {
        Self {
            mint_url: data.mint_url.into(),
            proofs: data.proofs.into_iter().map(Into::into).collect(),
            memo: data.memo,
            value: data.value.into(),
            unit: data.unit.into(),
            redeem_fee: data.redeem_fee.map(Into::into),
        }
    }
}