cdk_ffi/
postgres.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4// Bring the CDK wallet database trait into scope so trait methods resolve on the inner DB
5use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
6#[cfg(feature = "postgres")]
7use cdk_postgres::WalletPgDatabase as CdkWalletPgDatabase;
8
9use crate::{
10    CurrencyUnit, FfiError, Id, KeySet, KeySetInfo, Keys, MeltQuote, MintInfo, MintQuote, MintUrl,
11    ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, TransactionDirection,
12    TransactionId, WalletDatabase,
13};
14
15#[derive(uniffi::Object)]
16pub struct WalletPostgresDatabase {
17    inner: Arc<CdkWalletPgDatabase>,
18}
19
20// Keep a long-lived Tokio runtime for Postgres-created resources so that
21// background tasks (e.g., tokio-postgres connection drivers spawned during
22// construction) are not tied to a short-lived, ad-hoc runtime.
23#[cfg(feature = "postgres")]
24static PG_RUNTIME: once_cell::sync::OnceCell<tokio::runtime::Runtime> =
25    once_cell::sync::OnceCell::new();
26
27#[cfg(feature = "postgres")]
28fn pg_runtime() -> &'static tokio::runtime::Runtime {
29    PG_RUNTIME.get_or_init(|| {
30        tokio::runtime::Builder::new_multi_thread()
31            .enable_all()
32            .thread_name("cdk-ffi-pg")
33            .build()
34            .expect("failed to build pg runtime")
35    })
36}
37
38// Implement the local WalletDatabase trait (simple trait path required by uniffi)
39#[uniffi::export(async_runtime = "tokio")]
40#[async_trait::async_trait]
41impl WalletDatabase for WalletPostgresDatabase {
42    // Forward all trait methods to inner CDK database via the bridge adapter
43    async fn add_mint(
44        &self,
45        mint_url: MintUrl,
46        mint_info: Option<MintInfo>,
47    ) -> Result<(), FfiError> {
48        let cdk_mint_url = mint_url.try_into()?;
49        let cdk_mint_info = mint_info.map(Into::into);
50        println!("adding new mint");
51        self.inner
52            .add_mint(cdk_mint_url, cdk_mint_info)
53            .await
54            .map_err(|e| {
55                println!("ffi error {:?}", e);
56                FfiError::Database { msg: e.to_string() }
57            })
58    }
59    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
60        let cdk_mint_url = mint_url.try_into()?;
61        self.inner
62            .remove_mint(cdk_mint_url)
63            .await
64            .map_err(|e| FfiError::Database { msg: e.to_string() })
65    }
66    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
67        let cdk_mint_url = mint_url.try_into()?;
68        let result = self
69            .inner
70            .get_mint(cdk_mint_url)
71            .await
72            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
73        Ok(result.map(Into::into))
74    }
75    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
76        let result = self
77            .inner
78            .get_mints()
79            .await
80            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
81        Ok(result
82            .into_iter()
83            .map(|(k, v)| (k.into(), v.map(Into::into)))
84            .collect())
85    }
86    async fn update_mint_url(
87        &self,
88        old_mint_url: MintUrl,
89        new_mint_url: MintUrl,
90    ) -> Result<(), FfiError> {
91        let cdk_old_mint_url = old_mint_url.try_into()?;
92        let cdk_new_mint_url = new_mint_url.try_into()?;
93        self.inner
94            .update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
95            .await
96            .map_err(|e| FfiError::Database { msg: e.to_string() })
97    }
98    async fn add_mint_keysets(
99        &self,
100        mint_url: MintUrl,
101        keysets: Vec<KeySetInfo>,
102    ) -> Result<(), FfiError> {
103        let cdk_mint_url = mint_url.try_into()?;
104        let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
105        self.inner
106            .add_mint_keysets(cdk_mint_url, cdk_keysets)
107            .await
108            .map_err(|e| FfiError::Database { msg: e.to_string() })
109    }
110    async fn get_mint_keysets(
111        &self,
112        mint_url: MintUrl,
113    ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
114        let cdk_mint_url = mint_url.try_into()?;
115        let result = self
116            .inner
117            .get_mint_keysets(cdk_mint_url)
118            .await
119            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
120        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
121    }
122
123    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
124        let cdk_id = keyset_id.into();
125        let result = self
126            .inner
127            .get_keyset_by_id(&cdk_id)
128            .await
129            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
130        Ok(result.map(Into::into))
131    }
132
133    // Mint Quote Management
134    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
135        let cdk_quote = quote.try_into()?;
136        self.inner
137            .add_mint_quote(cdk_quote)
138            .await
139            .map_err(|e| FfiError::Database { msg: e.to_string() })
140    }
141
142    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
143        let result = self
144            .inner
145            .get_mint_quote(&quote_id)
146            .await
147            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
148        Ok(result.map(|q| q.into()))
149    }
150
151    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
152        let result = self
153            .inner
154            .get_mint_quotes()
155            .await
156            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
157        Ok(result.into_iter().map(|q| q.into()).collect())
158    }
159
160    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
161        self.inner
162            .remove_mint_quote(&quote_id)
163            .await
164            .map_err(|e| FfiError::Database { msg: e.to_string() })
165    }
166
167    // Melt Quote Management
168    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
169        let cdk_quote = quote.try_into()?;
170        self.inner
171            .add_melt_quote(cdk_quote)
172            .await
173            .map_err(|e| FfiError::Database { msg: e.to_string() })
174    }
175
176    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
177        let result = self
178            .inner
179            .get_melt_quote(&quote_id)
180            .await
181            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
182        Ok(result.map(|q| q.into()))
183    }
184
185    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
186        let result = self
187            .inner
188            .get_melt_quotes()
189            .await
190            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
191        Ok(result.into_iter().map(|q| q.into()).collect())
192    }
193
194    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
195        self.inner
196            .remove_melt_quote(&quote_id)
197            .await
198            .map_err(|e| FfiError::Database { msg: e.to_string() })
199    }
200
201    // Keys Management
202    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
203        // Convert FFI KeySet to cdk::nuts::KeySet
204        let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
205        self.inner
206            .add_keys(cdk_keyset)
207            .await
208            .map_err(|e| FfiError::Database { msg: e.to_string() })
209    }
210
211    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
212        let cdk_id = id.into();
213        let result = self
214            .inner
215            .get_keys(&cdk_id)
216            .await
217            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
218        Ok(result.map(Into::into))
219    }
220
221    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
222        let cdk_id = id.into();
223        self.inner
224            .remove_keys(&cdk_id)
225            .await
226            .map_err(|e| FfiError::Database { msg: e.to_string() })
227    }
228
229    // Proof Management
230    async fn update_proofs(
231        &self,
232        added: Vec<ProofInfo>,
233        removed_ys: Vec<PublicKey>,
234    ) -> Result<(), FfiError> {
235        // Convert FFI types to CDK types
236        let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
237            .into_iter()
238            .map(|info| {
239                Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
240                    proof: info.proof.try_into()?,
241                    y: info.y.try_into()?,
242                    mint_url: info.mint_url.try_into()?,
243                    state: info.state.into(),
244                    spending_condition: info
245                        .spending_condition
246                        .map(|sc| sc.try_into())
247                        .transpose()?,
248                    unit: info.unit.into(),
249                })
250            })
251            .collect();
252        let cdk_added = cdk_added?;
253
254        let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
255            removed_ys.into_iter().map(|pk| pk.try_into()).collect();
256        let cdk_removed_ys = cdk_removed_ys?;
257
258        self.inner
259            .update_proofs(cdk_added, cdk_removed_ys)
260            .await
261            .map_err(|e| FfiError::Database { msg: e.to_string() })
262    }
263
264    async fn get_proofs(
265        &self,
266        mint_url: Option<MintUrl>,
267        unit: Option<CurrencyUnit>,
268        state: Option<Vec<ProofState>>,
269        spending_conditions: Option<Vec<SpendingConditions>>,
270    ) -> Result<Vec<ProofInfo>, FfiError> {
271        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
272        let cdk_unit = unit.map(Into::into);
273        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
274        let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
275            spending_conditions
276                .map(|sc| {
277                    sc.into_iter()
278                        .map(|c| c.try_into())
279                        .collect::<Result<Vec<_>, FfiError>>()
280                })
281                .transpose()?;
282
283        let result = self
284            .inner
285            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
286            .await
287            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
288
289        Ok(result.into_iter().map(Into::into).collect())
290    }
291
292    async fn get_balance(
293        &self,
294        mint_url: Option<MintUrl>,
295        unit: Option<CurrencyUnit>,
296        state: Option<Vec<ProofState>>,
297    ) -> Result<u64, FfiError> {
298        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
299        let cdk_unit = unit.map(Into::into);
300        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
301
302        self.inner
303            .get_balance(cdk_mint_url, cdk_unit, cdk_state)
304            .await
305            .map_err(|e| FfiError::Database { msg: e.to_string() })
306    }
307
308    async fn update_proofs_state(
309        &self,
310        ys: Vec<PublicKey>,
311        state: ProofState,
312    ) -> Result<(), FfiError> {
313        let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
314            ys.into_iter().map(|pk| pk.try_into()).collect();
315        let cdk_ys = cdk_ys?;
316        let cdk_state = state.into();
317
318        self.inner
319            .update_proofs_state(cdk_ys, cdk_state)
320            .await
321            .map_err(|e| FfiError::Database { msg: e.to_string() })
322    }
323
324    // Keyset Counter Management
325    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
326        let cdk_id = keyset_id.into();
327        self.inner
328            .increment_keyset_counter(&cdk_id, count)
329            .await
330            .map_err(|e| FfiError::Database { msg: e.to_string() })
331    }
332
333    // Transaction Management
334    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
335        // Convert FFI Transaction to CDK Transaction using TryFrom
336        let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
337
338        self.inner
339            .add_transaction(cdk_transaction)
340            .await
341            .map_err(|e| FfiError::Database { msg: e.to_string() })
342    }
343
344    async fn get_transaction(
345        &self,
346        transaction_id: TransactionId,
347    ) -> Result<Option<Transaction>, FfiError> {
348        let cdk_id = transaction_id.try_into()?;
349        let result = self
350            .inner
351            .get_transaction(cdk_id)
352            .await
353            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
354        Ok(result.map(Into::into))
355    }
356
357    async fn list_transactions(
358        &self,
359        mint_url: Option<MintUrl>,
360        direction: Option<TransactionDirection>,
361        unit: Option<CurrencyUnit>,
362    ) -> Result<Vec<Transaction>, FfiError> {
363        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
364        let cdk_direction = direction.map(Into::into);
365        let cdk_unit = unit.map(Into::into);
366
367        let result = self
368            .inner
369            .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
370            .await
371            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
372
373        Ok(result.into_iter().map(Into::into).collect())
374    }
375
376    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
377        let cdk_id = transaction_id.try_into()?;
378        self.inner
379            .remove_transaction(cdk_id)
380            .await
381            .map_err(|e| FfiError::Database { msg: e.to_string() })
382    }
383}
384
385#[uniffi::export]
386impl WalletPostgresDatabase {
387    /// Create a new Postgres-backed wallet database
388    /// Requires cdk-ffi to be built with feature "postgres".
389    /// Example URL:
390    ///  "host=localhost user=test password=test dbname=testdb port=5433 schema=wallet sslmode=prefer"
391    #[cfg(feature = "postgres")]
392    #[uniffi::constructor]
393    pub fn new(url: String) -> Result<Arc<Self>, FfiError> {
394        let inner = match tokio::runtime::Handle::try_current() {
395            Ok(handle) => tokio::task::block_in_place(|| {
396                handle.block_on(
397                    async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await },
398                )
399            }),
400            // Important: use a process-long runtime so background connection tasks stay alive.
401            Err(_) => pg_runtime()
402                .block_on(async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await }),
403        }
404        .map_err(|e| FfiError::Database { msg: e.to_string() })?;
405        Ok(Arc::new(WalletPostgresDatabase {
406            inner: Arc::new(inner),
407        }))
408    }
409
410    fn clone_as_trait(&self) -> Arc<dyn WalletDatabase> {
411        // Safety: UniFFI objects are reference counted and Send+Sync via Arc
412        let obj: Arc<dyn WalletDatabase> = Arc::new(WalletPostgresDatabase {
413            inner: self.inner.clone(),
414        });
415        obj
416    }
417}