Skip to main content

cdk_ffi/
wallet_repository.rs

1//! FFI WalletRepository bindings
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use bip39::Mnemonic;
7use cdk::wallet::wallet_repository::{
8    WalletRepository as CdkWalletRepository, WalletRepositoryBuilder,
9};
10
11use crate::error::FfiError;
12use crate::types::*;
13
14/// FFI-compatible WalletRepository
15#[derive(uniffi::Object)]
16pub struct WalletRepository {
17    inner: Arc<CdkWalletRepository>,
18}
19
20#[uniffi::export(async_runtime = "tokio")]
21impl WalletRepository {
22    /// Create a new WalletRepository from locally persisted wallet state.
23    ///
24    /// Construction does not make network requests to configured mints.
25    ///
26    /// Accepts a `WalletStore` which can be:
27    /// - `Sqlite { path }` — built-in Rust SQLite backend
28    /// - `Postgres { url }` — built-in Rust Postgres backend
29    /// - `Custom { db }` — foreign-language implementation of `WalletDatabase`
30    #[uniffi::constructor]
31    pub fn new(mnemonic: String, store: crate::database::WalletStore) -> Result<Self, FfiError> {
32        let db = crate::database::resolve_wallet_store(store)?;
33
34        // Parse mnemonic and generate seed without passphrase
35        let m = Mnemonic::parse(&mnemonic)
36            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
37        let seed = m.to_seed_normalized("");
38
39        // Convert the FFI database trait to a CDK database implementation
40        let localstore = crate::database::create_cdk_database_from_ffi(db);
41
42        let rt = crate::runtime::RuntimeGuard::new().map_err(FfiError::internal)?;
43        let wallet = rt.block_on(async move {
44            WalletRepositoryBuilder::new()
45                .localstore(localstore)
46                .seed(seed)
47                .build()
48                .await
49        })?;
50
51        Ok(Self {
52            inner: Arc::new(wallet),
53        })
54    }
55
56    /// Create a new WalletRepository with proxy configuration.
57    ///
58    /// Construction restores locally persisted wallet state without making
59    /// network requests to configured mints. The proxy is used by subsequent
60    /// mint operations.
61    #[uniffi::constructor]
62    pub fn new_with_proxy(
63        mnemonic: String,
64        store: crate::database::WalletStore,
65        proxy_url: String,
66    ) -> Result<Self, FfiError> {
67        let db = crate::database::resolve_wallet_store(store)?;
68
69        // Parse mnemonic and generate seed without passphrase
70        let m = Mnemonic::parse(&mnemonic)
71            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
72        let seed = m.to_seed_normalized("");
73
74        // Convert the FFI database trait to a CDK database implementation
75        let localstore = crate::database::create_cdk_database_from_ffi(db);
76
77        // Parse proxy URL
78        let proxy_url = url::Url::parse(&proxy_url)
79            .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;
80
81        let rt = crate::runtime::RuntimeGuard::new().map_err(FfiError::internal)?;
82        let wallet = rt.block_on(async move {
83            WalletRepositoryBuilder::new()
84                .localstore(localstore)
85                .seed(seed)
86                .proxy_url(proxy_url)
87                .build()
88                .await
89        })?;
90
91        Ok(Self {
92            inner: Arc::new(wallet),
93        })
94    }
95
96    /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
97    ///
98    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
99    /// before requiring a refresh from the mint server for a specific mint.
100    ///
101    /// # Arguments
102    ///
103    /// * `mint_url` - The mint URL to set the TTL for
104    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
105    pub async fn set_metadata_cache_ttl_for_mint(
106        &self,
107        mint_url: MintUrl,
108        ttl_secs: Option<u64>,
109    ) -> Result<(), FfiError> {
110        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
111        let wallets = self.inner.get_wallets().await;
112
113        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
114            let ttl = ttl_secs.map(std::time::Duration::from_secs);
115            wallet.set_metadata_cache_ttl(ttl);
116            Ok(())
117        } else {
118            Err(FfiError::internal(format!(
119                "Mint not found: {}",
120                cdk_mint_url
121            )))
122        }
123    }
124
125    /// Set metadata cache TTL (time-to-live) in seconds for all mints
126    ///
127    /// Controls how long cached mint metadata is considered fresh for all mints
128    /// in this WalletRepository.
129    ///
130    /// # Arguments
131    ///
132    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
133    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
134        let wallets = self.inner.get_wallets().await;
135        let ttl = ttl_secs.map(std::time::Duration::from_secs);
136
137        for wallet in wallets.iter() {
138            wallet.set_metadata_cache_ttl(ttl);
139        }
140    }
141
142    /// Add a mint to this WalletRepository
143    pub async fn create_wallet(
144        &self,
145        mint_url: MintUrl,
146        unit: Option<CurrencyUnit>,
147        target_proof_count: Option<u32>,
148    ) -> Result<(), FfiError> {
149        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
150
151        let config = target_proof_count.map(|count| {
152            cdk::wallet::wallet_repository::WalletConfig::new()
153                .with_target_proof_count(count as usize)
154        });
155
156        let unit_enum = unit.unwrap_or(CurrencyUnit::Sat);
157
158        self.inner
159            .create_wallet(cdk_mint_url, unit_enum.into(), config)
160            .await?;
161
162        Ok(())
163    }
164
165    /// Remove mint from WalletRepository
166    pub async fn remove_wallet(
167        &self,
168        mint_url: MintUrl,
169        currency_unit: CurrencyUnit,
170    ) -> Result<(), FfiError> {
171        // 1. Convert MintUrl safely without unwrap()
172        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
173            .try_into()
174            .map_err(|_| FfiError::internal("invalid mint url"))?; // Map the error to your FfiError type
175
176        // 2. Await the inner call and propagate its result with '?'
177        self.inner
178            .remove_wallet(cdk_mint_url, currency_unit.into())
179            .await
180            .map_err(|e| e.into()) // Ensure the inner error can convert to FfiError
181    }
182
183    /// Check if mint is in wallet
184    pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
185        if let Ok(cdk_mint_url) = mint_url.try_into() {
186            self.inner.has_mint(&cdk_mint_url).await
187        } else {
188            false
189        }
190    }
191
192    /// Get the NUT-27 mint backup public key as hex.
193    pub fn mint_backup_public_key(&self) -> Result<String, FfiError> {
194        let keys = self.inner.backup_keys()?;
195        Ok(keys.public_key().to_hex())
196    }
197
198    /// Backup the current mint list to Nostr relays using NUT-27.
199    pub async fn backup_mints(
200        &self,
201        relays: Vec<String>,
202        options: BackupOptions,
203    ) -> Result<BackupResult, FfiError> {
204        let result = self.inner.backup_mints(relays, options.into()).await?;
205        Ok(result.into())
206    }
207
208    /// Restore the mint list from Nostr relays using NUT-27.
209    pub async fn restore_mints(
210        &self,
211        relays: Vec<String>,
212        add_mints: bool,
213        options: RestoreOptions,
214    ) -> Result<RestoreResult, FfiError> {
215        let result = self
216            .inner
217            .restore_mints(relays, add_mints, options.into())
218            .await?;
219        Ok(result.into())
220    }
221
222    /// Fetch the NUT-27 mint backup without adding mints to the repository.
223    pub async fn fetch_mint_backup(
224        &self,
225        relays: Vec<String>,
226        options: RestoreOptions,
227    ) -> Result<MintBackup, FfiError> {
228        let backup = self.inner.fetch_backup(relays, options.into()).await?;
229        Ok(backup.into())
230    }
231
232    /// Get wallet balances for all mints
233    pub async fn get_balances(&self) -> Result<HashMap<WalletKey, Amount>, FfiError> {
234        let balances = self.inner.get_balances().await?;
235        let mut balance_map = HashMap::new();
236        for (wallet_key, amount) in balances {
237            balance_map.insert(wallet_key.into(), amount.into());
238        }
239        Ok(balance_map)
240    }
241
242    /// Get all wallets from WalletRepository
243    pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
244        let wallets = self.inner.get_wallets().await;
245        wallets
246            .into_iter()
247            .map(|w| Arc::new(crate::wallet::Wallet::from_inner(Arc::new(w))))
248            .collect()
249    }
250
251    /// Get a specific wallet from WalletRepository by mint URL
252    ///
253    /// Returns an error if no wallet exists for the given mint URL.
254    pub async fn get_wallet(
255        &self,
256        mint_url: MintUrl,
257        unit: CurrencyUnit,
258    ) -> Result<Arc<crate::wallet::Wallet>, FfiError> {
259        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
260        let unit_cdk: cdk::nuts::CurrencyUnit = unit.into();
261        let wallet = self.inner.get_wallet(&cdk_mint_url, &unit_cdk).await?;
262        Ok(Arc::new(crate::wallet::Wallet::from_inner(Arc::new(
263            wallet,
264        ))))
265    }
266
267    /// Get token data, including the expected redemption fee, without redeeming it.
268    pub async fn get_token_data(
269        &self,
270        token: Arc<crate::token::Token>,
271    ) -> Result<TokenData, FfiError> {
272        Ok(self.inner.get_token_data(&token.inner).await?.into())
273    }
274}
275
276/// Token data FFI type
277///
278/// Contains information extracted from a parsed token.
279#[derive(Debug, Clone, uniffi::Record)]
280pub struct TokenData {
281    /// The mint URL from the token
282    pub mint_url: MintUrl,
283    /// The proofs contained in the token
284    pub proofs: Vec<crate::types::Proof>,
285    /// The memo from the token, if present
286    pub memo: Option<String>,
287    /// Value of token in smallest unit
288    pub value: Amount,
289    /// Currency unit
290    pub unit: CurrencyUnit,
291    /// Fee to redeem (None if unknown)
292    pub redeem_fee: Option<Amount>,
293}
294
295impl From<cdk::wallet::TokenData> for TokenData {
296    fn from(data: cdk::wallet::TokenData) -> Self {
297        Self {
298            mint_url: data.mint_url.into(),
299            proofs: data.proofs.into_iter().map(Into::into).collect(),
300            memo: data.memo,
301            value: data.value.into(),
302            unit: data.unit.into(),
303            redeem_fee: data.redeem_fee.map(Into::into),
304        }
305    }
306}