multi_mint_wallet.rs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. //! FFI MultiMintWallet bindings
  2. use std::collections::HashMap;
  3. use std::str::FromStr;
  4. use std::sync::Arc;
  5. use bip39::Mnemonic;
  6. use cdk::wallet::multi_mint_wallet::{
  7. MultiMintReceiveOptions as CdkMultiMintReceiveOptions,
  8. MultiMintSendOptions as CdkMultiMintSendOptions, MultiMintWallet as CdkMultiMintWallet,
  9. TransferMode as CdkTransferMode, TransferResult as CdkTransferResult,
  10. };
  11. use crate::error::FfiError;
  12. use crate::token::Token;
  13. use crate::types::*;
  14. /// FFI-compatible MultiMintWallet
  15. #[derive(uniffi::Object)]
  16. pub struct MultiMintWallet {
  17. inner: Arc<CdkMultiMintWallet>,
  18. }
  19. #[uniffi::export(async_runtime = "tokio")]
  20. impl MultiMintWallet {
  21. /// Create a new MultiMintWallet from mnemonic using WalletDatabaseFfi trait
  22. #[uniffi::constructor]
  23. pub fn new(
  24. unit: CurrencyUnit,
  25. mnemonic: String,
  26. db: Arc<dyn crate::database::WalletDatabase>,
  27. ) -> Result<Self, FfiError> {
  28. // Parse mnemonic and generate seed without passphrase
  29. let m = Mnemonic::parse(&mnemonic)
  30. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  31. let seed = m.to_seed_normalized("");
  32. // Convert the FFI database trait to a CDK database implementation
  33. let localstore = crate::database::create_cdk_database_from_ffi(db);
  34. let wallet = match tokio::runtime::Handle::try_current() {
  35. Ok(handle) => tokio::task::block_in_place(|| {
  36. handle.block_on(async move {
  37. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  38. })
  39. }),
  40. Err(_) => {
  41. // No current runtime, create a new one
  42. tokio::runtime::Runtime::new()
  43. .map_err(|e| FfiError::Database {
  44. msg: format!("Failed to create runtime: {}", e),
  45. })?
  46. .block_on(async move {
  47. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  48. })
  49. }
  50. }?;
  51. Ok(Self {
  52. inner: Arc::new(wallet),
  53. })
  54. }
  55. /// Create a new MultiMintWallet with proxy configuration
  56. #[uniffi::constructor]
  57. pub fn new_with_proxy(
  58. unit: CurrencyUnit,
  59. mnemonic: String,
  60. db: Arc<dyn crate::database::WalletDatabase>,
  61. proxy_url: String,
  62. ) -> Result<Self, FfiError> {
  63. // Parse mnemonic and generate seed without passphrase
  64. let m = Mnemonic::parse(&mnemonic)
  65. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  66. let seed = m.to_seed_normalized("");
  67. // Convert the FFI database trait to a CDK database implementation
  68. let localstore = crate::database::create_cdk_database_from_ffi(db);
  69. // Parse proxy URL
  70. let proxy_url =
  71. url::Url::parse(&proxy_url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;
  72. let wallet = match tokio::runtime::Handle::try_current() {
  73. Ok(handle) => tokio::task::block_in_place(|| {
  74. handle.block_on(async move {
  75. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  76. .await
  77. })
  78. }),
  79. Err(_) => {
  80. // No current runtime, create a new one
  81. tokio::runtime::Runtime::new()
  82. .map_err(|e| FfiError::Database {
  83. msg: format!("Failed to create runtime: {}", e),
  84. })?
  85. .block_on(async move {
  86. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  87. .await
  88. })
  89. }
  90. }?;
  91. Ok(Self {
  92. inner: Arc::new(wallet),
  93. })
  94. }
  95. /// Get the currency unit for this wallet
  96. pub fn unit(&self) -> CurrencyUnit {
  97. self.inner.unit().clone().into()
  98. }
  99. /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
  100. ///
  101. /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
  102. /// before requiring a refresh from the mint server for a specific mint.
  103. ///
  104. /// # Arguments
  105. ///
  106. /// * `mint_url` - The mint URL to set the TTL for
  107. /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
  108. pub async fn set_metadata_cache_ttl_for_mint(
  109. &self,
  110. mint_url: MintUrl,
  111. ttl_secs: Option<u64>,
  112. ) -> Result<(), FfiError> {
  113. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  114. let wallets = self.inner.get_wallets().await;
  115. if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
  116. let ttl = ttl_secs.map(std::time::Duration::from_secs);
  117. wallet.set_metadata_cache_ttl(ttl);
  118. Ok(())
  119. } else {
  120. Err(FfiError::Generic {
  121. msg: format!("Mint not found: {}", cdk_mint_url),
  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 MultiMintWallet.
  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. for wallet in wallets.iter() {
  137. wallet.set_metadata_cache_ttl(ttl);
  138. }
  139. }
  140. /// Add a mint to this MultiMintWallet
  141. pub async fn add_mint(
  142. &self,
  143. mint_url: MintUrl,
  144. target_proof_count: Option<u32>,
  145. ) -> Result<(), FfiError> {
  146. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  147. if let Some(count) = target_proof_count {
  148. let config = cdk::wallet::multi_mint_wallet::WalletConfig::new()
  149. .with_target_proof_count(count as usize);
  150. self.inner
  151. .add_mint_with_config(cdk_mint_url, config)
  152. .await?;
  153. } else {
  154. self.inner.add_mint(cdk_mint_url).await?;
  155. }
  156. Ok(())
  157. }
  158. /// Remove mint from MultiMintWallet
  159. pub async fn remove_mint(&self, mint_url: MintUrl) {
  160. let url_str = mint_url.url.clone();
  161. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().unwrap_or_else(|_| {
  162. // If conversion fails, we can't remove the mint, but we shouldn't panic
  163. // This is a best-effort operation
  164. cdk::mint_url::MintUrl::from_str(&url_str).unwrap_or_else(|_| {
  165. // Last resort: create a dummy URL that won't match anything
  166. cdk::mint_url::MintUrl::from_str("https://invalid.mint").unwrap()
  167. })
  168. });
  169. self.inner.remove_mint(&cdk_mint_url).await;
  170. }
  171. /// Check if mint is in wallet
  172. pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
  173. if let Ok(cdk_mint_url) = mint_url.try_into() {
  174. self.inner.has_mint(&cdk_mint_url).await
  175. } else {
  176. false
  177. }
  178. }
  179. /// Get wallet balances for all mints
  180. pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
  181. let balances = self.inner.get_balances().await?;
  182. let mut balance_map = HashMap::new();
  183. for (mint_url, amount) in balances {
  184. balance_map.insert(mint_url.to_string(), amount.into());
  185. }
  186. Ok(balance_map)
  187. }
  188. /// Get total balance across all mints
  189. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  190. let total = self.inner.total_balance().await?;
  191. Ok(total.into())
  192. }
  193. /// List proofs for all mints
  194. pub async fn list_proofs(&self) -> Result<ProofsByMint, FfiError> {
  195. let proofs = self.inner.list_proofs().await?;
  196. let mut proofs_by_mint = HashMap::new();
  197. for (mint_url, mint_proofs) in proofs {
  198. let ffi_proofs: Vec<Proof> = mint_proofs.into_iter().map(|p| p.into()).collect();
  199. proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
  200. }
  201. Ok(proofs_by_mint)
  202. }
  203. /// Receive token
  204. pub async fn receive(
  205. &self,
  206. token: Arc<Token>,
  207. options: MultiMintReceiveOptions,
  208. ) -> Result<Amount, FfiError> {
  209. let amount = self
  210. .inner
  211. .receive(&token.to_string(), options.into())
  212. .await?;
  213. Ok(amount.into())
  214. }
  215. /// Restore wallets for a specific mint
  216. pub async fn restore(&self, mint_url: MintUrl) -> Result<Amount, FfiError> {
  217. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  218. let amount = self.inner.restore(&cdk_mint_url).await?;
  219. Ok(amount.into())
  220. }
  221. /// Prepare a send operation from a specific mint
  222. pub async fn prepare_send(
  223. &self,
  224. mint_url: MintUrl,
  225. amount: Amount,
  226. options: MultiMintSendOptions,
  227. ) -> Result<Arc<PreparedSend>, FfiError> {
  228. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  229. let prepared = self
  230. .inner
  231. .prepare_send(cdk_mint_url, amount.into(), options.into())
  232. .await?;
  233. Ok(Arc::new(prepared.into()))
  234. }
  235. /// Get a mint quote from a specific mint
  236. pub async fn mint_quote(
  237. &self,
  238. mint_url: MintUrl,
  239. amount: Amount,
  240. description: Option<String>,
  241. ) -> Result<MintQuote, FfiError> {
  242. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  243. let quote = self
  244. .inner
  245. .mint_quote(&cdk_mint_url, amount.into(), description)
  246. .await?;
  247. Ok(quote.into())
  248. }
  249. /// Check a specific mint quote status
  250. pub async fn check_mint_quote(
  251. &self,
  252. mint_url: MintUrl,
  253. quote_id: String,
  254. ) -> Result<MintQuote, FfiError> {
  255. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  256. let quote = self
  257. .inner
  258. .check_mint_quote(&cdk_mint_url, &quote_id)
  259. .await?;
  260. Ok(quote.into())
  261. }
  262. /// Mint tokens at a specific mint
  263. pub async fn mint(
  264. &self,
  265. mint_url: MintUrl,
  266. quote_id: String,
  267. spending_conditions: Option<SpendingConditions>,
  268. ) -> Result<Proofs, FfiError> {
  269. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  270. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  271. let proofs = self
  272. .inner
  273. .mint(&cdk_mint_url, &quote_id, conditions)
  274. .await?;
  275. Ok(proofs.into_iter().map(|p| p.into()).collect())
  276. }
  277. /// Wait for a mint quote to be paid and automatically mint the proofs
  278. #[cfg(not(target_arch = "wasm32"))]
  279. pub async fn wait_for_mint_quote(
  280. &self,
  281. mint_url: MintUrl,
  282. quote_id: String,
  283. split_target: SplitTarget,
  284. spending_conditions: Option<SpendingConditions>,
  285. timeout_secs: u64,
  286. ) -> Result<Proofs, FfiError> {
  287. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  288. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  289. let proofs = self
  290. .inner
  291. .wait_for_mint_quote(
  292. &cdk_mint_url,
  293. &quote_id,
  294. split_target.into(),
  295. conditions,
  296. timeout_secs,
  297. )
  298. .await?;
  299. Ok(proofs.into_iter().map(|p| p.into()).collect())
  300. }
  301. /// Get a melt quote from a specific mint
  302. pub async fn melt_quote(
  303. &self,
  304. mint_url: MintUrl,
  305. request: String,
  306. options: Option<MeltOptions>,
  307. ) -> Result<MeltQuote, FfiError> {
  308. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  309. let cdk_options = options.map(Into::into);
  310. let quote = self
  311. .inner
  312. .melt_quote(&cdk_mint_url, request, cdk_options)
  313. .await?;
  314. Ok(quote.into())
  315. }
  316. /// Get a melt quote for a BIP353 human-readable address
  317. ///
  318. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  319. /// and then creates a melt quote for that offer at the specified mint.
  320. ///
  321. /// # Arguments
  322. ///
  323. /// * `mint_url` - The mint to use for creating the melt quote
  324. /// * `bip353_address` - Human-readable address in the format "user@domain.com"
  325. /// * `amount_msat` - Amount to pay in millisatoshis
  326. #[cfg(not(target_arch = "wasm32"))]
  327. pub async fn melt_bip353_quote(
  328. &self,
  329. mint_url: MintUrl,
  330. bip353_address: String,
  331. amount_msat: u64,
  332. ) -> Result<MeltQuote, FfiError> {
  333. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  334. let cdk_amount = cdk::Amount::from(amount_msat);
  335. let quote = self
  336. .inner
  337. .melt_bip353_quote(&cdk_mint_url, &bip353_address, cdk_amount)
  338. .await?;
  339. Ok(quote.into())
  340. }
  341. /// Get a melt quote for a Lightning address
  342. ///
  343. /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
  344. /// and then creates a melt quote for that invoice at the specified mint.
  345. ///
  346. /// # Arguments
  347. ///
  348. /// * `mint_url` - The mint to use for creating the melt quote
  349. /// * `lightning_address` - Lightning address in the format "user@domain.com"
  350. /// * `amount_msat` - Amount to pay in millisatoshis
  351. pub async fn melt_lightning_address_quote(
  352. &self,
  353. mint_url: MintUrl,
  354. lightning_address: String,
  355. amount_msat: u64,
  356. ) -> Result<MeltQuote, FfiError> {
  357. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  358. let cdk_amount = cdk::Amount::from(amount_msat);
  359. let quote = self
  360. .inner
  361. .melt_lightning_address_quote(&cdk_mint_url, &lightning_address, cdk_amount)
  362. .await?;
  363. Ok(quote.into())
  364. }
  365. /// Get a melt quote for a human-readable address
  366. ///
  367. /// This method accepts a human-readable address that could be either a BIP353 address
  368. /// or a Lightning address. It intelligently determines which to try based on mint support:
  369. ///
  370. /// 1. If the mint supports Bolt12, it tries BIP353 first
  371. /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
  372. /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
  373. /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
  374. ///
  375. /// # Arguments
  376. ///
  377. /// * `mint_url` - The mint to use for creating the melt quote
  378. /// * `address` - Human-readable address (BIP353 or Lightning address)
  379. /// * `amount_msat` - Amount to pay in millisatoshis
  380. #[cfg(not(target_arch = "wasm32"))]
  381. pub async fn melt_human_readable_quote(
  382. &self,
  383. mint_url: MintUrl,
  384. address: String,
  385. amount_msat: u64,
  386. ) -> Result<MeltQuote, FfiError> {
  387. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  388. let cdk_amount = cdk::Amount::from(amount_msat);
  389. let quote = self
  390. .inner
  391. .melt_human_readable_quote(&cdk_mint_url, &address, cdk_amount)
  392. .await?;
  393. Ok(quote.into())
  394. }
  395. /// Melt tokens
  396. pub async fn melt_with_mint(
  397. &self,
  398. mint_url: MintUrl,
  399. quote_id: String,
  400. ) -> Result<Melted, FfiError> {
  401. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  402. let melted = self.inner.melt_with_mint(&cdk_mint_url, &quote_id).await?;
  403. Ok(melted.into())
  404. }
  405. /// Melt specific proofs from a specific mint
  406. ///
  407. /// This method allows melting proofs that may not be in the wallet's database,
  408. /// similar to how `receive_proofs` handles external proofs. The proofs will be
  409. /// added to the database and used for the melt operation.
  410. ///
  411. /// # Arguments
  412. ///
  413. /// * `mint_url` - The mint to use for the melt operation
  414. /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
  415. /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
  416. ///
  417. /// # Returns
  418. ///
  419. /// A `Melted` result containing the payment details and any change proofs
  420. pub async fn melt_proofs(
  421. &self,
  422. mint_url: MintUrl,
  423. quote_id: String,
  424. proofs: Proofs,
  425. ) -> Result<Melted, FfiError> {
  426. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  427. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  428. proofs.into_iter().map(|p| p.try_into()).collect();
  429. let cdk_proofs = cdk_proofs?;
  430. let melted = self
  431. .inner
  432. .melt_proofs(&cdk_mint_url, &quote_id, cdk_proofs)
  433. .await?;
  434. Ok(melted.into())
  435. }
  436. /// Check melt quote status
  437. pub async fn check_melt_quote(
  438. &self,
  439. mint_url: MintUrl,
  440. quote_id: String,
  441. ) -> Result<MeltQuote, FfiError> {
  442. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  443. let melted = self
  444. .inner
  445. .check_melt_quote(&cdk_mint_url, &quote_id)
  446. .await?;
  447. Ok(melted.into())
  448. }
  449. /// Melt tokens (pay a bolt11 invoice)
  450. pub async fn melt(
  451. &self,
  452. bolt11: String,
  453. options: Option<MeltOptions>,
  454. max_fee: Option<Amount>,
  455. ) -> Result<Melted, FfiError> {
  456. let cdk_options = options.map(Into::into);
  457. let cdk_max_fee = max_fee.map(Into::into);
  458. let melted = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
  459. Ok(melted.into())
  460. }
  461. /// Transfer funds between mints
  462. pub async fn transfer(
  463. &self,
  464. source_mint: MintUrl,
  465. target_mint: MintUrl,
  466. transfer_mode: TransferMode,
  467. ) -> Result<TransferResult, FfiError> {
  468. let source_cdk: cdk::mint_url::MintUrl = source_mint.try_into()?;
  469. let target_cdk: cdk::mint_url::MintUrl = target_mint.try_into()?;
  470. let result = self
  471. .inner
  472. .transfer(&source_cdk, &target_cdk, transfer_mode.into())
  473. .await?;
  474. Ok(result.into())
  475. }
  476. /// Swap proofs with automatic wallet selection
  477. pub async fn swap(
  478. &self,
  479. amount: Option<Amount>,
  480. spending_conditions: Option<SpendingConditions>,
  481. ) -> Result<Option<Proofs>, FfiError> {
  482. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  483. let result = self.inner.swap(amount.map(Into::into), conditions).await?;
  484. Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
  485. }
  486. /// List transactions from all mints
  487. pub async fn list_transactions(
  488. &self,
  489. direction: Option<TransactionDirection>,
  490. ) -> Result<Vec<Transaction>, FfiError> {
  491. let cdk_direction = direction.map(Into::into);
  492. let transactions = self.inner.list_transactions(cdk_direction).await?;
  493. Ok(transactions.into_iter().map(Into::into).collect())
  494. }
  495. /// Get proofs for a transaction by transaction ID
  496. ///
  497. /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
  498. /// it will only check that specific mint's wallet. Otherwise, it searches across all
  499. /// wallets to find which mint the transaction belongs to.
  500. ///
  501. /// # Arguments
  502. ///
  503. /// * `id` - The transaction ID
  504. /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
  505. pub async fn get_proofs_for_transaction(
  506. &self,
  507. id: TransactionId,
  508. mint_url: Option<MintUrl>,
  509. ) -> Result<Vec<Proof>, FfiError> {
  510. let cdk_id = id.try_into()?;
  511. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  512. let proofs = self
  513. .inner
  514. .get_proofs_for_transaction(cdk_id, cdk_mint_url)
  515. .await?;
  516. Ok(proofs.into_iter().map(Into::into).collect())
  517. }
  518. /// Check all mint quotes and mint if paid
  519. pub async fn check_all_mint_quotes(
  520. &self,
  521. mint_url: Option<MintUrl>,
  522. ) -> Result<Amount, FfiError> {
  523. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  524. let amount = self.inner.check_all_mint_quotes(cdk_mint_url).await?;
  525. Ok(amount.into())
  526. }
  527. /// Consolidate proofs across all mints
  528. pub async fn consolidate(&self) -> Result<Amount, FfiError> {
  529. let amount = self.inner.consolidate().await?;
  530. Ok(amount.into())
  531. }
  532. /// Get list of mint URLs
  533. pub async fn get_mint_urls(&self) -> Vec<String> {
  534. let wallets = self.inner.get_wallets().await;
  535. wallets.iter().map(|w| w.mint_url.to_string()).collect()
  536. }
  537. /// Get all wallets from MultiMintWallet
  538. pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
  539. let wallets = self.inner.get_wallets().await;
  540. wallets
  541. .into_iter()
  542. .map(|w| Arc::new(crate::wallet::Wallet::from_inner(Arc::new(w))))
  543. .collect()
  544. }
  545. /// Get a specific wallet from MultiMintWallet by mint URL
  546. pub async fn get_wallet(&self, mint_url: MintUrl) -> Option<Arc<crate::wallet::Wallet>> {
  547. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().ok()?;
  548. let wallet = self.inner.get_wallet(&cdk_mint_url).await?;
  549. Some(Arc::new(crate::wallet::Wallet::from_inner(Arc::new(
  550. wallet,
  551. ))))
  552. }
  553. /// Verify token DLEQ proofs
  554. pub async fn verify_token_dleq(&self, token: Arc<Token>) -> Result<(), FfiError> {
  555. let cdk_token = token.inner.clone();
  556. self.inner.verify_token_dleq(&cdk_token).await?;
  557. Ok(())
  558. }
  559. /// Query mint for current mint information
  560. pub async fn fetch_mint_info(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
  561. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  562. let mint_info = self.inner.fetch_mint_info(&cdk_mint_url).await?;
  563. Ok(mint_info.map(Into::into))
  564. }
  565. }
  566. /// Auth methods for MultiMintWallet
  567. #[uniffi::export(async_runtime = "tokio")]
  568. impl MultiMintWallet {
  569. /// Set Clear Auth Token (CAT) for a specific mint
  570. pub async fn set_cat(&self, mint_url: MintUrl, cat: String) -> Result<(), FfiError> {
  571. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  572. self.inner.set_cat(&cdk_mint_url, cat).await?;
  573. Ok(())
  574. }
  575. /// Set refresh token for a specific mint
  576. pub async fn set_refresh_token(
  577. &self,
  578. mint_url: MintUrl,
  579. refresh_token: String,
  580. ) -> Result<(), FfiError> {
  581. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  582. self.inner
  583. .set_refresh_token(&cdk_mint_url, refresh_token)
  584. .await?;
  585. Ok(())
  586. }
  587. /// Refresh access token for a specific mint using the stored refresh token
  588. pub async fn refresh_access_token(&self, mint_url: MintUrl) -> Result<(), FfiError> {
  589. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  590. self.inner.refresh_access_token(&cdk_mint_url).await?;
  591. Ok(())
  592. }
  593. /// Mint blind auth tokens at a specific mint
  594. pub async fn mint_blind_auth(
  595. &self,
  596. mint_url: MintUrl,
  597. amount: Amount,
  598. ) -> Result<Proofs, FfiError> {
  599. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  600. let proofs = self
  601. .inner
  602. .mint_blind_auth(&cdk_mint_url, amount.into())
  603. .await?;
  604. Ok(proofs.into_iter().map(|p| p.into()).collect())
  605. }
  606. /// Get unspent auth proofs for a specific mint
  607. pub async fn get_unspent_auth_proofs(
  608. &self,
  609. mint_url: MintUrl,
  610. ) -> Result<Vec<AuthProof>, FfiError> {
  611. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  612. let auth_proofs = self.inner.get_unspent_auth_proofs(&cdk_mint_url).await?;
  613. Ok(auth_proofs.into_iter().map(Into::into).collect())
  614. }
  615. }
  616. /// Transfer mode for mint-to-mint transfers
  617. #[derive(Debug, Clone, uniffi::Enum)]
  618. pub enum TransferMode {
  619. /// Transfer exact amount to target (target receives specified amount)
  620. ExactReceive { amount: Amount },
  621. /// Transfer all available balance (source will be emptied)
  622. FullBalance,
  623. }
  624. impl From<TransferMode> for CdkTransferMode {
  625. fn from(mode: TransferMode) -> Self {
  626. match mode {
  627. TransferMode::ExactReceive { amount } => CdkTransferMode::ExactReceive(amount.into()),
  628. TransferMode::FullBalance => CdkTransferMode::FullBalance,
  629. }
  630. }
  631. }
  632. /// Result of a transfer operation with detailed breakdown
  633. #[derive(Debug, Clone, uniffi::Record)]
  634. pub struct TransferResult {
  635. /// Amount deducted from source mint
  636. pub amount_sent: Amount,
  637. /// Amount received at target mint
  638. pub amount_received: Amount,
  639. /// Total fees paid for the transfer
  640. pub fees_paid: Amount,
  641. /// Remaining balance in source mint after transfer
  642. pub source_balance_after: Amount,
  643. /// New balance in target mint after transfer
  644. pub target_balance_after: Amount,
  645. }
  646. impl From<CdkTransferResult> for TransferResult {
  647. fn from(result: CdkTransferResult) -> Self {
  648. Self {
  649. amount_sent: result.amount_sent.into(),
  650. amount_received: result.amount_received.into(),
  651. fees_paid: result.fees_paid.into(),
  652. source_balance_after: result.source_balance_after.into(),
  653. target_balance_after: result.target_balance_after.into(),
  654. }
  655. }
  656. }
  657. /// Options for receiving tokens in multi-mint context
  658. #[derive(Debug, Clone, Default, uniffi::Record)]
  659. pub struct MultiMintReceiveOptions {
  660. /// Whether to allow receiving from untrusted (not yet added) mints
  661. pub allow_untrusted: bool,
  662. /// Mint URL to transfer tokens to from untrusted mints (None means keep in original mint)
  663. pub transfer_to_mint: Option<MintUrl>,
  664. /// Base receive options to apply to the wallet receive
  665. pub receive_options: ReceiveOptions,
  666. }
  667. impl From<MultiMintReceiveOptions> for CdkMultiMintReceiveOptions {
  668. fn from(options: MultiMintReceiveOptions) -> Self {
  669. let mut opts = CdkMultiMintReceiveOptions::new();
  670. opts.allow_untrusted = options.allow_untrusted;
  671. opts.transfer_to_mint = options.transfer_to_mint.and_then(|url| url.try_into().ok());
  672. opts.receive_options = options.receive_options.into();
  673. opts
  674. }
  675. }
  676. /// Options for sending tokens in multi-mint context
  677. #[derive(Debug, Clone, Default, uniffi::Record)]
  678. pub struct MultiMintSendOptions {
  679. /// Whether to allow transferring funds from other mints if needed
  680. pub allow_transfer: bool,
  681. /// Maximum amount to transfer from other mints (optional limit)
  682. pub max_transfer_amount: Option<Amount>,
  683. /// Specific mint URLs allowed for transfers (empty means all mints allowed)
  684. pub allowed_mints: Vec<MintUrl>,
  685. /// Specific mint URLs to exclude from transfers
  686. pub excluded_mints: Vec<MintUrl>,
  687. /// Base send options to apply to the wallet send
  688. pub send_options: SendOptions,
  689. }
  690. impl From<MultiMintSendOptions> for CdkMultiMintSendOptions {
  691. fn from(options: MultiMintSendOptions) -> Self {
  692. let mut opts = CdkMultiMintSendOptions::new();
  693. opts.allow_transfer = options.allow_transfer;
  694. opts.max_transfer_amount = options.max_transfer_amount.map(Into::into);
  695. opts.allowed_mints = options
  696. .allowed_mints
  697. .into_iter()
  698. .filter_map(|url| url.try_into().ok())
  699. .collect();
  700. opts.excluded_mints = options
  701. .excluded_mints
  702. .into_iter()
  703. .filter_map(|url| url.try_into().ok())
  704. .collect();
  705. opts.send_options = options.send_options.into();
  706. opts
  707. }
  708. }
  709. /// Type alias for balances by mint URL
  710. pub type BalanceMap = HashMap<String, Amount>;
  711. /// Type alias for proofs by mint URL
  712. pub type ProofsByMint = HashMap<String, Vec<Proof>>;