multi_mint_wallet.rs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  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. TokenData as CdkTokenData, TransferMode as CdkTransferMode,
  10. TransferResult as CdkTransferResult,
  11. };
  12. use crate::error::FfiError;
  13. use crate::token::Token;
  14. use crate::types::payment_request::{
  15. CreateRequestParams, CreateRequestResult, NostrWaitInfo, PaymentRequest,
  16. };
  17. use crate::types::*;
  18. /// FFI-compatible MultiMintWallet
  19. #[derive(uniffi::Object)]
  20. pub struct MultiMintWallet {
  21. inner: Arc<CdkMultiMintWallet>,
  22. }
  23. #[uniffi::export(async_runtime = "tokio")]
  24. impl MultiMintWallet {
  25. /// Create a new MultiMintWallet from mnemonic using WalletDatabaseFfi trait
  26. #[uniffi::constructor]
  27. pub fn new(
  28. unit: CurrencyUnit,
  29. mnemonic: String,
  30. db: Arc<dyn crate::database::WalletDatabase>,
  31. ) -> Result<Self, FfiError> {
  32. // Parse mnemonic and generate seed without passphrase
  33. let m = Mnemonic::parse(&mnemonic)
  34. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  35. let seed = m.to_seed_normalized("");
  36. // Convert the FFI database trait to a CDK database implementation
  37. let localstore = crate::database::create_cdk_database_from_ffi(db);
  38. let wallet = match tokio::runtime::Handle::try_current() {
  39. Ok(handle) => tokio::task::block_in_place(|| {
  40. handle.block_on(async move {
  41. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  42. })
  43. }),
  44. Err(_) => {
  45. // No current runtime, create a new one
  46. tokio::runtime::Runtime::new()
  47. .map_err(|e| FfiError::Database {
  48. msg: format!("Failed to create runtime: {}", e),
  49. })?
  50. .block_on(async move {
  51. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  52. })
  53. }
  54. }?;
  55. Ok(Self {
  56. inner: Arc::new(wallet),
  57. })
  58. }
  59. /// Create a new MultiMintWallet with proxy configuration
  60. #[uniffi::constructor]
  61. pub fn new_with_proxy(
  62. unit: CurrencyUnit,
  63. mnemonic: String,
  64. db: Arc<dyn crate::database::WalletDatabase>,
  65. proxy_url: String,
  66. ) -> Result<Self, FfiError> {
  67. // Parse mnemonic and generate seed without passphrase
  68. let m = Mnemonic::parse(&mnemonic)
  69. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  70. let seed = m.to_seed_normalized("");
  71. // Convert the FFI database trait to a CDK database implementation
  72. let localstore = crate::database::create_cdk_database_from_ffi(db);
  73. // Parse proxy URL
  74. let proxy_url =
  75. url::Url::parse(&proxy_url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;
  76. let wallet = match tokio::runtime::Handle::try_current() {
  77. Ok(handle) => tokio::task::block_in_place(|| {
  78. handle.block_on(async move {
  79. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  80. .await
  81. })
  82. }),
  83. Err(_) => {
  84. // No current runtime, create a new one
  85. tokio::runtime::Runtime::new()
  86. .map_err(|e| FfiError::Database {
  87. msg: format!("Failed to create runtime: {}", e),
  88. })?
  89. .block_on(async move {
  90. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  91. .await
  92. })
  93. }
  94. }?;
  95. Ok(Self {
  96. inner: Arc::new(wallet),
  97. })
  98. }
  99. /// Get the currency unit for this wallet
  100. pub fn unit(&self) -> CurrencyUnit {
  101. self.inner.unit().clone().into()
  102. }
  103. /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
  104. ///
  105. /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
  106. /// before requiring a refresh from the mint server for a specific mint.
  107. ///
  108. /// # Arguments
  109. ///
  110. /// * `mint_url` - The mint URL to set the TTL for
  111. /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
  112. pub async fn set_metadata_cache_ttl_for_mint(
  113. &self,
  114. mint_url: MintUrl,
  115. ttl_secs: Option<u64>,
  116. ) -> Result<(), FfiError> {
  117. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  118. let wallets = self.inner.get_wallets().await;
  119. if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
  120. let ttl = ttl_secs.map(std::time::Duration::from_secs);
  121. wallet.set_metadata_cache_ttl(ttl);
  122. Ok(())
  123. } else {
  124. Err(FfiError::Generic {
  125. msg: format!("Mint not found: {}", cdk_mint_url),
  126. })
  127. }
  128. }
  129. /// Set metadata cache TTL (time-to-live) in seconds for all mints
  130. ///
  131. /// Controls how long cached mint metadata is considered fresh for all mints
  132. /// in this MultiMintWallet.
  133. ///
  134. /// # Arguments
  135. ///
  136. /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
  137. pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
  138. let wallets = self.inner.get_wallets().await;
  139. let ttl = ttl_secs.map(std::time::Duration::from_secs);
  140. for wallet in wallets.iter() {
  141. wallet.set_metadata_cache_ttl(ttl);
  142. }
  143. }
  144. /// Add a mint to this MultiMintWallet
  145. pub async fn add_mint(
  146. &self,
  147. mint_url: MintUrl,
  148. target_proof_count: Option<u32>,
  149. ) -> Result<(), FfiError> {
  150. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  151. if let Some(count) = target_proof_count {
  152. let config = cdk::wallet::multi_mint_wallet::WalletConfig::new()
  153. .with_target_proof_count(count as usize);
  154. self.inner
  155. .add_mint_with_config(cdk_mint_url, config)
  156. .await?;
  157. } else {
  158. self.inner.add_mint(cdk_mint_url).await?;
  159. }
  160. Ok(())
  161. }
  162. /// Remove mint from MultiMintWallet
  163. ///
  164. /// # Panics
  165. ///
  166. /// Panics if the hardcoded fallback URL is invalid (should never happen).
  167. pub async fn remove_mint(&self, mint_url: MintUrl) {
  168. let url_str = mint_url.url.clone();
  169. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().unwrap_or_else(|_| {
  170. // If conversion fails, we can't remove the mint, but we shouldn't panic
  171. // This is a best-effort operation
  172. cdk::mint_url::MintUrl::from_str(&url_str).unwrap_or_else(|_| {
  173. // Last resort: create a dummy URL that won't match anything
  174. cdk::mint_url::MintUrl::from_str("https://invalid.mint")
  175. .expect("Valid hardcoded URL")
  176. })
  177. });
  178. self.inner.remove_mint(&cdk_mint_url).await;
  179. }
  180. /// Check if mint is in wallet
  181. pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
  182. if let Ok(cdk_mint_url) = mint_url.try_into() {
  183. self.inner.has_mint(&cdk_mint_url).await
  184. } else {
  185. false
  186. }
  187. }
  188. pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Vec<KeySetInfo>, FfiError> {
  189. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  190. let keysets = self.inner.get_mint_keysets(&cdk_mint_url).await?;
  191. let keysets = keysets.into_iter().map(|k| k.into()).collect();
  192. Ok(keysets)
  193. }
  194. /// Get token data (mint URL and proofs) from a token
  195. ///
  196. /// This method extracts the mint URL and proofs from a token. It will automatically
  197. /// fetch the keysets from the mint if needed to properly decode the proofs.
  198. ///
  199. /// The mint must already be added to the wallet. If the mint is not in the wallet,
  200. /// use `add_mint` first.
  201. pub async fn get_token_data(&self, token: Arc<Token>) -> Result<TokenData, FfiError> {
  202. let token_data = self.inner.get_token_data(&token.inner).await?;
  203. Ok(token_data.into())
  204. }
  205. /// Get wallet balances for all mints
  206. pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
  207. let balances = self.inner.get_balances().await?;
  208. let mut balance_map = HashMap::new();
  209. for (mint_url, amount) in balances {
  210. balance_map.insert(mint_url.to_string(), amount.into());
  211. }
  212. Ok(balance_map)
  213. }
  214. /// Get total balance across all mints
  215. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  216. let total = self.inner.total_balance().await?;
  217. Ok(total.into())
  218. }
  219. /// List proofs for all mints
  220. pub async fn list_proofs(&self) -> Result<ProofsByMint, FfiError> {
  221. let proofs = self.inner.list_proofs().await?;
  222. let mut proofs_by_mint = HashMap::new();
  223. for (mint_url, mint_proofs) in proofs {
  224. let ffi_proofs: Vec<Proof> = mint_proofs.into_iter().map(|p| p.into()).collect();
  225. proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
  226. }
  227. Ok(proofs_by_mint)
  228. }
  229. /// Check the state of proofs at a specific mint
  230. pub async fn check_proofs_state(
  231. &self,
  232. mint_url: MintUrl,
  233. proofs: Proofs,
  234. ) -> Result<Vec<ProofState>, FfiError> {
  235. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  236. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  237. proofs.into_iter().map(|p| p.try_into()).collect();
  238. let cdk_proofs = cdk_proofs?;
  239. let states = self
  240. .inner
  241. .check_proofs_state(&cdk_mint_url, cdk_proofs)
  242. .await?;
  243. Ok(states.into_iter().map(|s| s.into()).collect())
  244. }
  245. /// Receive token
  246. pub async fn receive(
  247. &self,
  248. token: Arc<Token>,
  249. options: MultiMintReceiveOptions,
  250. ) -> Result<Amount, FfiError> {
  251. let amount = self
  252. .inner
  253. .receive(&token.to_string(), options.into())
  254. .await?;
  255. Ok(amount.into())
  256. }
  257. /// Restore wallets for a specific mint
  258. pub async fn restore(&self, mint_url: MintUrl) -> Result<Amount, FfiError> {
  259. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  260. let amount = self.inner.restore(&cdk_mint_url).await?;
  261. Ok(amount.into())
  262. }
  263. /// Prepare a send operation from a specific mint
  264. pub async fn prepare_send(
  265. &self,
  266. mint_url: MintUrl,
  267. amount: Amount,
  268. options: MultiMintSendOptions,
  269. ) -> Result<Arc<PreparedSend>, FfiError> {
  270. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  271. let prepared = self
  272. .inner
  273. .prepare_send(cdk_mint_url, amount.into(), options.into())
  274. .await?;
  275. Ok(Arc::new(prepared.into()))
  276. }
  277. /// Get a mint quote from a specific mint
  278. pub async fn mint_quote(
  279. &self,
  280. mint_url: MintUrl,
  281. amount: Amount,
  282. description: Option<String>,
  283. ) -> Result<MintQuote, FfiError> {
  284. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  285. let quote = self
  286. .inner
  287. .mint_quote(&cdk_mint_url, amount.into(), description)
  288. .await?;
  289. Ok(quote.into())
  290. }
  291. /// Check a specific mint quote status
  292. pub async fn check_mint_quote(
  293. &self,
  294. mint_url: MintUrl,
  295. quote_id: String,
  296. ) -> Result<MintQuote, FfiError> {
  297. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  298. let quote = self
  299. .inner
  300. .check_mint_quote(&cdk_mint_url, &quote_id)
  301. .await?;
  302. Ok(quote.into())
  303. }
  304. /// Mint tokens at a specific mint
  305. pub async fn mint(
  306. &self,
  307. mint_url: MintUrl,
  308. quote_id: String,
  309. spending_conditions: Option<SpendingConditions>,
  310. ) -> Result<Proofs, FfiError> {
  311. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  312. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  313. let proofs = self
  314. .inner
  315. .mint(&cdk_mint_url, &quote_id, conditions)
  316. .await?;
  317. Ok(proofs.into_iter().map(|p| p.into()).collect())
  318. }
  319. /// Wait for a mint quote to be paid and automatically mint the proofs
  320. #[cfg(not(target_arch = "wasm32"))]
  321. pub async fn wait_for_mint_quote(
  322. &self,
  323. mint_url: MintUrl,
  324. quote_id: String,
  325. split_target: SplitTarget,
  326. spending_conditions: Option<SpendingConditions>,
  327. timeout_secs: u64,
  328. ) -> Result<Proofs, FfiError> {
  329. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  330. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  331. let proofs = self
  332. .inner
  333. .wait_for_mint_quote(
  334. &cdk_mint_url,
  335. &quote_id,
  336. split_target.into(),
  337. conditions,
  338. timeout_secs,
  339. )
  340. .await?;
  341. Ok(proofs.into_iter().map(|p| p.into()).collect())
  342. }
  343. /// Get a melt quote from a specific mint
  344. pub async fn melt_quote(
  345. &self,
  346. mint_url: MintUrl,
  347. request: String,
  348. options: Option<MeltOptions>,
  349. ) -> Result<MeltQuote, FfiError> {
  350. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  351. let cdk_options = options.map(Into::into);
  352. let quote = self
  353. .inner
  354. .melt_quote(&cdk_mint_url, request, cdk_options)
  355. .await?;
  356. Ok(quote.into())
  357. }
  358. /// Get a melt quote for a BIP353 human-readable address
  359. ///
  360. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  361. /// and then creates a melt quote for that offer at the specified mint.
  362. ///
  363. /// # Arguments
  364. ///
  365. /// * `mint_url` - The mint to use for creating the melt quote
  366. /// * `bip353_address` - Human-readable address in the format "user@domain.com"
  367. /// * `amount_msat` - Amount to pay in millisatoshis
  368. #[cfg(not(target_arch = "wasm32"))]
  369. pub async fn melt_bip353_quote(
  370. &self,
  371. mint_url: MintUrl,
  372. bip353_address: String,
  373. amount_msat: u64,
  374. ) -> Result<MeltQuote, FfiError> {
  375. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  376. let cdk_amount = cdk::Amount::from(amount_msat);
  377. let quote = self
  378. .inner
  379. .melt_bip353_quote(&cdk_mint_url, &bip353_address, cdk_amount)
  380. .await?;
  381. Ok(quote.into())
  382. }
  383. /// Get a melt quote for a Lightning address
  384. ///
  385. /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
  386. /// and then creates a melt quote for that invoice at the specified mint.
  387. ///
  388. /// # Arguments
  389. ///
  390. /// * `mint_url` - The mint to use for creating the melt quote
  391. /// * `lightning_address` - Lightning address in the format "user@domain.com"
  392. /// * `amount_msat` - Amount to pay in millisatoshis
  393. pub async fn melt_lightning_address_quote(
  394. &self,
  395. mint_url: MintUrl,
  396. lightning_address: String,
  397. amount_msat: u64,
  398. ) -> Result<MeltQuote, FfiError> {
  399. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  400. let cdk_amount = cdk::Amount::from(amount_msat);
  401. let quote = self
  402. .inner
  403. .melt_lightning_address_quote(&cdk_mint_url, &lightning_address, cdk_amount)
  404. .await?;
  405. Ok(quote.into())
  406. }
  407. /// Get a melt quote for a human-readable address
  408. ///
  409. /// This method accepts a human-readable address that could be either a BIP353 address
  410. /// or a Lightning address. It intelligently determines which to try based on mint support:
  411. ///
  412. /// 1. If the mint supports Bolt12, it tries BIP353 first
  413. /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
  414. /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
  415. /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
  416. ///
  417. /// # Arguments
  418. ///
  419. /// * `mint_url` - The mint to use for creating the melt quote
  420. /// * `address` - Human-readable address (BIP353 or Lightning address)
  421. /// * `amount_msat` - Amount to pay in millisatoshis
  422. #[cfg(not(target_arch = "wasm32"))]
  423. pub async fn melt_human_readable_quote(
  424. &self,
  425. mint_url: MintUrl,
  426. address: String,
  427. amount_msat: u64,
  428. ) -> Result<MeltQuote, FfiError> {
  429. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  430. let cdk_amount = cdk::Amount::from(amount_msat);
  431. let quote = self
  432. .inner
  433. .melt_human_readable_quote(&cdk_mint_url, &address, cdk_amount)
  434. .await?;
  435. Ok(quote.into())
  436. }
  437. /// Melt tokens
  438. pub async fn melt_with_mint(
  439. &self,
  440. mint_url: MintUrl,
  441. quote_id: String,
  442. ) -> Result<Melted, FfiError> {
  443. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  444. let melted = self.inner.melt_with_mint(&cdk_mint_url, &quote_id).await?;
  445. Ok(melted.into())
  446. }
  447. /// Melt specific proofs from a specific mint
  448. ///
  449. /// This method allows melting proofs that may not be in the wallet's database,
  450. /// similar to how `receive_proofs` handles external proofs. The proofs will be
  451. /// added to the database and used for the melt operation.
  452. ///
  453. /// # Arguments
  454. ///
  455. /// * `mint_url` - The mint to use for the melt operation
  456. /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
  457. /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
  458. ///
  459. /// # Returns
  460. ///
  461. /// A `Melted` result containing the payment details and any change proofs
  462. pub async fn melt_proofs(
  463. &self,
  464. mint_url: MintUrl,
  465. quote_id: String,
  466. proofs: Proofs,
  467. ) -> Result<Melted, FfiError> {
  468. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  469. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  470. proofs.into_iter().map(|p| p.try_into()).collect();
  471. let cdk_proofs = cdk_proofs?;
  472. let melted = self
  473. .inner
  474. .melt_proofs(&cdk_mint_url, &quote_id, cdk_proofs)
  475. .await?;
  476. Ok(melted.into())
  477. }
  478. /// Check melt quote status
  479. pub async fn check_melt_quote(
  480. &self,
  481. mint_url: MintUrl,
  482. quote_id: String,
  483. ) -> Result<MeltQuote, FfiError> {
  484. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  485. let melted = self
  486. .inner
  487. .check_melt_quote(&cdk_mint_url, &quote_id)
  488. .await?;
  489. Ok(melted.into())
  490. }
  491. /// Melt tokens (pay a bolt11 invoice)
  492. pub async fn melt(
  493. &self,
  494. bolt11: String,
  495. options: Option<MeltOptions>,
  496. max_fee: Option<Amount>,
  497. ) -> Result<Melted, FfiError> {
  498. let cdk_options = options.map(Into::into);
  499. let cdk_max_fee = max_fee.map(Into::into);
  500. let melted = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
  501. Ok(melted.into())
  502. }
  503. /// Transfer funds between mints
  504. pub async fn transfer(
  505. &self,
  506. source_mint: MintUrl,
  507. target_mint: MintUrl,
  508. transfer_mode: TransferMode,
  509. ) -> Result<TransferResult, FfiError> {
  510. let source_cdk: cdk::mint_url::MintUrl = source_mint.try_into()?;
  511. let target_cdk: cdk::mint_url::MintUrl = target_mint.try_into()?;
  512. let result = self
  513. .inner
  514. .transfer(&source_cdk, &target_cdk, transfer_mode.into())
  515. .await?;
  516. Ok(result.into())
  517. }
  518. /// Swap proofs with automatic wallet selection
  519. pub async fn swap(
  520. &self,
  521. amount: Option<Amount>,
  522. spending_conditions: Option<SpendingConditions>,
  523. ) -> Result<Option<Proofs>, FfiError> {
  524. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  525. let result = self.inner.swap(amount.map(Into::into), conditions).await?;
  526. Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
  527. }
  528. /// List transactions from all mints
  529. pub async fn list_transactions(
  530. &self,
  531. direction: Option<TransactionDirection>,
  532. ) -> Result<Vec<Transaction>, FfiError> {
  533. let cdk_direction = direction.map(Into::into);
  534. let transactions = self.inner.list_transactions(cdk_direction).await?;
  535. Ok(transactions.into_iter().map(Into::into).collect())
  536. }
  537. /// Get proofs for a transaction by transaction ID
  538. ///
  539. /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
  540. /// it will only check that specific mint's wallet. Otherwise, it searches across all
  541. /// wallets to find which mint the transaction belongs to.
  542. ///
  543. /// # Arguments
  544. ///
  545. /// * `id` - The transaction ID
  546. /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
  547. pub async fn get_proofs_for_transaction(
  548. &self,
  549. id: TransactionId,
  550. mint_url: Option<MintUrl>,
  551. ) -> Result<Vec<Proof>, FfiError> {
  552. let cdk_id = id.try_into()?;
  553. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  554. let proofs = self
  555. .inner
  556. .get_proofs_for_transaction(cdk_id, cdk_mint_url)
  557. .await?;
  558. Ok(proofs.into_iter().map(Into::into).collect())
  559. }
  560. /// Check all mint quotes and mint if paid
  561. pub async fn check_all_mint_quotes(
  562. &self,
  563. mint_url: Option<MintUrl>,
  564. ) -> Result<Amount, FfiError> {
  565. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  566. let amount = self.inner.check_all_mint_quotes(cdk_mint_url).await?;
  567. Ok(amount.into())
  568. }
  569. /// Consolidate proofs across all mints
  570. pub async fn consolidate(&self) -> Result<Amount, FfiError> {
  571. let amount = self.inner.consolidate().await?;
  572. Ok(amount.into())
  573. }
  574. /// Get list of mint URLs
  575. pub async fn get_mint_urls(&self) -> Vec<String> {
  576. let wallets = self.inner.get_wallets().await;
  577. wallets.iter().map(|w| w.mint_url.to_string()).collect()
  578. }
  579. /// Get all wallets from MultiMintWallet
  580. pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
  581. let wallets = self.inner.get_wallets().await;
  582. wallets
  583. .into_iter()
  584. .map(|w| Arc::new(crate::wallet::Wallet::from_inner(Arc::new(w))))
  585. .collect()
  586. }
  587. /// Get a specific wallet from MultiMintWallet by mint URL
  588. pub async fn get_wallet(&self, mint_url: MintUrl) -> Option<Arc<crate::wallet::Wallet>> {
  589. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().ok()?;
  590. let wallet = self.inner.get_wallet(&cdk_mint_url).await?;
  591. Some(Arc::new(crate::wallet::Wallet::from_inner(Arc::new(
  592. wallet,
  593. ))))
  594. }
  595. /// Verify token DLEQ proofs
  596. pub async fn verify_token_dleq(&self, token: Arc<Token>) -> Result<(), FfiError> {
  597. let cdk_token = token.inner.clone();
  598. self.inner.verify_token_dleq(&cdk_token).await?;
  599. Ok(())
  600. }
  601. /// Query mint for current mint information
  602. pub async fn fetch_mint_info(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
  603. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  604. let mint_info = self.inner.fetch_mint_info(&cdk_mint_url).await?;
  605. Ok(mint_info.map(Into::into))
  606. }
  607. /// Get mint info for all wallets
  608. ///
  609. /// This method loads the mint info for each wallet in the MultiMintWallet
  610. /// and returns a map of mint URLs to their corresponding mint info.
  611. ///
  612. /// Uses cached mint info when available, only fetching from the mint if the cache
  613. /// has expired.
  614. pub async fn get_all_mint_info(&self) -> Result<MintInfoMap, FfiError> {
  615. let mint_infos = self.inner.get_all_mint_info().await?;
  616. let mut result = HashMap::new();
  617. for (mint_url, mint_info) in mint_infos {
  618. result.insert(mint_url.to_string(), mint_info.into());
  619. }
  620. Ok(result)
  621. }
  622. }
  623. /// Payment request methods for MultiMintWallet
  624. #[uniffi::export(async_runtime = "tokio")]
  625. impl MultiMintWallet {
  626. /// Pay a NUT-18 PaymentRequest
  627. ///
  628. /// This method handles paying a payment request by selecting an appropriate mint:
  629. /// - If `mint_url` is provided, it verifies the payment request accepts that mint
  630. /// and uses it to pay.
  631. /// - If `mint_url` is None, it automatically selects the mint that:
  632. /// 1. Is accepted by the payment request (matches one of the request's mints, or request accepts any mint)
  633. /// 2. Has the highest balance among matching mints
  634. ///
  635. /// # Arguments
  636. ///
  637. /// * `payment_request` - The NUT-18 payment request to pay
  638. /// * `mint_url` - Optional specific mint to use. If None, automatically selects the best matching mint.
  639. /// * `custom_amount` - Custom amount to pay (required if payment request has no amount)
  640. ///
  641. /// # Errors
  642. ///
  643. /// Returns an error if:
  644. /// - The payment request has no amount and no custom amount is provided
  645. /// - The specified mint is not accepted by the payment request
  646. /// - No matching mint has sufficient balance
  647. /// - No transport is available in the payment request
  648. pub async fn pay_request(
  649. &self,
  650. payment_request: Arc<PaymentRequest>,
  651. mint_url: Option<MintUrl>,
  652. custom_amount: Option<Amount>,
  653. ) -> Result<(), FfiError> {
  654. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  655. let cdk_amount = custom_amount.map(Into::into);
  656. self.inner
  657. .pay_request(payment_request.inner().clone(), cdk_mint_url, cdk_amount)
  658. .await?;
  659. Ok(())
  660. }
  661. /// Create a NUT-18 payment request
  662. ///
  663. /// Creates a payment request that can be shared to receive Cashu tokens.
  664. /// The request can include optional amount, description, and spending conditions.
  665. ///
  666. /// # Arguments
  667. ///
  668. /// * `params` - Parameters for creating the payment request
  669. ///
  670. /// # Transport Options
  671. ///
  672. /// - `"nostr"` - Uses Nostr relays for privacy-preserving delivery (requires nostr_relays)
  673. /// - `"http"` - Uses HTTP POST for delivery (requires http_url)
  674. /// - `"none"` - No transport; token must be delivered out-of-band
  675. ///
  676. /// # Example
  677. ///
  678. /// ```ignore
  679. /// let params = CreateRequestParams {
  680. /// amount: Some(100),
  681. /// unit: "sat".to_string(),
  682. /// description: Some("Coffee payment".to_string()),
  683. /// transport: "http".to_string(),
  684. /// http_url: Some("https://example.com/callback".to_string()),
  685. /// ..Default::default()
  686. /// };
  687. /// let result = wallet.create_request(params).await?;
  688. /// println!("Share this request: {}", result.payment_request.to_string_encoded());
  689. ///
  690. /// // If using Nostr transport, wait for payment:
  691. /// if let Some(nostr_info) = result.nostr_wait_info {
  692. /// let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
  693. /// println!("Received {} sats", amount);
  694. /// }
  695. /// ```
  696. pub async fn create_request(
  697. &self,
  698. params: CreateRequestParams,
  699. ) -> Result<CreateRequestResult, FfiError> {
  700. let (payment_request, nostr_wait_info) = self.inner.create_request(params.into()).await?;
  701. Ok(CreateRequestResult {
  702. payment_request: Arc::new(PaymentRequest::from_inner(payment_request)),
  703. nostr_wait_info: nostr_wait_info.map(|info| Arc::new(NostrWaitInfo::from_inner(info))),
  704. })
  705. }
  706. /// Wait for a Nostr payment and receive it into the wallet
  707. ///
  708. /// This method connects to the Nostr relays specified in the `NostrWaitInfo`,
  709. /// subscribes for incoming payment events, and receives the first valid
  710. /// payment into the wallet.
  711. ///
  712. /// # Arguments
  713. ///
  714. /// * `info` - The Nostr wait info returned from `create_request` when using Nostr transport
  715. ///
  716. /// # Returns
  717. ///
  718. /// The amount received from the payment.
  719. ///
  720. /// # Example
  721. ///
  722. /// ```ignore
  723. /// let result = wallet.create_request(params).await?;
  724. /// if let Some(nostr_info) = result.nostr_wait_info {
  725. /// let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
  726. /// println!("Received {} sats", amount);
  727. /// }
  728. /// ```
  729. pub async fn wait_for_nostr_payment(
  730. &self,
  731. info: Arc<NostrWaitInfo>,
  732. ) -> Result<Amount, FfiError> {
  733. // We need to clone the inner NostrWaitInfo since we can't consume the Arc
  734. let info_inner = cdk::wallet::payment_request::NostrWaitInfo {
  735. keys: info.inner().keys.clone(),
  736. relays: info.inner().relays.clone(),
  737. pubkey: info.inner().pubkey,
  738. };
  739. let amount = self
  740. .inner
  741. .wait_for_nostr_payment(info_inner)
  742. .await
  743. .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
  744. Ok(amount.into())
  745. }
  746. }
  747. /// Auth methods for MultiMintWallet
  748. #[uniffi::export(async_runtime = "tokio")]
  749. impl MultiMintWallet {
  750. /// Set Clear Auth Token (CAT) for a specific mint
  751. pub async fn set_cat(&self, mint_url: MintUrl, cat: String) -> Result<(), FfiError> {
  752. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  753. self.inner.set_cat(&cdk_mint_url, cat).await?;
  754. Ok(())
  755. }
  756. /// Set refresh token for a specific mint
  757. pub async fn set_refresh_token(
  758. &self,
  759. mint_url: MintUrl,
  760. refresh_token: String,
  761. ) -> Result<(), FfiError> {
  762. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  763. self.inner
  764. .set_refresh_token(&cdk_mint_url, refresh_token)
  765. .await?;
  766. Ok(())
  767. }
  768. /// Refresh access token for a specific mint using the stored refresh token
  769. pub async fn refresh_access_token(&self, mint_url: MintUrl) -> Result<(), FfiError> {
  770. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  771. self.inner.refresh_access_token(&cdk_mint_url).await?;
  772. Ok(())
  773. }
  774. /// Mint blind auth tokens at a specific mint
  775. pub async fn mint_blind_auth(
  776. &self,
  777. mint_url: MintUrl,
  778. amount: Amount,
  779. ) -> Result<Proofs, FfiError> {
  780. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  781. let proofs = self
  782. .inner
  783. .mint_blind_auth(&cdk_mint_url, amount.into())
  784. .await?;
  785. Ok(proofs.into_iter().map(|p| p.into()).collect())
  786. }
  787. /// Get unspent auth proofs for a specific mint
  788. pub async fn get_unspent_auth_proofs(
  789. &self,
  790. mint_url: MintUrl,
  791. ) -> Result<Vec<AuthProof>, FfiError> {
  792. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  793. let auth_proofs = self.inner.get_unspent_auth_proofs(&cdk_mint_url).await?;
  794. Ok(auth_proofs.into_iter().map(Into::into).collect())
  795. }
  796. }
  797. /// Transfer mode for mint-to-mint transfers
  798. #[derive(Debug, Clone, uniffi::Enum)]
  799. pub enum TransferMode {
  800. /// Transfer exact amount to target (target receives specified amount)
  801. ExactReceive { amount: Amount },
  802. /// Transfer all available balance (source will be emptied)
  803. FullBalance,
  804. }
  805. impl From<TransferMode> for CdkTransferMode {
  806. fn from(mode: TransferMode) -> Self {
  807. match mode {
  808. TransferMode::ExactReceive { amount } => CdkTransferMode::ExactReceive(amount.into()),
  809. TransferMode::FullBalance => CdkTransferMode::FullBalance,
  810. }
  811. }
  812. }
  813. /// Result of a transfer operation with detailed breakdown
  814. #[derive(Debug, Clone, uniffi::Record)]
  815. pub struct TransferResult {
  816. /// Amount deducted from source mint
  817. pub amount_sent: Amount,
  818. /// Amount received at target mint
  819. pub amount_received: Amount,
  820. /// Total fees paid for the transfer
  821. pub fees_paid: Amount,
  822. /// Remaining balance in source mint after transfer
  823. pub source_balance_after: Amount,
  824. /// New balance in target mint after transfer
  825. pub target_balance_after: Amount,
  826. }
  827. impl From<CdkTransferResult> for TransferResult {
  828. fn from(result: CdkTransferResult) -> Self {
  829. Self {
  830. amount_sent: result.amount_sent.into(),
  831. amount_received: result.amount_received.into(),
  832. fees_paid: result.fees_paid.into(),
  833. source_balance_after: result.source_balance_after.into(),
  834. target_balance_after: result.target_balance_after.into(),
  835. }
  836. }
  837. }
  838. /// Data extracted from a token including mint URL, proofs, and memo
  839. #[derive(Debug, Clone, uniffi::Record)]
  840. pub struct TokenData {
  841. /// The mint URL from the token
  842. pub mint_url: MintUrl,
  843. /// The proofs contained in the token
  844. pub proofs: Proofs,
  845. /// The memo from the token, if present
  846. pub memo: Option<String>,
  847. /// Value of token
  848. pub value: Amount,
  849. /// Unit of token
  850. pub unit: CurrencyUnit,
  851. /// Fee to redeem
  852. ///
  853. /// If the token is for a proof that we do not know, we cannot get the fee.
  854. /// To avoid just erroring and still allow decoding, this is an option.
  855. /// None does not mean there is no fee, it means we do not know the fee.
  856. pub redeem_fee: Option<Amount>,
  857. }
  858. impl From<CdkTokenData> for TokenData {
  859. fn from(data: CdkTokenData) -> Self {
  860. Self {
  861. mint_url: data.mint_url.into(),
  862. proofs: data.proofs.into_iter().map(|p| p.into()).collect(),
  863. memo: data.memo,
  864. value: data.value.into(),
  865. unit: data.unit.into(),
  866. redeem_fee: data.redeem_fee.map(|a| a.into()),
  867. }
  868. }
  869. }
  870. /// Options for receiving tokens in multi-mint context
  871. #[derive(Debug, Clone, Default, uniffi::Record)]
  872. pub struct MultiMintReceiveOptions {
  873. /// Whether to allow receiving from untrusted (not yet added) mints
  874. pub allow_untrusted: bool,
  875. /// Mint URL to transfer tokens to from untrusted mints (None means keep in original mint)
  876. pub transfer_to_mint: Option<MintUrl>,
  877. /// Base receive options to apply to the wallet receive
  878. pub receive_options: ReceiveOptions,
  879. }
  880. impl From<MultiMintReceiveOptions> for CdkMultiMintReceiveOptions {
  881. fn from(options: MultiMintReceiveOptions) -> Self {
  882. let mut opts = CdkMultiMintReceiveOptions::new();
  883. opts.allow_untrusted = options.allow_untrusted;
  884. opts.transfer_to_mint = options.transfer_to_mint.and_then(|url| url.try_into().ok());
  885. opts.receive_options = options.receive_options.into();
  886. opts
  887. }
  888. }
  889. /// Options for sending tokens in multi-mint context
  890. #[derive(Debug, Clone, Default, uniffi::Record)]
  891. pub struct MultiMintSendOptions {
  892. /// Whether to allow transferring funds from other mints if needed
  893. pub allow_transfer: bool,
  894. /// Maximum amount to transfer from other mints (optional limit)
  895. pub max_transfer_amount: Option<Amount>,
  896. /// Specific mint URLs allowed for transfers (empty means all mints allowed)
  897. pub allowed_mints: Vec<MintUrl>,
  898. /// Specific mint URLs to exclude from transfers
  899. pub excluded_mints: Vec<MintUrl>,
  900. /// Base send options to apply to the wallet send
  901. pub send_options: SendOptions,
  902. }
  903. impl From<MultiMintSendOptions> for CdkMultiMintSendOptions {
  904. fn from(options: MultiMintSendOptions) -> Self {
  905. let mut opts = CdkMultiMintSendOptions::new();
  906. opts.allow_transfer = options.allow_transfer;
  907. opts.max_transfer_amount = options.max_transfer_amount.map(Into::into);
  908. opts.allowed_mints = options
  909. .allowed_mints
  910. .into_iter()
  911. .filter_map(|url| url.try_into().ok())
  912. .collect();
  913. opts.excluded_mints = options
  914. .excluded_mints
  915. .into_iter()
  916. .filter_map(|url| url.try_into().ok())
  917. .collect();
  918. opts.send_options = options.send_options.into();
  919. opts
  920. }
  921. }
  922. /// Type alias for balances by mint URL
  923. pub type BalanceMap = HashMap<String, Amount>;
  924. /// Type alias for proofs by mint URL
  925. pub type ProofsByMint = HashMap<String, Vec<Proof>>;
  926. /// Type alias for mint info by mint URL
  927. pub type MintInfoMap = HashMap<String, MintInfo>;