postgres.rs 15 KB

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