postgres.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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_balance(
  268. &self,
  269. mint_url: Option<MintUrl>,
  270. unit: Option<CurrencyUnit>,
  271. state: Option<Vec<ProofState>>,
  272. ) -> Result<u64, FfiError> {
  273. let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
  274. let cdk_unit = unit.map(Into::into);
  275. let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
  276. self.inner
  277. .get_balance(cdk_mint_url, cdk_unit, cdk_state)
  278. .await
  279. .map_err(|e| FfiError::Database { msg: e.to_string() })
  280. }
  281. async fn update_proofs_state(
  282. &self,
  283. ys: Vec<PublicKey>,
  284. state: ProofState,
  285. ) -> Result<(), FfiError> {
  286. let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
  287. ys.into_iter().map(|pk| pk.try_into()).collect();
  288. let cdk_ys = cdk_ys?;
  289. let cdk_state = state.into();
  290. self.inner
  291. .update_proofs_state(cdk_ys, cdk_state)
  292. .await
  293. .map_err(|e| FfiError::Database { msg: e.to_string() })
  294. }
  295. // Keyset Counter Management
  296. async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
  297. let cdk_id = keyset_id.into();
  298. self.inner
  299. .increment_keyset_counter(&cdk_id, count)
  300. .await
  301. .map_err(|e| FfiError::Database { msg: e.to_string() })
  302. }
  303. // Transaction Management
  304. async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
  305. // Convert FFI Transaction to CDK Transaction using TryFrom
  306. let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
  307. self.inner
  308. .add_transaction(cdk_transaction)
  309. .await
  310. .map_err(|e| FfiError::Database { msg: e.to_string() })
  311. }
  312. async fn get_transaction(
  313. &self,
  314. transaction_id: TransactionId,
  315. ) -> Result<Option<Transaction>, FfiError> {
  316. let cdk_id = transaction_id.try_into()?;
  317. let result = self
  318. .inner
  319. .get_transaction(cdk_id)
  320. .await
  321. .map_err(|e| FfiError::Database { msg: e.to_string() })?;
  322. Ok(result.map(Into::into))
  323. }
  324. async fn list_transactions(
  325. &self,
  326. mint_url: Option<MintUrl>,
  327. direction: Option<TransactionDirection>,
  328. unit: Option<CurrencyUnit>,
  329. ) -> Result<Vec<Transaction>, FfiError> {
  330. let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
  331. let cdk_direction = direction.map(Into::into);
  332. let cdk_unit = unit.map(Into::into);
  333. let result = self
  334. .inner
  335. .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
  336. .await
  337. .map_err(|e| FfiError::Database { msg: e.to_string() })?;
  338. Ok(result.into_iter().map(Into::into).collect())
  339. }
  340. async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
  341. let cdk_id = transaction_id.try_into()?;
  342. self.inner
  343. .remove_transaction(cdk_id)
  344. .await
  345. .map_err(|e| FfiError::Database { msg: e.to_string() })
  346. }
  347. }
  348. #[uniffi::export]
  349. impl WalletPostgresDatabase {
  350. /// Create a new Postgres-backed wallet database
  351. /// Requires cdk-ffi to be built with feature "postgres".
  352. /// Example URL:
  353. /// "host=localhost user=test password=test dbname=testdb port=5433 schema=wallet sslmode=prefer"
  354. #[cfg(feature = "postgres")]
  355. #[uniffi::constructor]
  356. pub fn new(url: String) -> Result<Arc<Self>, FfiError> {
  357. let inner = match tokio::runtime::Handle::try_current() {
  358. Ok(handle) => tokio::task::block_in_place(|| {
  359. handle.block_on(
  360. async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await },
  361. )
  362. }),
  363. // Important: use a process-long runtime so background connection tasks stay alive.
  364. Err(_) => pg_runtime()
  365. .block_on(async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await }),
  366. }
  367. .map_err(|e| FfiError::Database { msg: e.to_string() })?;
  368. Ok(Arc::new(WalletPostgresDatabase {
  369. inner: Arc::new(inner),
  370. }))
  371. }
  372. fn clone_as_trait(&self) -> Arc<dyn WalletDatabase> {
  373. // Safety: UniFFI objects are reference counted and Send+Sync via Arc
  374. let obj: Arc<dyn WalletDatabase> = Arc::new(WalletPostgresDatabase {
  375. inner: self.inner.clone(),
  376. });
  377. obj
  378. }
  379. }