cdk_ffi/
database.rs

1//! FFI Database bindings
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
7
8use crate::error::FfiError;
9#[cfg(feature = "postgres")]
10use crate::postgres::WalletPostgresDatabase;
11use crate::sqlite::WalletSqliteDatabase;
12use crate::types::*;
13
14/// FFI-compatible trait for wallet database operations
15/// This trait mirrors the CDK WalletDatabase trait but uses FFI-compatible types
16#[uniffi::export(with_foreign)]
17#[async_trait::async_trait]
18pub trait WalletDatabase: Send + Sync {
19    // Mint Management
20    /// Add Mint to storage
21    async fn add_mint(
22        &self,
23        mint_url: MintUrl,
24        mint_info: Option<MintInfo>,
25    ) -> Result<(), FfiError>;
26
27    /// Remove Mint from storage
28    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>;
29
30    /// Get mint from storage
31    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError>;
32
33    /// Get all mints from storage
34    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError>;
35
36    /// Update mint url
37    async fn update_mint_url(
38        &self,
39        old_mint_url: MintUrl,
40        new_mint_url: MintUrl,
41    ) -> Result<(), FfiError>;
42
43    // Keyset Management
44    /// Add mint keyset to storage
45    async fn add_mint_keysets(
46        &self,
47        mint_url: MintUrl,
48        keysets: Vec<KeySetInfo>,
49    ) -> Result<(), FfiError>;
50
51    /// Get mint keysets for mint url
52    async fn get_mint_keysets(
53        &self,
54        mint_url: MintUrl,
55    ) -> Result<Option<Vec<KeySetInfo>>, FfiError>;
56
57    /// Get mint keyset by id
58    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError>;
59
60    // Mint Quote Management
61    /// Add mint quote to storage
62    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>;
63
64    /// Get mint quote from storage
65    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError>;
66
67    /// Get mint quotes from storage
68    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError>;
69
70    /// Remove mint quote from storage
71    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>;
72
73    // Melt Quote Management
74    /// Add melt quote to storage
75    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>;
76
77    /// Get melt quote from storage
78    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError>;
79
80    /// Get melt quotes from storage
81    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError>;
82
83    /// Remove melt quote from storage
84    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>;
85
86    // Keys Management
87    /// Add Keys to storage
88    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>;
89
90    /// Get Keys from storage
91    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError>;
92
93    /// Remove Keys from storage
94    async fn remove_keys(&self, id: Id) -> Result<(), FfiError>;
95
96    // Proof Management
97    /// Update the proofs in storage by adding new proofs or removing proofs by their Y value
98    async fn update_proofs(
99        &self,
100        added: Vec<ProofInfo>,
101        removed_ys: Vec<PublicKey>,
102    ) -> Result<(), FfiError>;
103
104    /// Get proofs from storage
105    async fn get_proofs(
106        &self,
107        mint_url: Option<MintUrl>,
108        unit: Option<CurrencyUnit>,
109        state: Option<Vec<ProofState>>,
110        spending_conditions: Option<Vec<SpendingConditions>>,
111    ) -> Result<Vec<ProofInfo>, FfiError>;
112
113    /// Get balance efficiently using SQL aggregation
114    async fn get_balance(
115        &self,
116        mint_url: Option<MintUrl>,
117        unit: Option<CurrencyUnit>,
118        state: Option<Vec<ProofState>>,
119    ) -> Result<u64, FfiError>;
120
121    /// Update proofs state in storage
122    async fn update_proofs_state(
123        &self,
124        ys: Vec<PublicKey>,
125        state: ProofState,
126    ) -> Result<(), FfiError>;
127
128    // Keyset Counter Management
129    /// Increment Keyset counter
130    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError>;
131
132    // Transaction Management
133    /// Add transaction to storage
134    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError>;
135
136    /// Get transaction from storage
137    async fn get_transaction(
138        &self,
139        transaction_id: TransactionId,
140    ) -> Result<Option<Transaction>, FfiError>;
141
142    /// List transactions from storage
143    async fn list_transactions(
144        &self,
145        mint_url: Option<MintUrl>,
146        direction: Option<TransactionDirection>,
147        unit: Option<CurrencyUnit>,
148    ) -> Result<Vec<Transaction>, FfiError>;
149
150    /// Remove transaction from storage
151    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>;
152}
153
154/// Internal bridge trait to convert from the FFI trait to the CDK database trait
155/// This allows us to bridge between the UniFFI trait and the CDK's internal database trait
156struct WalletDatabaseBridge {
157    ffi_db: Arc<dyn WalletDatabase>,
158}
159
160impl WalletDatabaseBridge {
161    fn new(ffi_db: Arc<dyn WalletDatabase>) -> Self {
162        Self { ffi_db }
163    }
164}
165
166impl std::fmt::Debug for WalletDatabaseBridge {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        write!(f, "WalletDatabaseBridge")
169    }
170}
171
172#[async_trait::async_trait]
173impl CdkWalletDatabase for WalletDatabaseBridge {
174    type Err = cdk::cdk_database::Error;
175
176    // Mint Management
177    async fn add_mint(
178        &self,
179        mint_url: cdk::mint_url::MintUrl,
180        mint_info: Option<cdk::nuts::MintInfo>,
181    ) -> Result<(), Self::Err> {
182        let ffi_mint_url = mint_url.into();
183        let ffi_mint_info = mint_info.map(Into::into);
184        self.ffi_db
185            .add_mint(ffi_mint_url, ffi_mint_info)
186            .await
187            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
188    }
189
190    async fn remove_mint(&self, mint_url: cdk::mint_url::MintUrl) -> Result<(), Self::Err> {
191        let ffi_mint_url = mint_url.into();
192        self.ffi_db
193            .remove_mint(ffi_mint_url)
194            .await
195            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
196    }
197
198    async fn get_mint(
199        &self,
200        mint_url: cdk::mint_url::MintUrl,
201    ) -> Result<Option<cdk::nuts::MintInfo>, Self::Err> {
202        let ffi_mint_url = mint_url.into();
203        let result = self
204            .ffi_db
205            .get_mint(ffi_mint_url)
206            .await
207            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
208        Ok(result.map(Into::into))
209    }
210
211    async fn get_mints(
212        &self,
213    ) -> Result<HashMap<cdk::mint_url::MintUrl, Option<cdk::nuts::MintInfo>>, Self::Err> {
214        let result = self
215            .ffi_db
216            .get_mints()
217            .await
218            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
219
220        let mut cdk_result = HashMap::new();
221        for (ffi_mint_url, mint_info_opt) in result {
222            let cdk_url = ffi_mint_url
223                .try_into()
224                .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))?;
225            cdk_result.insert(cdk_url, mint_info_opt.map(Into::into));
226        }
227        Ok(cdk_result)
228    }
229
230    async fn update_mint_url(
231        &self,
232        old_mint_url: cdk::mint_url::MintUrl,
233        new_mint_url: cdk::mint_url::MintUrl,
234    ) -> Result<(), Self::Err> {
235        let ffi_old_mint_url = old_mint_url.into();
236        let ffi_new_mint_url = new_mint_url.into();
237        self.ffi_db
238            .update_mint_url(ffi_old_mint_url, ffi_new_mint_url)
239            .await
240            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
241    }
242
243    // Keyset Management
244    async fn add_mint_keysets(
245        &self,
246        mint_url: cdk::mint_url::MintUrl,
247        keysets: Vec<cdk::nuts::KeySetInfo>,
248    ) -> Result<(), Self::Err> {
249        let ffi_mint_url = mint_url.into();
250        let ffi_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
251
252        self.ffi_db
253            .add_mint_keysets(ffi_mint_url, ffi_keysets)
254            .await
255            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
256    }
257
258    async fn get_mint_keysets(
259        &self,
260        mint_url: cdk::mint_url::MintUrl,
261    ) -> Result<Option<Vec<cdk::nuts::KeySetInfo>>, Self::Err> {
262        let ffi_mint_url = mint_url.into();
263        let result = self
264            .ffi_db
265            .get_mint_keysets(ffi_mint_url)
266            .await
267            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
268        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
269    }
270
271    async fn get_keyset_by_id(
272        &self,
273        keyset_id: &cdk::nuts::Id,
274    ) -> Result<Option<cdk::nuts::KeySetInfo>, Self::Err> {
275        let ffi_id = (*keyset_id).into();
276        let result = self
277            .ffi_db
278            .get_keyset_by_id(ffi_id)
279            .await
280            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
281        Ok(result.map(Into::into))
282    }
283
284    // Mint Quote Management
285    async fn add_mint_quote(&self, quote: cdk::wallet::MintQuote) -> Result<(), Self::Err> {
286        let ffi_quote = quote.into();
287        self.ffi_db
288            .add_mint_quote(ffi_quote)
289            .await
290            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
291    }
292
293    async fn get_mint_quote(
294        &self,
295        quote_id: &str,
296    ) -> Result<Option<cdk::wallet::MintQuote>, Self::Err> {
297        let result = self
298            .ffi_db
299            .get_mint_quote(quote_id.to_string())
300            .await
301            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
302        Ok(result
303            .map(|q| {
304                q.try_into()
305                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
306            })
307            .transpose()?)
308    }
309
310    async fn get_mint_quotes(&self) -> Result<Vec<cdk::wallet::MintQuote>, Self::Err> {
311        let result = self
312            .ffi_db
313            .get_mint_quotes()
314            .await
315            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
316        Ok(result
317            .into_iter()
318            .map(|q| {
319                q.try_into()
320                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
321            })
322            .collect::<Result<Vec<_>, _>>()?)
323    }
324
325    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
326        self.ffi_db
327            .remove_mint_quote(quote_id.to_string())
328            .await
329            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
330    }
331
332    // Melt Quote Management
333    async fn add_melt_quote(&self, quote: cdk::wallet::MeltQuote) -> Result<(), Self::Err> {
334        let ffi_quote = quote.into();
335        self.ffi_db
336            .add_melt_quote(ffi_quote)
337            .await
338            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
339    }
340
341    async fn get_melt_quote(
342        &self,
343        quote_id: &str,
344    ) -> Result<Option<cdk::wallet::MeltQuote>, Self::Err> {
345        let result = self
346            .ffi_db
347            .get_melt_quote(quote_id.to_string())
348            .await
349            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
350        Ok(result
351            .map(|q| {
352                q.try_into()
353                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
354            })
355            .transpose()?)
356    }
357
358    async fn get_melt_quotes(&self) -> Result<Vec<cdk::wallet::MeltQuote>, Self::Err> {
359        let result = self
360            .ffi_db
361            .get_melt_quotes()
362            .await
363            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
364        Ok(result
365            .into_iter()
366            .map(|q| {
367                q.try_into()
368                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
369            })
370            .collect::<Result<Vec<_>, _>>()?)
371    }
372
373    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
374        self.ffi_db
375            .remove_melt_quote(quote_id.to_string())
376            .await
377            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
378    }
379
380    // Keys Management
381    async fn add_keys(&self, keyset: cdk::nuts::KeySet) -> Result<(), Self::Err> {
382        let ffi_keyset: KeySet = keyset.into();
383        self.ffi_db
384            .add_keys(ffi_keyset)
385            .await
386            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
387    }
388
389    async fn get_keys(&self, id: &cdk::nuts::Id) -> Result<Option<cdk::nuts::Keys>, Self::Err> {
390        let ffi_id: Id = (*id).into();
391        let result = self
392            .ffi_db
393            .get_keys(ffi_id)
394            .await
395            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
396
397        // Convert FFI Keys back to CDK Keys using TryFrom
398        result
399            .map(|ffi_keys| {
400                ffi_keys
401                    .try_into()
402                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
403            })
404            .transpose()
405    }
406
407    async fn remove_keys(&self, id: &cdk::nuts::Id) -> Result<(), Self::Err> {
408        let ffi_id = (*id).into();
409        self.ffi_db
410            .remove_keys(ffi_id)
411            .await
412            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
413    }
414
415    // Proof Management
416    async fn update_proofs(
417        &self,
418        added: Vec<cdk::types::ProofInfo>,
419        removed_ys: Vec<cdk::nuts::PublicKey>,
420    ) -> Result<(), Self::Err> {
421        let ffi_added: Vec<ProofInfo> = added.into_iter().map(Into::into).collect();
422        let ffi_removed_ys: Vec<PublicKey> = removed_ys.into_iter().map(Into::into).collect();
423
424        self.ffi_db
425            .update_proofs(ffi_added, ffi_removed_ys)
426            .await
427            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
428    }
429
430    async fn get_proofs(
431        &self,
432        mint_url: Option<cdk::mint_url::MintUrl>,
433        unit: Option<cdk::nuts::CurrencyUnit>,
434        state: Option<Vec<cdk::nuts::State>>,
435        spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>>,
436    ) -> Result<Vec<cdk::types::ProofInfo>, Self::Err> {
437        let ffi_mint_url = mint_url.map(Into::into);
438        let ffi_unit = unit.map(Into::into);
439        let ffi_state = state.map(|s| s.into_iter().map(Into::into).collect());
440        let ffi_spending_conditions =
441            spending_conditions.map(|sc| sc.into_iter().map(Into::into).collect());
442
443        let result = self
444            .ffi_db
445            .get_proofs(ffi_mint_url, ffi_unit, ffi_state, ffi_spending_conditions)
446            .await
447            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
448
449        // Convert back to CDK ProofInfo
450        let cdk_result: Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> = result
451            .into_iter()
452            .map(|info| {
453                Ok(cdk::types::ProofInfo {
454                    proof: info.proof.try_into().map_err(|e: FfiError| {
455                        cdk::cdk_database::Error::Database(e.to_string().into())
456                    })?,
457                    y: info.y.try_into().map_err(|e: FfiError| {
458                        cdk::cdk_database::Error::Database(e.to_string().into())
459                    })?,
460                    mint_url: info.mint_url.try_into().map_err(|e: FfiError| {
461                        cdk::cdk_database::Error::Database(e.to_string().into())
462                    })?,
463                    state: info.state.into(),
464                    spending_condition: info
465                        .spending_condition
466                        .map(|sc| sc.try_into())
467                        .transpose()
468                        .map_err(|e: FfiError| {
469                            cdk::cdk_database::Error::Database(e.to_string().into())
470                        })?,
471                    unit: info.unit.into(),
472                })
473            })
474            .collect();
475
476        cdk_result
477    }
478
479    async fn get_balance(
480        &self,
481        mint_url: Option<cdk::mint_url::MintUrl>,
482        unit: Option<cdk::nuts::CurrencyUnit>,
483        state: Option<Vec<cdk::nuts::State>>,
484    ) -> Result<u64, Self::Err> {
485        let ffi_mint_url = mint_url.map(Into::into);
486        let ffi_unit = unit.map(Into::into);
487        let ffi_state = state.map(|s| s.into_iter().map(Into::into).collect());
488
489        self.ffi_db
490            .get_balance(ffi_mint_url, ffi_unit, ffi_state)
491            .await
492            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
493    }
494
495    async fn update_proofs_state(
496        &self,
497        ys: Vec<cdk::nuts::PublicKey>,
498        state: cdk::nuts::State,
499    ) -> Result<(), Self::Err> {
500        let ffi_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
501        let ffi_state = state.into();
502
503        self.ffi_db
504            .update_proofs_state(ffi_ys, ffi_state)
505            .await
506            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
507    }
508
509    // Keyset Counter Management
510    async fn increment_keyset_counter(
511        &self,
512        keyset_id: &cdk::nuts::Id,
513        count: u32,
514    ) -> Result<u32, Self::Err> {
515        let ffi_id = (*keyset_id).into();
516        self.ffi_db
517            .increment_keyset_counter(ffi_id, count)
518            .await
519            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
520    }
521
522    // Transaction Management
523    async fn add_transaction(
524        &self,
525        transaction: cdk::wallet::types::Transaction,
526    ) -> Result<(), Self::Err> {
527        let ffi_transaction = transaction.into();
528        self.ffi_db
529            .add_transaction(ffi_transaction)
530            .await
531            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
532    }
533
534    async fn get_transaction(
535        &self,
536        transaction_id: cdk::wallet::types::TransactionId,
537    ) -> Result<Option<cdk::wallet::types::Transaction>, Self::Err> {
538        let ffi_id = transaction_id.into();
539        let result = self
540            .ffi_db
541            .get_transaction(ffi_id)
542            .await
543            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
544
545        result
546            .map(|tx| tx.try_into())
547            .transpose()
548            .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
549    }
550
551    async fn list_transactions(
552        &self,
553        mint_url: Option<cdk::mint_url::MintUrl>,
554        direction: Option<cdk::wallet::types::TransactionDirection>,
555        unit: Option<cdk::nuts::CurrencyUnit>,
556    ) -> Result<Vec<cdk::wallet::types::Transaction>, Self::Err> {
557        let ffi_mint_url = mint_url.map(Into::into);
558        let ffi_direction = direction.map(Into::into);
559        let ffi_unit = unit.map(Into::into);
560
561        let result = self
562            .ffi_db
563            .list_transactions(ffi_mint_url, ffi_direction, ffi_unit)
564            .await
565            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
566
567        result
568            .into_iter()
569            .map(|tx| tx.try_into())
570            .collect::<Result<Vec<_>, FfiError>>()
571            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
572    }
573
574    async fn remove_transaction(
575        &self,
576        transaction_id: cdk::wallet::types::TransactionId,
577    ) -> Result<(), Self::Err> {
578        let ffi_id = transaction_id.into();
579        self.ffi_db
580            .remove_transaction(ffi_id)
581            .await
582            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
583    }
584}
585
586/// FFI-safe wallet database backend selection
587#[derive(uniffi::Enum)]
588pub enum WalletDbBackend {
589    Sqlite {
590        path: String,
591    },
592    #[cfg(feature = "postgres")]
593    Postgres {
594        url: String,
595    },
596}
597
598/// Factory helpers returning a CDK wallet database behind the FFI trait
599#[uniffi::export]
600pub fn create_wallet_db(backend: WalletDbBackend) -> Result<Arc<dyn WalletDatabase>, FfiError> {
601    match backend {
602        WalletDbBackend::Sqlite { path } => {
603            let sqlite = WalletSqliteDatabase::new(path)?;
604            Ok(sqlite as Arc<dyn WalletDatabase>)
605        }
606        #[cfg(feature = "postgres")]
607        WalletDbBackend::Postgres { url } => {
608            let pg = WalletPostgresDatabase::new(url)?;
609            Ok(pg as Arc<dyn WalletDatabase>)
610        }
611    }
612}
613
614/// Helper function to create a CDK database from the FFI trait
615pub fn create_cdk_database_from_ffi(
616    ffi_db: Arc<dyn WalletDatabase>,
617) -> Arc<dyn CdkWalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync> {
618    Arc::new(WalletDatabaseBridge::new(ffi_db))
619}