multi_mint_wallet.rs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  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::internal(format!("Invalid mnemonic: {}", e)))?;
  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::internal(format!("Failed to create runtime: {}", e)))?
  48. .block_on(async move {
  49. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  50. })
  51. }
  52. }?;
  53. Ok(Self {
  54. inner: Arc::new(wallet),
  55. })
  56. }
  57. /// Create a new MultiMintWallet with proxy configuration
  58. #[uniffi::constructor]
  59. pub fn new_with_proxy(
  60. unit: CurrencyUnit,
  61. mnemonic: String,
  62. db: Arc<dyn crate::database::WalletDatabase>,
  63. proxy_url: String,
  64. ) -> Result<Self, FfiError> {
  65. // Parse mnemonic and generate seed without passphrase
  66. let m = Mnemonic::parse(&mnemonic)
  67. .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
  68. let seed = m.to_seed_normalized("");
  69. // Convert the FFI database trait to a CDK database implementation
  70. let localstore = crate::database::create_cdk_database_from_ffi(db);
  71. // Parse proxy URL
  72. let proxy_url = url::Url::parse(&proxy_url)
  73. .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;
  74. let wallet = match tokio::runtime::Handle::try_current() {
  75. Ok(handle) => tokio::task::block_in_place(|| {
  76. handle.block_on(async move {
  77. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  78. .await
  79. })
  80. }),
  81. Err(_) => {
  82. // No current runtime, create a new one
  83. tokio::runtime::Runtime::new()
  84. .map_err(|e| FfiError::internal(format!("Failed to create runtime: {}", e)))?
  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::internal(format!(
  121. "Mint not found: {}",
  122. cdk_mint_url
  123. )))
  124. }
  125. }
  126. /// Set metadata cache TTL (time-to-live) in seconds for all mints
  127. ///
  128. /// Controls how long cached mint metadata is considered fresh for all mints
  129. /// in this MultiMintWallet.
  130. ///
  131. /// # Arguments
  132. ///
  133. /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
  134. pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
  135. let wallets = self.inner.get_wallets().await;
  136. let ttl = ttl_secs.map(std::time::Duration::from_secs);
  137. for wallet in wallets.iter() {
  138. wallet.set_metadata_cache_ttl(ttl);
  139. }
  140. }
  141. /// Add a mint to this MultiMintWallet
  142. pub async fn add_mint(
  143. &self,
  144. mint_url: MintUrl,
  145. target_proof_count: Option<u32>,
  146. ) -> Result<(), FfiError> {
  147. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  148. if let Some(count) = target_proof_count {
  149. let config = cdk::wallet::multi_mint_wallet::WalletConfig::new()
  150. .with_target_proof_count(count as usize);
  151. self.inner
  152. .add_mint_with_config(cdk_mint_url, config)
  153. .await?;
  154. } else {
  155. self.inner.add_mint(cdk_mint_url).await?;
  156. }
  157. Ok(())
  158. }
  159. /// Remove mint from MultiMintWallet
  160. ///
  161. /// # Panics
  162. ///
  163. /// Panics if the hardcoded fallback URL is invalid (should never happen).
  164. pub async fn remove_mint(&self, mint_url: MintUrl) {
  165. let url_str = mint_url.url.clone();
  166. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().unwrap_or_else(|_| {
  167. // If conversion fails, we can't remove the mint, but we shouldn't panic
  168. // This is a best-effort operation
  169. cdk::mint_url::MintUrl::from_str(&url_str).unwrap_or_else(|_| {
  170. // Last resort: create a dummy URL that won't match anything
  171. cdk::mint_url::MintUrl::from_str("https://invalid.mint")
  172. .expect("Valid hardcoded URL")
  173. })
  174. });
  175. self.inner.remove_mint(&cdk_mint_url).await;
  176. }
  177. /// Check if mint is in wallet
  178. pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
  179. if let Ok(cdk_mint_url) = mint_url.try_into() {
  180. self.inner.has_mint(&cdk_mint_url).await
  181. } else {
  182. false
  183. }
  184. }
  185. pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Vec<KeySetInfo>, FfiError> {
  186. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  187. let keysets = self.inner.get_mint_keysets(&cdk_mint_url).await?;
  188. let keysets = keysets.into_iter().map(|k| k.into()).collect();
  189. Ok(keysets)
  190. }
  191. /// Get token data (mint URL and proofs) from a token
  192. ///
  193. /// This method extracts the mint URL and proofs from a token. It will automatically
  194. /// fetch the keysets from the mint if needed to properly decode the proofs.
  195. ///
  196. /// The mint must already be added to the wallet. If the mint is not in the wallet,
  197. /// use `add_mint` first.
  198. pub async fn get_token_data(&self, token: Arc<Token>) -> Result<TokenData, FfiError> {
  199. let token_data = self.inner.get_token_data(&token.inner).await?;
  200. Ok(token_data.into())
  201. }
  202. /// Get wallet balances for all mints
  203. pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
  204. let balances = self.inner.get_balances().await?;
  205. let mut balance_map = HashMap::new();
  206. for (mint_url, amount) in balances {
  207. balance_map.insert(mint_url.to_string(), amount.into());
  208. }
  209. Ok(balance_map)
  210. }
  211. /// Get total balance across all mints
  212. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  213. let total = self.inner.total_balance().await?;
  214. Ok(total.into())
  215. }
  216. /// List proofs for all mints
  217. pub async fn list_proofs(&self) -> Result<ProofsByMint, FfiError> {
  218. let proofs = self.inner.list_proofs().await?;
  219. let mut proofs_by_mint = HashMap::new();
  220. for (mint_url, mint_proofs) in proofs {
  221. let ffi_proofs: Vec<Proof> = mint_proofs.into_iter().map(|p| p.into()).collect();
  222. proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
  223. }
  224. Ok(proofs_by_mint)
  225. }
  226. /// Check the state of proofs at a specific mint
  227. pub async fn check_proofs_state(
  228. &self,
  229. mint_url: MintUrl,
  230. proofs: Proofs,
  231. ) -> Result<Vec<ProofState>, FfiError> {
  232. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  233. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  234. proofs.into_iter().map(|p| p.try_into()).collect();
  235. let cdk_proofs = cdk_proofs?;
  236. let states = self
  237. .inner
  238. .check_proofs_state(&cdk_mint_url, cdk_proofs)
  239. .await?;
  240. Ok(states.into_iter().map(|s| s.into()).collect())
  241. }
  242. /// Receive token
  243. pub async fn receive(
  244. &self,
  245. token: Arc<Token>,
  246. options: MultiMintReceiveOptions,
  247. ) -> Result<Amount, FfiError> {
  248. let amount = self
  249. .inner
  250. .receive(&token.to_string(), options.into())
  251. .await?;
  252. Ok(amount.into())
  253. }
  254. /// Restore wallets for a specific mint
  255. pub async fn restore(&self, mint_url: MintUrl) -> Result<Restored, FfiError> {
  256. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  257. let restored = self.inner.restore(&cdk_mint_url).await?;
  258. Ok(restored.into())
  259. }
  260. /// Get all pending send operations across all mints
  261. pub async fn get_pending_sends(&self) -> Result<Vec<PendingSend>, FfiError> {
  262. let sends = self.inner.get_pending_sends().await?;
  263. Ok(sends
  264. .into_iter()
  265. .map(|(mint_url, id)| PendingSend {
  266. mint_url: mint_url.into(),
  267. operation_id: id.to_string(),
  268. })
  269. .collect())
  270. }
  271. /// Revoke a pending send operation for a specific mint
  272. pub async fn revoke_send(
  273. &self,
  274. mint_url: MintUrl,
  275. operation_id: String,
  276. ) -> Result<Amount, FfiError> {
  277. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  278. let uuid = uuid::Uuid::parse_str(&operation_id)
  279. .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
  280. let amount = self.inner.revoke_send(cdk_mint_url, uuid).await?;
  281. Ok(amount.into())
  282. }
  283. /// Check status of a pending send operation for a specific mint
  284. pub async fn check_send_status(
  285. &self,
  286. mint_url: MintUrl,
  287. operation_id: String,
  288. ) -> Result<bool, FfiError> {
  289. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  290. let uuid = uuid::Uuid::parse_str(&operation_id)
  291. .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
  292. let claimed = self.inner.check_send_status(cdk_mint_url, uuid).await?;
  293. Ok(claimed)
  294. }
  295. /// Send tokens from a specific mint
  296. ///
  297. /// This method prepares and confirms the send in one step.
  298. /// For more control over the send process, use the single-mint Wallet.
  299. pub async fn send(
  300. &self,
  301. mint_url: MintUrl,
  302. amount: Amount,
  303. options: MultiMintSendOptions,
  304. ) -> Result<Token, FfiError> {
  305. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  306. let token = self
  307. .inner
  308. .send(cdk_mint_url, amount.into(), options.into())
  309. .await?;
  310. Ok(token.into())
  311. }
  312. /// Get a mint quote from a specific mint
  313. pub async fn mint_quote(
  314. &self,
  315. mint_url: MintUrl,
  316. payment_method: PaymentMethod,
  317. amount: Option<Amount>,
  318. description: Option<String>,
  319. extra: Option<String>,
  320. ) -> Result<MintQuote, FfiError> {
  321. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  322. let quote = self
  323. .inner
  324. .mint_quote(
  325. &cdk_mint_url,
  326. payment_method,
  327. amount.map(Into::into),
  328. description,
  329. extra,
  330. )
  331. .await?;
  332. Ok(quote.into())
  333. }
  334. /// Refresh a specific mint quote status from the mint.
  335. /// Updates local store with current state from mint.
  336. /// Does NOT mint tokens - use mint() to mint a specific quote.
  337. pub async fn refresh_mint_quote(
  338. &self,
  339. mint_url: MintUrl,
  340. quote_id: String,
  341. ) -> Result<MintQuote, FfiError> {
  342. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  343. let quote = self
  344. .inner
  345. .refresh_mint_quote(&cdk_mint_url, &quote_id)
  346. .await?;
  347. Ok(quote.into())
  348. }
  349. /// Fetch a mint quote from the mint and store it locally
  350. ///
  351. /// This method contacts the mint to get the current state of a quote,
  352. /// creates or updates the quote in local storage, and returns the stored quote.
  353. ///
  354. /// Works with all payment methods (Bolt11, Bolt12, and custom payment methods).
  355. ///
  356. /// # Arguments
  357. /// * `mint_url` - The URL of the mint
  358. /// * `quote_id` - The ID of the quote to fetch
  359. /// * `payment_method` - The payment method for the quote. Required if the quote
  360. /// is not already stored locally. If the quote exists locally, the stored
  361. /// payment method will be used and this parameter is ignored.
  362. pub async fn fetch_mint_quote(
  363. &self,
  364. mint_url: MintUrl,
  365. quote_id: String,
  366. payment_method: Option<PaymentMethod>,
  367. ) -> Result<MintQuote, FfiError> {
  368. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  369. let method = payment_method.map(Into::into);
  370. let quote = self
  371. .inner
  372. .fetch_mint_quote(&cdk_mint_url, &quote_id, method)
  373. .await?;
  374. Ok(quote.into())
  375. }
  376. /// Mint tokens at a specific mint
  377. pub async fn mint(
  378. &self,
  379. mint_url: MintUrl,
  380. quote_id: String,
  381. spending_conditions: Option<SpendingConditions>,
  382. ) -> Result<Proofs, FfiError> {
  383. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  384. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  385. let proofs = self
  386. .inner
  387. .mint(
  388. &cdk_mint_url,
  389. &quote_id,
  390. cdk::amount::SplitTarget::default(),
  391. conditions,
  392. )
  393. .await?;
  394. Ok(proofs.into_iter().map(|p| p.into()).collect())
  395. }
  396. /// Wait for a mint quote to be paid and automatically mint the proofs
  397. #[cfg(not(target_arch = "wasm32"))]
  398. pub async fn wait_for_mint_quote(
  399. &self,
  400. mint_url: MintUrl,
  401. quote_id: String,
  402. split_target: SplitTarget,
  403. spending_conditions: Option<SpendingConditions>,
  404. timeout_secs: u64,
  405. ) -> Result<Proofs, FfiError> {
  406. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  407. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  408. let timeout = std::time::Duration::from_secs(timeout_secs);
  409. let proofs = self
  410. .inner
  411. .wait_for_mint_quote(
  412. &cdk_mint_url,
  413. &quote_id,
  414. split_target.into(),
  415. conditions,
  416. timeout,
  417. )
  418. .await?;
  419. Ok(proofs.into_iter().map(|p| p.into()).collect())
  420. }
  421. /// Get a melt quote from a specific mint
  422. pub async fn melt_quote(
  423. &self,
  424. mint_url: MintUrl,
  425. payment_method: PaymentMethod,
  426. request: String,
  427. options: Option<MeltOptions>,
  428. extra: Option<String>,
  429. ) -> Result<MeltQuote, FfiError> {
  430. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  431. let cdk_options = options.map(Into::into);
  432. let quote = self
  433. .inner
  434. .melt_quote(&cdk_mint_url, payment_method, request, cdk_options, extra)
  435. .await?;
  436. Ok(quote.into())
  437. }
  438. /// Get a melt quote for a BIP353 human-readable address
  439. ///
  440. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  441. /// and then creates a melt quote for that offer at the specified mint.
  442. ///
  443. /// # Arguments
  444. ///
  445. /// * `mint_url` - The mint to use for creating the melt quote
  446. /// * `bip353_address` - Human-readable address in the format "user@domain.com"
  447. /// * `amount_msat` - Amount to pay in millisatoshis
  448. #[cfg(not(target_arch = "wasm32"))]
  449. pub async fn melt_bip353_quote(
  450. &self,
  451. mint_url: MintUrl,
  452. bip353_address: String,
  453. amount_msat: u64,
  454. ) -> Result<MeltQuote, FfiError> {
  455. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  456. let cdk_amount = cdk::Amount::from(amount_msat);
  457. let quote = self
  458. .inner
  459. .melt_bip353_quote(&cdk_mint_url, &bip353_address, cdk_amount)
  460. .await?;
  461. Ok(quote.into())
  462. }
  463. /// Get a melt quote for a Lightning address
  464. ///
  465. /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
  466. /// and then creates a melt quote for that invoice at the specified mint.
  467. ///
  468. /// # Arguments
  469. ///
  470. /// * `mint_url` - The mint to use for creating the melt quote
  471. /// * `lightning_address` - Lightning address in the format "user@domain.com"
  472. /// * `amount_msat` - Amount to pay in millisatoshis
  473. pub async fn melt_lightning_address_quote(
  474. &self,
  475. mint_url: MintUrl,
  476. lightning_address: String,
  477. amount_msat: u64,
  478. ) -> Result<MeltQuote, FfiError> {
  479. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  480. let cdk_amount = cdk::Amount::from(amount_msat);
  481. let quote = self
  482. .inner
  483. .melt_lightning_address_quote(&cdk_mint_url, &lightning_address, cdk_amount)
  484. .await?;
  485. Ok(quote.into())
  486. }
  487. /// Get a melt quote for a human-readable address
  488. ///
  489. /// This method accepts a human-readable address that could be either a BIP353 address
  490. /// or a Lightning address. It intelligently determines which to try based on mint support:
  491. ///
  492. /// 1. If the mint supports Bolt12, it tries BIP353 first
  493. /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
  494. /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
  495. /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
  496. ///
  497. /// # Arguments
  498. ///
  499. /// * `mint_url` - The mint to use for creating the melt quote
  500. /// * `address` - Human-readable address (BIP353 or Lightning address)
  501. /// * `amount_msat` - Amount to pay in millisatoshis
  502. #[cfg(not(target_arch = "wasm32"))]
  503. pub async fn melt_human_readable_quote(
  504. &self,
  505. mint_url: MintUrl,
  506. address: String,
  507. amount_msat: u64,
  508. ) -> Result<MeltQuote, FfiError> {
  509. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  510. let cdk_amount = cdk::Amount::from(amount_msat);
  511. let quote = self
  512. .inner
  513. .melt_human_readable_quote(&cdk_mint_url, &address, cdk_amount)
  514. .await?;
  515. Ok(quote.into())
  516. }
  517. /// Melt tokens
  518. pub async fn melt_with_mint(
  519. &self,
  520. mint_url: MintUrl,
  521. quote_id: String,
  522. ) -> Result<FinalizedMelt, FfiError> {
  523. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  524. let finalized = self.inner.melt_with_mint(&cdk_mint_url, &quote_id).await?;
  525. Ok(finalized.into())
  526. }
  527. /// Melt specific proofs from a specific mint
  528. ///
  529. /// This method allows melting proofs that may not be in the wallet's database,
  530. /// similar to how `receive_proofs` handles external proofs. The proofs will be
  531. /// added to the database and used for the melt operation.
  532. ///
  533. /// # Arguments
  534. ///
  535. /// * `mint_url` - The mint to use for the melt operation
  536. /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
  537. /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
  538. ///
  539. /// # Returns
  540. ///
  541. /// A `FinalizedMelt` result containing the payment details and any change proofs
  542. pub async fn melt_proofs(
  543. &self,
  544. mint_url: MintUrl,
  545. quote_id: String,
  546. proofs: Proofs,
  547. ) -> Result<FinalizedMelt, FfiError> {
  548. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  549. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  550. proofs.into_iter().map(|p| p.try_into()).collect();
  551. let cdk_proofs = cdk_proofs?;
  552. let finalized = self
  553. .inner
  554. .melt_proofs(&cdk_mint_url, &quote_id, cdk_proofs)
  555. .await?;
  556. Ok(finalized.into())
  557. }
  558. /// Check melt quote status
  559. pub async fn check_melt_quote(
  560. &self,
  561. mint_url: MintUrl,
  562. quote_id: String,
  563. ) -> Result<MeltQuote, FfiError> {
  564. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  565. let quote = self
  566. .inner
  567. .check_melt_quote(&cdk_mint_url, &quote_id)
  568. .await?;
  569. Ok(quote.into())
  570. }
  571. /// Melt tokens (pay a bolt11 invoice)
  572. pub async fn melt(
  573. &self,
  574. bolt11: String,
  575. options: Option<MeltOptions>,
  576. max_fee: Option<Amount>,
  577. ) -> Result<FinalizedMelt, FfiError> {
  578. let cdk_options = options.map(Into::into);
  579. let cdk_max_fee = max_fee.map(Into::into);
  580. let finalized = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
  581. Ok(finalized.into())
  582. }
  583. /// Transfer funds between mints
  584. pub async fn transfer(
  585. &self,
  586. source_mint: MintUrl,
  587. target_mint: MintUrl,
  588. transfer_mode: TransferMode,
  589. ) -> Result<TransferResult, FfiError> {
  590. let source_cdk: cdk::mint_url::MintUrl = source_mint.try_into()?;
  591. let target_cdk: cdk::mint_url::MintUrl = target_mint.try_into()?;
  592. let result = self
  593. .inner
  594. .transfer(&source_cdk, &target_cdk, transfer_mode.into())
  595. .await?;
  596. Ok(result.into())
  597. }
  598. /// Swap proofs with automatic wallet selection
  599. pub async fn swap(
  600. &self,
  601. amount: Option<Amount>,
  602. spending_conditions: Option<SpendingConditions>,
  603. ) -> Result<Option<Proofs>, FfiError> {
  604. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  605. let result = self.inner.swap(amount.map(Into::into), conditions).await?;
  606. Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
  607. }
  608. /// List transactions from all mints
  609. pub async fn list_transactions(
  610. &self,
  611. direction: Option<TransactionDirection>,
  612. ) -> Result<Vec<Transaction>, FfiError> {
  613. let cdk_direction = direction.map(Into::into);
  614. let transactions = self.inner.list_transactions(cdk_direction).await?;
  615. Ok(transactions.into_iter().map(Into::into).collect())
  616. }
  617. /// Get proofs for a transaction by transaction ID
  618. ///
  619. /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
  620. /// it will only check that specific mint's wallet. Otherwise, it searches across all
  621. /// wallets to find which mint the transaction belongs to.
  622. ///
  623. /// # Arguments
  624. ///
  625. /// * `id` - The transaction ID
  626. /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
  627. pub async fn get_proofs_for_transaction(
  628. &self,
  629. id: TransactionId,
  630. mint_url: Option<MintUrl>,
  631. ) -> Result<Vec<Proof>, FfiError> {
  632. let cdk_id = id.try_into()?;
  633. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  634. let proofs = self
  635. .inner
  636. .get_proofs_for_transaction(cdk_id, cdk_mint_url)
  637. .await?;
  638. Ok(proofs.into_iter().map(Into::into).collect())
  639. }
  640. /// Refresh all unissued mint quote states
  641. /// Does NOT mint - use mint_unissued_quotes() for that
  642. pub async fn refresh_all_mint_quotes(
  643. &self,
  644. mint_url: Option<MintUrl>,
  645. ) -> Result<Vec<MintQuote>, FfiError> {
  646. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  647. let quotes = self.inner.refresh_all_mint_quotes(cdk_mint_url).await?;
  648. Ok(quotes.into_iter().map(Into::into).collect())
  649. }
  650. /// Refresh states and mint all unissued quotes
  651. /// Returns total amount minted across all wallets
  652. pub async fn mint_unissued_quotes(
  653. &self,
  654. mint_url: Option<MintUrl>,
  655. ) -> Result<Amount, FfiError> {
  656. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  657. let amount = self.inner.mint_unissued_quotes(cdk_mint_url).await?;
  658. Ok(amount.into())
  659. }
  660. /// Consolidate proofs across all mints
  661. pub async fn consolidate(&self) -> Result<Amount, FfiError> {
  662. let amount = self.inner.consolidate().await?;
  663. Ok(amount.into())
  664. }
  665. /// Get list of mint URLs
  666. pub async fn get_mint_urls(&self) -> Vec<String> {
  667. let wallets = self.inner.get_wallets().await;
  668. wallets.iter().map(|w| w.mint_url.to_string()).collect()
  669. }
  670. /// Get all wallets from MultiMintWallet
  671. pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
  672. let wallets = self.inner.get_wallets().await;
  673. wallets
  674. .into_iter()
  675. .map(|w| Arc::new(crate::wallet::Wallet::from_inner(w)))
  676. .collect()
  677. }
  678. /// Get a specific wallet from MultiMintWallet by mint URL
  679. pub async fn get_wallet(&self, mint_url: MintUrl) -> Option<Arc<crate::wallet::Wallet>> {
  680. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().ok()?;
  681. let wallet = self.inner.get_wallet(&cdk_mint_url).await?;
  682. Some(Arc::new(crate::wallet::Wallet::from_inner(wallet)))
  683. }
  684. /// Verify token DLEQ proofs
  685. pub async fn verify_token_dleq(&self, token: Arc<Token>) -> Result<(), FfiError> {
  686. let cdk_token = token.inner.clone();
  687. self.inner.verify_token_dleq(&cdk_token).await?;
  688. Ok(())
  689. }
  690. /// Query mint for current mint information
  691. pub async fn fetch_mint_info(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
  692. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  693. let mint_info = self.inner.fetch_mint_info(&cdk_mint_url).await?;
  694. Ok(mint_info.map(Into::into))
  695. }
  696. /// Get mint info for all wallets
  697. ///
  698. /// This method loads the mint info for each wallet in the MultiMintWallet
  699. /// and returns a map of mint URLs to their corresponding mint info.
  700. ///
  701. /// Uses cached mint info when available, only fetching from the mint if the cache
  702. /// has expired.
  703. pub async fn get_all_mint_info(&self) -> Result<MintInfoMap, FfiError> {
  704. let mint_infos = self.inner.get_all_mint_info().await?;
  705. let mut result = HashMap::new();
  706. for (mint_url, mint_info) in mint_infos {
  707. result.insert(mint_url.to_string(), mint_info.into());
  708. }
  709. Ok(result)
  710. }
  711. }
  712. /// Payment request methods for MultiMintWallet
  713. #[uniffi::export(async_runtime = "tokio")]
  714. impl MultiMintWallet {
  715. /// Pay a NUT-18 PaymentRequest
  716. ///
  717. /// This method handles paying a payment request by selecting an appropriate mint:
  718. /// - If `mint_url` is provided, it verifies the payment request accepts that mint
  719. /// and uses it to pay.
  720. /// - If `mint_url` is None, it automatically selects the mint that:
  721. /// 1. Is accepted by the payment request (matches one of the request's mints, or request accepts any mint)
  722. /// 2. Has the highest balance among matching mints
  723. ///
  724. /// # Arguments
  725. ///
  726. /// * `payment_request` - The NUT-18 payment request to pay
  727. /// * `mint_url` - Optional specific mint to use. If None, automatically selects the best matching mint.
  728. /// * `custom_amount` - Custom amount to pay (required if payment request has no amount)
  729. ///
  730. /// # Errors
  731. ///
  732. /// Returns an error if:
  733. /// - The payment request has no amount and no custom amount is provided
  734. /// - The specified mint is not accepted by the payment request
  735. /// - No matching mint has sufficient balance
  736. /// - No transport is available in the payment request
  737. pub async fn pay_request(
  738. &self,
  739. payment_request: Arc<PaymentRequest>,
  740. mint_url: Option<MintUrl>,
  741. custom_amount: Option<Amount>,
  742. ) -> Result<(), FfiError> {
  743. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  744. let cdk_amount = custom_amount.map(Into::into);
  745. self.inner
  746. .pay_request(payment_request.inner().clone(), cdk_mint_url, cdk_amount)
  747. .await?;
  748. Ok(())
  749. }
  750. /// Create a NUT-18 payment request
  751. ///
  752. /// Creates a payment request that can be shared to receive Cashu tokens.
  753. /// The request can include optional amount, description, and spending conditions.
  754. ///
  755. /// # Arguments
  756. ///
  757. /// * `params` - Parameters for creating the payment request
  758. ///
  759. /// # Transport Options
  760. ///
  761. /// - `"nostr"` - Uses Nostr relays for privacy-preserving delivery (requires nostr_relays)
  762. /// - `"http"` - Uses HTTP POST for delivery (requires http_url)
  763. /// - `"none"` - No transport; token must be delivered out-of-band
  764. ///
  765. /// # Example
  766. ///
  767. /// ```ignore
  768. /// let params = CreateRequestParams {
  769. /// amount: Some(100),
  770. /// unit: "sat".to_string(),
  771. /// description: Some("Coffee payment".to_string()),
  772. /// transport: "http".to_string(),
  773. /// http_url: Some("https://example.com/callback".to_string()),
  774. /// ..Default::default()
  775. /// };
  776. /// let result = wallet.create_request(params).await?;
  777. /// println!("Share this request: {}", result.payment_request.to_string_encoded());
  778. ///
  779. /// // If using Nostr transport, wait for payment:
  780. /// if let Some(nostr_info) = result.nostr_wait_info {
  781. /// let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
  782. /// println!("Received {} sats", amount);
  783. /// }
  784. /// ```
  785. pub async fn create_request(
  786. &self,
  787. params: CreateRequestParams,
  788. ) -> Result<CreateRequestResult, FfiError> {
  789. let (payment_request, nostr_wait_info) = self.inner.create_request(params.into()).await?;
  790. Ok(CreateRequestResult {
  791. payment_request: Arc::new(PaymentRequest::from_inner(payment_request)),
  792. nostr_wait_info: nostr_wait_info.map(|info| Arc::new(NostrWaitInfo::from_inner(info))),
  793. })
  794. }
  795. /// Wait for a Nostr payment and receive it into the wallet
  796. ///
  797. /// This method connects to the Nostr relays specified in the `NostrWaitInfo`,
  798. /// subscribes for incoming payment events, and receives the first valid
  799. /// payment into the wallet.
  800. ///
  801. /// # Arguments
  802. ///
  803. /// * `info` - The Nostr wait info returned from `create_request` when using Nostr transport
  804. ///
  805. /// # Returns
  806. ///
  807. /// The amount received from the payment.
  808. ///
  809. /// # Example
  810. ///
  811. /// ```ignore
  812. /// let result = wallet.create_request(params).await?;
  813. /// if let Some(nostr_info) = result.nostr_wait_info {
  814. /// let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
  815. /// println!("Received {} sats", amount);
  816. /// }
  817. /// ```
  818. pub async fn wait_for_nostr_payment(
  819. &self,
  820. info: Arc<NostrWaitInfo>,
  821. ) -> Result<Amount, FfiError> {
  822. // We need to clone the inner NostrWaitInfo since we can't consume the Arc
  823. let info_inner = cdk::wallet::payment_request::NostrWaitInfo {
  824. keys: info.inner().keys.clone(),
  825. relays: info.inner().relays.clone(),
  826. pubkey: info.inner().pubkey,
  827. };
  828. let amount = self
  829. .inner
  830. .wait_for_nostr_payment(info_inner)
  831. .await
  832. .map_err(FfiError::internal)?;
  833. Ok(amount.into())
  834. }
  835. }
  836. /// Auth methods for MultiMintWallet
  837. #[uniffi::export(async_runtime = "tokio")]
  838. impl MultiMintWallet {
  839. /// Set Clear Auth Token (CAT) for a specific mint
  840. pub async fn set_cat(&self, mint_url: MintUrl, cat: String) -> Result<(), FfiError> {
  841. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  842. self.inner.set_cat(&cdk_mint_url, cat).await?;
  843. Ok(())
  844. }
  845. /// Set refresh token for a specific mint
  846. pub async fn set_refresh_token(
  847. &self,
  848. mint_url: MintUrl,
  849. refresh_token: String,
  850. ) -> Result<(), FfiError> {
  851. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  852. self.inner
  853. .set_refresh_token(&cdk_mint_url, refresh_token)
  854. .await?;
  855. Ok(())
  856. }
  857. /// Refresh access token for a specific mint using the stored refresh token
  858. pub async fn refresh_access_token(&self, mint_url: MintUrl) -> Result<(), FfiError> {
  859. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  860. self.inner.refresh_access_token(&cdk_mint_url).await?;
  861. Ok(())
  862. }
  863. /// Mint blind auth tokens at a specific mint
  864. pub async fn mint_blind_auth(
  865. &self,
  866. mint_url: MintUrl,
  867. amount: Amount,
  868. ) -> Result<Proofs, FfiError> {
  869. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  870. let proofs = self
  871. .inner
  872. .mint_blind_auth(&cdk_mint_url, amount.into())
  873. .await?;
  874. Ok(proofs.into_iter().map(|p| p.into()).collect())
  875. }
  876. /// Get unspent auth proofs for a specific mint
  877. pub async fn get_unspent_auth_proofs(
  878. &self,
  879. mint_url: MintUrl,
  880. ) -> Result<Vec<AuthProof>, FfiError> {
  881. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  882. let auth_proofs = self.inner.get_unspent_auth_proofs(&cdk_mint_url).await?;
  883. Ok(auth_proofs.into_iter().map(Into::into).collect())
  884. }
  885. }
  886. /// Transfer mode for mint-to-mint transfers
  887. #[derive(Debug, Clone, uniffi::Enum)]
  888. pub enum TransferMode {
  889. /// Transfer exact amount to target (target receives specified amount)
  890. ExactReceive { amount: Amount },
  891. /// Transfer all available balance (source will be emptied)
  892. FullBalance,
  893. }
  894. impl From<TransferMode> for CdkTransferMode {
  895. fn from(mode: TransferMode) -> Self {
  896. match mode {
  897. TransferMode::ExactReceive { amount } => CdkTransferMode::ExactReceive(amount.into()),
  898. TransferMode::FullBalance => CdkTransferMode::FullBalance,
  899. }
  900. }
  901. }
  902. /// Result of a transfer operation with detailed breakdown
  903. #[derive(Debug, Clone, uniffi::Record)]
  904. pub struct TransferResult {
  905. /// Amount deducted from source mint
  906. pub amount_sent: Amount,
  907. /// Amount received at target mint
  908. pub amount_received: Amount,
  909. /// Total fees paid for the transfer
  910. pub fees_paid: Amount,
  911. /// Remaining balance in source mint after transfer
  912. pub source_balance_after: Amount,
  913. /// New balance in target mint after transfer
  914. pub target_balance_after: Amount,
  915. }
  916. impl From<CdkTransferResult> for TransferResult {
  917. fn from(result: CdkTransferResult) -> Self {
  918. Self {
  919. amount_sent: result.amount_sent.into(),
  920. amount_received: result.amount_received.into(),
  921. fees_paid: result.fees_paid.into(),
  922. source_balance_after: result.source_balance_after.into(),
  923. target_balance_after: result.target_balance_after.into(),
  924. }
  925. }
  926. }
  927. /// Represents a pending send operation
  928. #[derive(Debug, Clone, uniffi::Record)]
  929. pub struct PendingSend {
  930. /// The mint URL where the send is pending
  931. pub mint_url: MintUrl,
  932. /// The operation ID of the pending send
  933. pub operation_id: String,
  934. }
  935. /// Data extracted from a token including mint URL, proofs, and memo
  936. #[derive(Debug, Clone, uniffi::Record)]
  937. pub struct TokenData {
  938. /// The mint URL from the token
  939. pub mint_url: MintUrl,
  940. /// The proofs contained in the token
  941. pub proofs: Proofs,
  942. /// The memo from the token, if present
  943. pub memo: Option<String>,
  944. /// Value of token
  945. pub value: Amount,
  946. /// Unit of token
  947. pub unit: CurrencyUnit,
  948. /// Fee to redeem
  949. ///
  950. /// If the token is for a proof that we do not know, we cannot get the fee.
  951. /// To avoid just erroring and still allow decoding, this is an option.
  952. /// None does not mean there is no fee, it means we do not know the fee.
  953. pub redeem_fee: Option<Amount>,
  954. }
  955. impl From<CdkTokenData> for TokenData {
  956. fn from(data: CdkTokenData) -> Self {
  957. Self {
  958. mint_url: data.mint_url.into(),
  959. proofs: data.proofs.into_iter().map(|p| p.into()).collect(),
  960. memo: data.memo,
  961. value: data.value.into(),
  962. unit: data.unit.into(),
  963. redeem_fee: data.redeem_fee.map(|a| a.into()),
  964. }
  965. }
  966. }
  967. /// Options for receiving tokens in multi-mint context
  968. #[derive(Debug, Clone, Default, uniffi::Record)]
  969. pub struct MultiMintReceiveOptions {
  970. /// Whether to allow receiving from untrusted (not yet added) mints
  971. pub allow_untrusted: bool,
  972. /// Mint URL to transfer tokens to from untrusted mints (None means keep in original mint)
  973. pub transfer_to_mint: Option<MintUrl>,
  974. /// Base receive options to apply to the wallet receive
  975. pub receive_options: ReceiveOptions,
  976. }
  977. impl From<MultiMintReceiveOptions> for CdkMultiMintReceiveOptions {
  978. fn from(options: MultiMintReceiveOptions) -> Self {
  979. let mut opts = CdkMultiMintReceiveOptions::new();
  980. opts.allow_untrusted = options.allow_untrusted;
  981. opts.transfer_to_mint = options.transfer_to_mint.and_then(|url| url.try_into().ok());
  982. opts.receive_options = options.receive_options.into();
  983. opts
  984. }
  985. }
  986. /// Options for sending tokens in multi-mint context
  987. #[derive(Debug, Clone, Default, uniffi::Record)]
  988. pub struct MultiMintSendOptions {
  989. /// Whether to allow transferring funds from other mints if needed
  990. pub allow_transfer: bool,
  991. /// Maximum amount to transfer from other mints (optional limit)
  992. pub max_transfer_amount: Option<Amount>,
  993. /// Specific mint URLs allowed for transfers (empty means all mints allowed)
  994. pub allowed_mints: Vec<MintUrl>,
  995. /// Specific mint URLs to exclude from transfers
  996. pub excluded_mints: Vec<MintUrl>,
  997. /// Base send options to apply to the wallet send
  998. pub send_options: SendOptions,
  999. }
  1000. impl From<MultiMintSendOptions> for CdkMultiMintSendOptions {
  1001. fn from(options: MultiMintSendOptions) -> Self {
  1002. let mut opts = CdkMultiMintSendOptions::new();
  1003. opts.allow_transfer = options.allow_transfer;
  1004. opts.max_transfer_amount = options.max_transfer_amount.map(Into::into);
  1005. opts.allowed_mints = options
  1006. .allowed_mints
  1007. .into_iter()
  1008. .filter_map(|url| url.try_into().ok())
  1009. .collect();
  1010. opts.excluded_mints = options
  1011. .excluded_mints
  1012. .into_iter()
  1013. .filter_map(|url| url.try_into().ok())
  1014. .collect();
  1015. opts.send_options = options.send_options.into();
  1016. opts
  1017. }
  1018. }
  1019. /// Nostr backup methods for MultiMintWallet (NUT-XX)
  1020. #[uniffi::export(async_runtime = "tokio")]
  1021. impl MultiMintWallet {
  1022. /// Get the hex-encoded public key used for Nostr mint backup
  1023. ///
  1024. /// This key is deterministically derived from the wallet seed and can be used
  1025. /// to identify and decrypt backup events on Nostr relays.
  1026. pub fn backup_public_key(&self) -> Result<String, FfiError> {
  1027. let keys = self.inner.backup_keys()?;
  1028. Ok(keys.public_key().to_hex())
  1029. }
  1030. /// Backup the current mint list to Nostr relays
  1031. ///
  1032. /// Creates an encrypted NIP-78 addressable event containing all mint URLs
  1033. /// and publishes it to the specified relays.
  1034. ///
  1035. /// # Arguments
  1036. ///
  1037. /// * `relays` - List of Nostr relay URLs (e.g., "wss://relay.damus.io")
  1038. /// * `options` - Backup options including optional client name
  1039. ///
  1040. /// # Example
  1041. ///
  1042. /// ```ignore
  1043. /// let relays = vec!["wss://relay.damus.io".to_string(), "wss://nos.lol".to_string()];
  1044. /// let options = BackupOptions { client: Some("my-wallet".to_string()) };
  1045. /// let result = wallet.backup_mints(relays, options).await?;
  1046. /// println!("Backup published with event ID: {}", result.event_id);
  1047. /// ```
  1048. pub async fn backup_mints(
  1049. &self,
  1050. relays: Vec<String>,
  1051. options: BackupOptions,
  1052. ) -> Result<BackupResult, FfiError> {
  1053. let result = self.inner.backup_mints(relays, options.into()).await?;
  1054. Ok(result.into())
  1055. }
  1056. /// Restore mint list from Nostr relays
  1057. ///
  1058. /// Fetches the most recent backup event from the specified relays,
  1059. /// decrypts it, and optionally adds the discovered mints to the wallet.
  1060. ///
  1061. /// # Arguments
  1062. ///
  1063. /// * `relays` - List of Nostr relay URLs to fetch from
  1064. /// * `add_mints` - If true, automatically add discovered mints to the wallet
  1065. /// * `options` - Restore options including timeout
  1066. ///
  1067. /// # Example
  1068. ///
  1069. /// ```ignore
  1070. /// let relays = vec!["wss://relay.damus.io".to_string()];
  1071. /// let result = wallet.restore_mints(relays, true, RestoreOptions::default()).await?;
  1072. /// println!("Restored {} mints, {} newly added", result.mint_count, result.mints_added);
  1073. /// ```
  1074. pub async fn restore_mints(
  1075. &self,
  1076. relays: Vec<String>,
  1077. add_mints: bool,
  1078. options: RestoreOptions,
  1079. ) -> Result<RestoreResult, FfiError> {
  1080. let result = self
  1081. .inner
  1082. .restore_mints(relays, add_mints, options.into())
  1083. .await?;
  1084. Ok(result.into())
  1085. }
  1086. /// Fetch the backup without adding mints to the wallet
  1087. ///
  1088. /// This is useful for previewing what mints are in the backup before
  1089. /// deciding to add them.
  1090. ///
  1091. /// # Arguments
  1092. ///
  1093. /// * `relays` - List of Nostr relay URLs to fetch from
  1094. /// * `options` - Restore options including timeout
  1095. pub async fn fetch_backup(
  1096. &self,
  1097. relays: Vec<String>,
  1098. options: RestoreOptions,
  1099. ) -> Result<MintBackup, FfiError> {
  1100. let backup = self.inner.fetch_backup(relays, options.into()).await?;
  1101. Ok(backup.into())
  1102. }
  1103. }
  1104. /// Type alias for balances by mint URL
  1105. pub type BalanceMap = HashMap<String, Amount>;
  1106. /// Type alias for proofs by mint URL
  1107. pub type ProofsByMint = HashMap<String, Vec<Proof>>;
  1108. /// Type alias for mint info by mint URL
  1109. pub type MintInfoMap = HashMap<String, MintInfo>;