multi_mint_wallet.rs 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271
  1. //! MultiMint Wallet
  2. //!
  3. //! Wrapper around core [`Wallet`] that enables the use of multiple mint unit
  4. //! pairs
  5. use std::collections::BTreeMap;
  6. use std::ops::Deref;
  7. use std::str::FromStr;
  8. use std::sync::Arc;
  9. use anyhow::Result;
  10. use cdk_common::database::WalletDatabase;
  11. use cdk_common::task::spawn;
  12. use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection, TransactionId};
  13. use cdk_common::{database, KeySetInfo};
  14. use tokio::sync::RwLock;
  15. use tracing::instrument;
  16. use zeroize::Zeroize;
  17. use super::builder::WalletBuilder;
  18. use super::receive::ReceiveOptions;
  19. use super::send::{PreparedSend, SendOptions};
  20. use super::Error;
  21. use crate::amount::SplitTarget;
  22. use crate::mint_url::MintUrl;
  23. use crate::nuts::nut00::ProofsMethods;
  24. use crate::nuts::nut23::QuoteState;
  25. use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SpendingConditions, State, Token};
  26. use crate::types::Melted;
  27. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  28. use crate::wallet::mint_connector::transport::tor_transport::TorAsync;
  29. use crate::wallet::types::MintQuote;
  30. use crate::{Amount, Wallet};
  31. // Transfer timeout constants
  32. /// Total timeout for waiting for Lightning payment confirmation during transfers
  33. /// This needs to be long enough to handle slow networks and Lightning routing
  34. const TRANSFER_PAYMENT_TIMEOUT_SECS: u64 = 120; // 2 minutes
  35. /// Transfer mode for mint-to-mint transfers
  36. #[derive(Debug, Clone)]
  37. pub enum TransferMode {
  38. /// Transfer exact amount to target (target receives specified amount)
  39. ExactReceive(Amount),
  40. /// Transfer all available balance (source will be emptied)
  41. FullBalance,
  42. }
  43. /// Result of a transfer operation with detailed breakdown
  44. #[derive(Debug, Clone)]
  45. pub struct TransferResult {
  46. /// Amount deducted from source mint
  47. pub amount_sent: Amount,
  48. /// Amount received at target mint
  49. pub amount_received: Amount,
  50. /// Total fees paid for the transfer
  51. pub fees_paid: Amount,
  52. /// Remaining balance in source mint after transfer
  53. pub source_balance_after: Amount,
  54. /// New balance in target mint after transfer
  55. pub target_balance_after: Amount,
  56. }
  57. /// Data extracted from a token including mint URL, proofs, and memo
  58. #[derive(Debug, Clone)]
  59. pub struct TokenData {
  60. /// The mint URL from the token
  61. pub mint_url: MintUrl,
  62. /// The proofs contained in the token
  63. pub proofs: Proofs,
  64. /// The memo from the token, if present
  65. pub memo: Option<String>,
  66. /// Value of token
  67. pub value: Amount,
  68. /// Unit of token
  69. pub unit: CurrencyUnit,
  70. /// Fee to redeem
  71. ///
  72. /// If the token is for a proof that we do not know, we cannot get the fee.
  73. /// To avoid just erroring and still allow decoding, this is an option.
  74. /// None does not mean there is no fee, it means we do not know the fee.
  75. pub redeem_fee: Option<Amount>,
  76. }
  77. /// Configuration for individual wallets within MultiMintWallet
  78. #[derive(Clone, Default, Debug)]
  79. pub struct WalletConfig {
  80. /// Custom mint connector implementation
  81. pub mint_connector: Option<Arc<dyn super::MintConnector + Send + Sync>>,
  82. /// Custom auth connector implementation
  83. #[cfg(feature = "auth")]
  84. pub auth_connector: Option<Arc<dyn super::auth::AuthMintConnector + Send + Sync>>,
  85. /// Target number of proofs to maintain at each denomination
  86. pub target_proof_count: Option<usize>,
  87. }
  88. impl WalletConfig {
  89. /// Create a new empty WalletConfig
  90. pub fn new() -> Self {
  91. Self::default()
  92. }
  93. /// Set custom mint connector
  94. pub fn with_mint_connector(
  95. mut self,
  96. connector: Arc<dyn super::MintConnector + Send + Sync>,
  97. ) -> Self {
  98. self.mint_connector = Some(connector);
  99. self
  100. }
  101. /// Set custom auth connector
  102. #[cfg(feature = "auth")]
  103. pub fn with_auth_connector(
  104. mut self,
  105. connector: Arc<dyn super::auth::AuthMintConnector + Send + Sync>,
  106. ) -> Self {
  107. self.auth_connector = Some(connector);
  108. self
  109. }
  110. /// Set target proof count
  111. pub fn with_target_proof_count(mut self, count: usize) -> Self {
  112. self.target_proof_count = Some(count);
  113. self
  114. }
  115. }
  116. /// Multi Mint Wallet
  117. ///
  118. /// A wallet that manages multiple mints but supports only one currency unit.
  119. /// This simplifies the interface by removing the need to specify both mint and unit.
  120. ///
  121. /// # Examples
  122. ///
  123. /// ## Creating and using a multi-mint wallet
  124. /// ```ignore
  125. /// # use cdk::wallet::MultiMintWallet;
  126. /// # use cdk::mint_url::MintUrl;
  127. /// # use cdk::Amount;
  128. /// # use cdk::nuts::CurrencyUnit;
  129. /// # use std::sync::Arc;
  130. /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
  131. /// // Create a multi-mint wallet with a database
  132. /// // For real usage, you would use cdk_sqlite::wallet::memory::empty().await? or similar
  133. /// let seed = [0u8; 64]; // Use a secure random seed in production
  134. /// let database = cdk_sqlite::wallet::memory::empty().await?;
  135. ///
  136. /// let wallet = MultiMintWallet::new(
  137. /// Arc::new(database),
  138. /// seed,
  139. /// CurrencyUnit::Sat,
  140. /// ).await?;
  141. ///
  142. /// // Add mints to the wallet
  143. /// let mint_url1: MintUrl = "https://mint1.example.com".parse()?;
  144. /// let mint_url2: MintUrl = "https://mint2.example.com".parse()?;
  145. /// wallet.add_mint(mint_url1.clone()).await?;
  146. /// wallet.add_mint(mint_url2).await?;
  147. ///
  148. /// // Check total balance across all mints
  149. /// let balance = wallet.total_balance().await?;
  150. /// println!("Total balance: {} sats", balance);
  151. ///
  152. /// // Send tokens from a specific mint
  153. /// let prepared = wallet.prepare_send(
  154. /// mint_url1,
  155. /// Amount::from(100),
  156. /// Default::default()
  157. /// ).await?;
  158. /// let token = prepared.confirm(None).await?;
  159. /// # Ok(())
  160. /// # }
  161. /// ```
  162. #[derive(Clone)]
  163. pub struct MultiMintWallet {
  164. /// Storage backend
  165. localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
  166. seed: [u8; 64],
  167. /// The currency unit this wallet supports
  168. unit: CurrencyUnit,
  169. /// Wallets indexed by mint URL
  170. wallets: Arc<RwLock<BTreeMap<MintUrl, Wallet>>>,
  171. /// Proxy configuration for HTTP clients (optional)
  172. proxy_config: Option<url::Url>,
  173. /// Shared Tor transport to be cloned into each TorHttpClient (if enabled)
  174. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  175. shared_tor_transport: Option<TorAsync>,
  176. }
  177. impl MultiMintWallet {
  178. /// Create a new [MultiMintWallet] for a specific currency unit
  179. pub async fn new(
  180. localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
  181. seed: [u8; 64],
  182. unit: CurrencyUnit,
  183. ) -> Result<Self, Error> {
  184. let wallet = Self {
  185. localstore,
  186. seed,
  187. unit,
  188. wallets: Arc::new(RwLock::new(BTreeMap::new())),
  189. proxy_config: None,
  190. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  191. shared_tor_transport: None,
  192. };
  193. // Automatically load wallets from database for this currency unit
  194. wallet.load_wallets().await?;
  195. Ok(wallet)
  196. }
  197. /// Create a new [MultiMintWallet] with proxy configuration
  198. ///
  199. /// All wallets in this MultiMintWallet will use the specified proxy.
  200. /// This allows you to route all mint connections through a proxy server.
  201. pub async fn new_with_proxy(
  202. localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
  203. seed: [u8; 64],
  204. unit: CurrencyUnit,
  205. proxy_url: url::Url,
  206. ) -> Result<Self, Error> {
  207. let wallet = Self {
  208. localstore,
  209. seed,
  210. unit,
  211. wallets: Arc::new(RwLock::new(BTreeMap::new())),
  212. proxy_config: Some(proxy_url),
  213. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  214. shared_tor_transport: None,
  215. };
  216. // Automatically load wallets from database for this currency unit
  217. wallet.load_wallets().await?;
  218. Ok(wallet)
  219. }
  220. /// Create a new [MultiMintWallet] with Tor transport for all wallets
  221. ///
  222. /// When the `tor` feature is enabled (and not on wasm32), this constructor
  223. /// creates a single Tor transport (TorAsync) that is cloned into each
  224. /// TorHttpClient used by per-mint Wallets. This ensures only one Tor instance
  225. /// is bootstrapped and shared across wallets.
  226. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  227. pub async fn new_with_tor(
  228. localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
  229. seed: [u8; 64],
  230. unit: CurrencyUnit,
  231. ) -> Result<Self, Error> {
  232. let wallet = Self {
  233. localstore,
  234. seed,
  235. unit,
  236. wallets: Arc::new(RwLock::new(BTreeMap::new())),
  237. proxy_config: None,
  238. shared_tor_transport: Some(TorAsync::new()),
  239. };
  240. // Automatically load wallets from database for this currency unit
  241. wallet.load_wallets().await?;
  242. Ok(wallet)
  243. }
  244. /// Adds a mint to this [MultiMintWallet]
  245. ///
  246. /// Creates a wallet for the specified mint using default or global settings.
  247. /// For custom configuration, use `add_mint_with_config()`.
  248. #[instrument(skip(self))]
  249. pub async fn add_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
  250. // Create wallet with default settings
  251. let wallet = self
  252. .create_wallet_with_config(mint_url.clone(), None)
  253. .await?;
  254. // Insert into wallets map
  255. let mut wallets = self.wallets.write().await;
  256. wallets.insert(mint_url, wallet);
  257. Ok(())
  258. }
  259. /// Adds a mint to this [MultiMintWallet] with custom configuration
  260. ///
  261. /// The provided configuration is used to create the wallet with custom connectors
  262. /// and settings. Configuration is stored within the Wallet instance itself.
  263. #[instrument(skip(self))]
  264. pub async fn add_mint_with_config(
  265. &self,
  266. mint_url: MintUrl,
  267. config: WalletConfig,
  268. ) -> Result<(), Error> {
  269. // Create wallet with the provided config
  270. let wallet = self
  271. .create_wallet_with_config(mint_url.clone(), Some(&config))
  272. .await?;
  273. // Insert into wallets map
  274. let mut wallets = self.wallets.write().await;
  275. wallets.insert(mint_url, wallet);
  276. Ok(())
  277. }
  278. /// Set or update configuration for a mint
  279. ///
  280. /// If the wallet already exists, it will be updated with the new config.
  281. /// If the wallet doesn't exist, it will be created with the specified config.
  282. #[instrument(skip(self))]
  283. pub async fn set_mint_config(
  284. &self,
  285. mint_url: MintUrl,
  286. config: WalletConfig,
  287. ) -> Result<(), Error> {
  288. // Check if wallet already exists
  289. if self.has_mint(&mint_url).await {
  290. // Update existing wallet in place
  291. let mut wallets = self.wallets.write().await;
  292. if let Some(wallet) = wallets.get_mut(&mint_url) {
  293. // Update target_proof_count if provided
  294. if let Some(count) = config.target_proof_count {
  295. wallet.set_target_proof_count(count);
  296. }
  297. // Update connector if provided
  298. if let Some(connector) = config.mint_connector {
  299. wallet.set_client(connector);
  300. }
  301. // TODO: Handle auth_connector if provided
  302. #[cfg(feature = "auth")]
  303. if let Some(_auth_connector) = config.auth_connector {
  304. // For now, we can't easily inject auth_connector into the wallet
  305. // This would require additional work on the Wallet API
  306. // We'll note this as a future enhancement
  307. }
  308. }
  309. Ok(())
  310. } else {
  311. // Wallet doesn't exist, create it with the provided config
  312. self.add_mint_with_config(mint_url, config).await
  313. }
  314. }
  315. /// Set the auth client (AuthWallet) for a specific mint
  316. ///
  317. /// This allows updating the auth wallet for an existing mint wallet without recreating it.
  318. #[cfg(feature = "auth")]
  319. #[instrument(skip_all)]
  320. pub async fn set_auth_client(
  321. &self,
  322. mint_url: &MintUrl,
  323. auth_wallet: Option<super::auth::AuthWallet>,
  324. ) -> Result<(), Error> {
  325. let wallets = self.wallets.read().await;
  326. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  327. mint_url: mint_url.to_string(),
  328. })?;
  329. wallet.set_auth_client(auth_wallet).await;
  330. Ok(())
  331. }
  332. /// Remove mint from MultiMintWallet
  333. #[instrument(skip(self))]
  334. pub async fn remove_mint(&self, mint_url: &MintUrl) {
  335. let mut wallets = self.wallets.write().await;
  336. wallets.remove(mint_url);
  337. }
  338. /// Internal: Create wallet with optional custom configuration
  339. ///
  340. /// Priority order for configuration:
  341. /// 1. Custom connector from config (if provided)
  342. /// 2. Global settings (proxy/Tor)
  343. /// 3. Default HttpClient
  344. async fn create_wallet_with_config(
  345. &self,
  346. mint_url: MintUrl,
  347. config: Option<&WalletConfig>,
  348. ) -> Result<Wallet, Error> {
  349. // Check if custom connector is provided in config
  350. if let Some(cfg) = config {
  351. if let Some(custom_connector) = &cfg.mint_connector {
  352. // Use custom connector with WalletBuilder
  353. let builder = WalletBuilder::new()
  354. .mint_url(mint_url.clone())
  355. .unit(self.unit.clone())
  356. .localstore(self.localstore.clone())
  357. .seed(self.seed)
  358. .target_proof_count(cfg.target_proof_count.unwrap_or(3))
  359. .shared_client(custom_connector.clone());
  360. // TODO: Handle auth_connector if provided
  361. #[cfg(feature = "auth")]
  362. if let Some(_auth_connector) = &cfg.auth_connector {
  363. // For now, we can't easily inject auth_connector into the wallet
  364. // This would require additional work on the Wallet/WalletBuilder API
  365. // We'll note this as a future enhancement
  366. }
  367. return builder.build();
  368. }
  369. }
  370. // Fall back to existing logic: proxy/Tor/default
  371. let target_proof_count = config.and_then(|c| c.target_proof_count).unwrap_or(3);
  372. let wallet = if let Some(proxy_url) = &self.proxy_config {
  373. // Create wallet with proxy-configured client
  374. let client = crate::wallet::HttpClient::with_proxy(
  375. mint_url.clone(),
  376. proxy_url.clone(),
  377. None,
  378. true,
  379. )
  380. .unwrap_or_else(|_| {
  381. #[cfg(feature = "auth")]
  382. {
  383. crate::wallet::HttpClient::new(mint_url.clone(), None)
  384. }
  385. #[cfg(not(feature = "auth"))]
  386. {
  387. crate::wallet::HttpClient::new(mint_url.clone())
  388. }
  389. });
  390. WalletBuilder::new()
  391. .mint_url(mint_url.clone())
  392. .unit(self.unit.clone())
  393. .localstore(self.localstore.clone())
  394. .seed(self.seed)
  395. .target_proof_count(target_proof_count)
  396. .client(client)
  397. .build()?
  398. } else {
  399. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  400. if let Some(tor) = &self.shared_tor_transport {
  401. // Create wallet with Tor transport client, cloning the shared transport
  402. let client = {
  403. let transport = tor.clone();
  404. #[cfg(feature = "auth")]
  405. {
  406. crate::wallet::TorHttpClient::with_transport(
  407. mint_url.clone(),
  408. transport,
  409. None,
  410. )
  411. }
  412. #[cfg(not(feature = "auth"))]
  413. {
  414. crate::wallet::TorHttpClient::with_transport(mint_url.clone(), transport)
  415. }
  416. };
  417. WalletBuilder::new()
  418. .mint_url(mint_url.clone())
  419. .unit(self.unit.clone())
  420. .localstore(self.localstore.clone())
  421. .seed(self.seed)
  422. .target_proof_count(target_proof_count)
  423. .client(client)
  424. .build()?
  425. } else {
  426. // Create wallet with default client
  427. Wallet::new(
  428. &mint_url.to_string(),
  429. self.unit.clone(),
  430. self.localstore.clone(),
  431. self.seed,
  432. Some(target_proof_count),
  433. )?
  434. }
  435. #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
  436. {
  437. // Create wallet with default client
  438. Wallet::new(
  439. &mint_url.to_string(),
  440. self.unit.clone(),
  441. self.localstore.clone(),
  442. self.seed,
  443. Some(target_proof_count),
  444. )?
  445. }
  446. };
  447. Ok(wallet)
  448. }
  449. /// Load all wallets from database that have proofs for this currency unit
  450. #[instrument(skip(self))]
  451. async fn load_wallets(&self) -> Result<(), Error> {
  452. let mints = self.localstore.get_mints().await.map_err(Error::Database)?;
  453. // Get all proofs for this currency unit to determine which mints are relevant
  454. let all_proofs = self
  455. .localstore
  456. .get_proofs(None, Some(self.unit.clone()), None, None)
  457. .await
  458. .map_err(Error::Database)?;
  459. for (mint_url, _mint_info) in mints {
  460. // Check if this mint has any proofs for the specified currency unit
  461. // or if we have no proofs at all (initial setup)
  462. let mint_has_proofs_for_unit =
  463. all_proofs.is_empty() || all_proofs.iter().any(|proof| proof.mint_url == mint_url);
  464. if mint_has_proofs_for_unit {
  465. // Add mint to the MultiMintWallet if not already present
  466. if !self.has_mint(&mint_url).await {
  467. self.add_mint(mint_url.clone()).await?
  468. }
  469. }
  470. }
  471. Ok(())
  472. }
  473. /// Get Wallets from MultiMintWallet
  474. #[instrument(skip(self))]
  475. pub async fn get_wallets(&self) -> Vec<Wallet> {
  476. self.wallets.read().await.values().cloned().collect()
  477. }
  478. /// Get Wallet from MultiMintWallet
  479. #[instrument(skip(self))]
  480. pub async fn get_wallet(&self, mint_url: &MintUrl) -> Option<Wallet> {
  481. self.wallets.read().await.get(mint_url).cloned()
  482. }
  483. /// Check if mint is in wallet
  484. #[instrument(skip(self))]
  485. pub async fn has_mint(&self, mint_url: &MintUrl) -> bool {
  486. self.wallets.read().await.contains_key(mint_url)
  487. }
  488. /// Get the currency unit for this wallet
  489. pub fn unit(&self) -> &CurrencyUnit {
  490. &self.unit
  491. }
  492. /// Get keysets for a mint url
  493. pub async fn get_mint_keysets(&self, mint_url: &MintUrl) -> Result<Vec<KeySetInfo>, Error> {
  494. let wallets = self.wallets.read().await;
  495. let target_wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  496. mint_url: mint_url.to_string(),
  497. })?;
  498. target_wallet.get_mint_keysets().await
  499. }
  500. /// Get token data (mint URL and proofs) from a token
  501. ///
  502. /// This method extracts the mint URL and proofs from a token. It will automatically
  503. /// fetch the keysets from the mint if needed to properly decode the proofs.
  504. ///
  505. /// The mint must already be added to the wallet. If the mint is not in the wallet,
  506. /// use `add_mint` first or set `allow_untrusted` in receive options.
  507. ///
  508. /// # Arguments
  509. ///
  510. /// * `token` - The token to extract data from
  511. ///
  512. /// # Returns
  513. ///
  514. /// A `TokenData` struct containing the mint URL and proofs
  515. ///
  516. /// # Example
  517. ///
  518. /// ```no_run
  519. /// # use cdk::wallet::MultiMintWallet;
  520. /// # use cdk::nuts::Token;
  521. /// # use std::str::FromStr;
  522. /// # async fn example(wallet: &MultiMintWallet) -> Result<(), Box<dyn std::error::Error>> {
  523. /// let token = Token::from_str("cashuA...")?;
  524. /// let token_data = wallet.get_token_data(&token).await?;
  525. /// println!("Mint: {}", token_data.mint_url);
  526. /// println!("Proofs: {} total", token_data.proofs.len());
  527. /// # Ok(())
  528. /// # }
  529. /// ```
  530. #[instrument(skip(self, token))]
  531. pub async fn get_token_data(&self, token: &Token) -> Result<TokenData, Error> {
  532. let mint_url = token.mint_url()?;
  533. // Get the keysets for this mint
  534. let keysets = self.get_mint_keysets(&mint_url).await?;
  535. // Extract proofs using the keysets
  536. let proofs = token.proofs(&keysets)?;
  537. // Get the memo
  538. let memo = token.memo().clone();
  539. let redeem_fee = self.get_proofs_fee(&mint_url, &proofs).await.ok();
  540. Ok(TokenData {
  541. value: proofs.total_amount()?,
  542. mint_url,
  543. proofs,
  544. memo,
  545. unit: token.unit().unwrap_or_default(),
  546. redeem_fee,
  547. })
  548. }
  549. /// Get wallet balances for all mints
  550. #[instrument(skip(self))]
  551. pub async fn get_balances(&self) -> Result<BTreeMap<MintUrl, Amount>, Error> {
  552. let mut balances = BTreeMap::new();
  553. for (mint_url, wallet) in self.wallets.read().await.iter() {
  554. let wallet_balance = wallet.total_balance().await?;
  555. balances.insert(mint_url.clone(), wallet_balance);
  556. }
  557. Ok(balances)
  558. }
  559. /// List proofs.
  560. #[instrument(skip(self))]
  561. pub async fn list_proofs(&self) -> Result<BTreeMap<MintUrl, Vec<Proof>>, Error> {
  562. let mut mint_proofs = BTreeMap::new();
  563. for (mint_url, wallet) in self.wallets.read().await.iter() {
  564. let wallet_proofs = wallet.get_unspent_proofs().await?;
  565. mint_proofs.insert(mint_url.clone(), wallet_proofs);
  566. }
  567. Ok(mint_proofs)
  568. }
  569. /// NUT-07 Check the state of proofs with a specific mint
  570. #[instrument(skip(self, proofs))]
  571. pub async fn check_proofs_state(
  572. &self,
  573. mint_url: &MintUrl,
  574. proofs: Proofs,
  575. ) -> Result<Vec<State>, Error> {
  576. let wallet = self.get_wallet(mint_url).await.ok_or(Error::UnknownMint {
  577. mint_url: mint_url.to_string(),
  578. })?;
  579. let states = wallet.check_proofs_spent(proofs).await?;
  580. Ok(states.into_iter().map(|s| s.state).collect())
  581. }
  582. /// Fee required to redeem proof set
  583. #[instrument(skip(self, proofs))]
  584. pub async fn get_proofs_fee(
  585. &self,
  586. mint_url: &MintUrl,
  587. proofs: &Proofs,
  588. ) -> Result<Amount, Error> {
  589. let wallets = self.wallets.read().await;
  590. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  591. mint_url: mint_url.to_string(),
  592. })?;
  593. Ok(wallet.get_proofs_fee(proofs).await?.total)
  594. }
  595. /// List transactions
  596. #[instrument(skip(self))]
  597. pub async fn list_transactions(
  598. &self,
  599. direction: Option<TransactionDirection>,
  600. ) -> Result<Vec<Transaction>, Error> {
  601. let mut transactions = Vec::new();
  602. for (_, wallet) in self.wallets.read().await.iter() {
  603. let wallet_transactions = wallet.list_transactions(direction).await?;
  604. transactions.extend(wallet_transactions);
  605. }
  606. transactions.sort();
  607. Ok(transactions)
  608. }
  609. /// Get proofs for a transaction by transaction ID
  610. ///
  611. /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
  612. /// it will only check that specific mint's wallet. Otherwise, it searches across all
  613. /// wallets to find which mint the transaction belongs to.
  614. ///
  615. /// # Arguments
  616. ///
  617. /// * `id` - The transaction ID
  618. /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
  619. #[instrument(skip(self))]
  620. pub async fn get_proofs_for_transaction(
  621. &self,
  622. id: TransactionId,
  623. mint_url: Option<MintUrl>,
  624. ) -> Result<Proofs, Error> {
  625. let wallets = self.wallets.read().await;
  626. // If mint_url is provided, try that wallet directly
  627. if let Some(mint_url) = mint_url {
  628. if let Some(wallet) = wallets.get(&mint_url) {
  629. // Verify the transaction exists in this wallet
  630. if wallet.get_transaction(id).await?.is_some() {
  631. return wallet.get_proofs_for_transaction(id).await;
  632. }
  633. }
  634. // Transaction not found in specified mint
  635. return Err(Error::TransactionNotFound);
  636. }
  637. // No mint_url provided, search across all wallets
  638. for (mint_url, wallet) in wallets.iter() {
  639. if let Some(transaction) = wallet.get_transaction(id).await? {
  640. // Verify the transaction belongs to this wallet's mint
  641. if &transaction.mint_url == mint_url {
  642. return wallet.get_proofs_for_transaction(id).await;
  643. }
  644. }
  645. }
  646. // Transaction not found in any wallet
  647. Err(Error::TransactionNotFound)
  648. }
  649. /// Get total balance across all wallets (since all wallets use the same currency unit)
  650. #[instrument(skip(self))]
  651. pub async fn total_balance(&self) -> Result<Amount, Error> {
  652. let mut total = Amount::ZERO;
  653. for (_, wallet) in self.wallets.read().await.iter() {
  654. total += wallet.total_balance().await?;
  655. }
  656. Ok(total)
  657. }
  658. /// Prepare to send tokens from a specific mint with optional transfer from other mints
  659. ///
  660. /// This method ensures that sends always happen from only one mint. If the specified
  661. /// mint doesn't have sufficient balance and `allow_transfer` is enabled in options,
  662. /// it will first transfer funds from other mints to the target mint.
  663. #[instrument(skip(self))]
  664. pub async fn prepare_send(
  665. &self,
  666. mint_url: MintUrl,
  667. amount: Amount,
  668. opts: MultiMintSendOptions,
  669. ) -> Result<PreparedSend, Error> {
  670. // Ensure the mint exists
  671. let wallets = self.wallets.read().await;
  672. let target_wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  673. mint_url: mint_url.to_string(),
  674. })?;
  675. // Check current balance of target mint
  676. let target_balance = target_wallet.total_balance().await?;
  677. // If target mint has sufficient balance, prepare send directly
  678. if target_balance >= amount {
  679. return target_wallet.prepare_send(amount, opts.send_options).await;
  680. }
  681. // If transfer is not allowed, return insufficient funds error
  682. if !opts.allow_transfer {
  683. return Err(Error::InsufficientFunds);
  684. }
  685. // Calculate how much we need to transfer
  686. let transfer_needed = amount - target_balance;
  687. // Check if transfer amount exceeds max_transfer_amount
  688. if let Some(max_transfer) = opts.max_transfer_amount {
  689. if transfer_needed > max_transfer {
  690. return Err(Error::InsufficientFunds);
  691. }
  692. }
  693. // Find source wallets with available funds for transfer
  694. let mut available_for_transfer = Amount::ZERO;
  695. let mut source_mints = Vec::new();
  696. for (source_mint_url, wallet) in wallets.iter() {
  697. if source_mint_url == &mint_url {
  698. continue; // Skip the target mint
  699. }
  700. // Check if this mint is excluded from transfers
  701. if opts.excluded_mints.contains(source_mint_url) {
  702. continue;
  703. }
  704. // Check if we have a restricted allowed list and this mint isn't in it
  705. if !opts.allowed_mints.is_empty() && !opts.allowed_mints.contains(source_mint_url) {
  706. continue;
  707. }
  708. let balance = wallet.total_balance().await?;
  709. if balance > Amount::ZERO {
  710. source_mints.push((source_mint_url.clone(), balance));
  711. available_for_transfer += balance;
  712. }
  713. }
  714. // Check if we have enough funds across all mints
  715. if available_for_transfer < transfer_needed {
  716. return Err(Error::InsufficientFunds);
  717. }
  718. // Drop the read lock before performing transfers
  719. drop(wallets);
  720. // Perform transfers from source wallets to target wallet
  721. self.transfer_parallel(&mint_url, transfer_needed, source_mints)
  722. .await?;
  723. // Now prepare the send from the target mint
  724. let wallets = self.wallets.read().await;
  725. let target_wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  726. mint_url: mint_url.to_string(),
  727. })?;
  728. target_wallet.prepare_send(amount, opts.send_options).await
  729. }
  730. /// Transfer funds from a single source wallet to target mint using Lightning Network (melt/mint)
  731. ///
  732. /// This function properly accounts for fees by handling different transfer modes:
  733. /// - ExactReceive: Target receives exactly the specified amount, source pays amount + fees
  734. /// - FullBalance: All source balance is transferred, target receives balance - fees
  735. pub async fn transfer(
  736. &self,
  737. source_mint_url: &MintUrl,
  738. target_mint_url: &MintUrl,
  739. mode: TransferMode,
  740. ) -> Result<TransferResult, Error> {
  741. // Get wallets for the specified mints and clone them to release the lock
  742. let (source_wallet, target_wallet) = {
  743. let wallets = self.wallets.read().await;
  744. let source = wallets
  745. .get(source_mint_url)
  746. .ok_or(Error::UnknownMint {
  747. mint_url: source_mint_url.to_string(),
  748. })?
  749. .clone();
  750. let target = wallets
  751. .get(target_mint_url)
  752. .ok_or(Error::UnknownMint {
  753. mint_url: target_mint_url.to_string(),
  754. })?
  755. .clone();
  756. (source, target)
  757. };
  758. // Get initial balance
  759. let source_balance_initial = source_wallet.total_balance().await?;
  760. // Handle different transfer modes
  761. let (final_mint_quote, final_melt_quote) = match mode {
  762. TransferMode::ExactReceive(amount) => {
  763. self.handle_exact_receive_transfer(
  764. &source_wallet,
  765. &target_wallet,
  766. amount,
  767. source_balance_initial,
  768. )
  769. .await?
  770. }
  771. TransferMode::FullBalance => {
  772. self.handle_full_balance_transfer(
  773. &source_wallet,
  774. &target_wallet,
  775. source_balance_initial,
  776. )
  777. .await?
  778. }
  779. };
  780. // Execute the transfer
  781. let (melted, actual_receive_amount) = self
  782. .execute_transfer(
  783. &source_wallet,
  784. &target_wallet,
  785. &final_mint_quote,
  786. &final_melt_quote,
  787. )
  788. .await?;
  789. // Get final balances
  790. let source_balance_final = source_wallet.total_balance().await?;
  791. let target_balance_final = target_wallet.total_balance().await?;
  792. let amount_sent = source_balance_initial - source_balance_final;
  793. let fees_paid = melted.fee_paid;
  794. tracing::info!(
  795. "Transferred {} from {} to {} via Lightning (sent: {} sats, received: {} sats, fee: {} sats)",
  796. amount_sent,
  797. source_wallet.mint_url,
  798. target_wallet.mint_url,
  799. amount_sent,
  800. actual_receive_amount,
  801. fees_paid
  802. );
  803. Ok(TransferResult {
  804. amount_sent,
  805. amount_received: actual_receive_amount,
  806. fees_paid,
  807. source_balance_after: source_balance_final,
  808. target_balance_after: target_balance_final,
  809. })
  810. }
  811. /// Handle exact receive transfer mode - target gets exactly the specified amount
  812. async fn handle_exact_receive_transfer(
  813. &self,
  814. source_wallet: &Wallet,
  815. target_wallet: &Wallet,
  816. amount: Amount,
  817. source_balance: Amount,
  818. ) -> Result<(MintQuote, crate::wallet::types::MeltQuote), Error> {
  819. // Step 1: Create mint quote at target mint for the exact amount we want to receive
  820. let mint_quote = target_wallet.mint_quote(amount, None).await?;
  821. // Step 2: Create melt quote at source mint for the invoice
  822. let melt_quote = source_wallet
  823. .melt_quote(mint_quote.request.clone(), None)
  824. .await?;
  825. // Step 3: Check if source has enough balance for the total amount needed (amount + melt fees)
  826. let total_needed = melt_quote.amount + melt_quote.fee_reserve;
  827. if source_balance < total_needed {
  828. return Err(Error::InsufficientFunds);
  829. }
  830. Ok((mint_quote, melt_quote))
  831. }
  832. /// Handle full balance transfer mode - all source balance is transferred
  833. async fn handle_full_balance_transfer(
  834. &self,
  835. source_wallet: &Wallet,
  836. target_wallet: &Wallet,
  837. source_balance: Amount,
  838. ) -> Result<(MintQuote, crate::wallet::types::MeltQuote), Error> {
  839. if source_balance == Amount::ZERO {
  840. return Err(Error::InsufficientFunds);
  841. }
  842. // Step 1: Create melt quote for full balance to discover fees
  843. // We need to create a dummy mint quote first to get an invoice
  844. let dummy_mint_quote = target_wallet.mint_quote(source_balance, None).await?;
  845. let probe_melt_quote = source_wallet
  846. .melt_quote(dummy_mint_quote.request.clone(), None)
  847. .await?;
  848. // Step 2: Calculate actual receive amount (balance - fees)
  849. let receive_amount = source_balance
  850. .checked_sub(probe_melt_quote.fee_reserve)
  851. .ok_or(Error::InsufficientFunds)?;
  852. if receive_amount == Amount::ZERO {
  853. return Err(Error::InsufficientFunds);
  854. }
  855. // Step 3: Create final mint quote for the net amount
  856. let final_mint_quote = target_wallet.mint_quote(receive_amount, None).await?;
  857. // Step 4: Create final melt quote with the new invoice
  858. let final_melt_quote = source_wallet
  859. .melt_quote(final_mint_quote.request.clone(), None)
  860. .await?;
  861. Ok((final_mint_quote, final_melt_quote))
  862. }
  863. /// Execute the actual transfer using the prepared quotes
  864. async fn execute_transfer(
  865. &self,
  866. source_wallet: &Wallet,
  867. target_wallet: &Wallet,
  868. final_mint_quote: &MintQuote,
  869. final_melt_quote: &crate::wallet::types::MeltQuote,
  870. ) -> Result<(Melted, Amount), Error> {
  871. // Step 1: Subscribe to mint quote updates before melting
  872. let mut subscription = target_wallet
  873. .subscribe(super::WalletSubscription::Bolt11MintQuoteState(vec![
  874. final_mint_quote.id.clone(),
  875. ]))
  876. .await;
  877. // Step 2: Melt from source wallet using the final melt quote
  878. let melted = source_wallet.melt(&final_melt_quote.id).await?;
  879. // Step 3: Wait for payment confirmation via subscription
  880. tracing::debug!(
  881. "Waiting for Lightning payment confirmation (max {} seconds) for transfer from {} to {}",
  882. TRANSFER_PAYMENT_TIMEOUT_SECS,
  883. source_wallet.mint_url,
  884. target_wallet.mint_url
  885. );
  886. // Wait for payment notification with overall timeout
  887. let timeout_duration = tokio::time::Duration::from_secs(TRANSFER_PAYMENT_TIMEOUT_SECS);
  888. loop {
  889. match tokio::time::timeout(timeout_duration, subscription.recv()).await {
  890. Ok(Some(notification)) => {
  891. // Check if this is a mint quote response with paid state
  892. if let crate::nuts::nut17::NotificationPayload::MintQuoteBolt11Response(
  893. quote_response,
  894. ) = notification.deref()
  895. {
  896. if quote_response.state == QuoteState::Paid {
  897. // Quote is paid, now mint the tokens
  898. target_wallet
  899. .mint(
  900. &final_mint_quote.id,
  901. crate::amount::SplitTarget::default(),
  902. None,
  903. )
  904. .await?;
  905. break;
  906. }
  907. }
  908. }
  909. Ok(None) => {
  910. // Subscription closed
  911. tracing::warn!("Subscription closed while waiting for mint quote payment");
  912. return Err(Error::TransferTimeout {
  913. source_mint: source_wallet.mint_url.to_string(),
  914. target_mint: target_wallet.mint_url.to_string(),
  915. amount: final_mint_quote.amount.unwrap_or(Amount::ZERO),
  916. });
  917. }
  918. Err(_) => {
  919. // Overall timeout reached
  920. tracing::warn!(
  921. "Transfer timed out after {} seconds waiting for Lightning payment confirmation",
  922. TRANSFER_PAYMENT_TIMEOUT_SECS
  923. );
  924. return Err(Error::TransferTimeout {
  925. source_mint: source_wallet.mint_url.to_string(),
  926. target_mint: target_wallet.mint_url.to_string(),
  927. amount: final_mint_quote.amount.unwrap_or(Amount::ZERO),
  928. });
  929. }
  930. }
  931. }
  932. let actual_receive_amount = final_mint_quote.amount.unwrap_or(Amount::ZERO);
  933. Ok((melted, actual_receive_amount))
  934. }
  935. /// Transfer funds from multiple source wallets to target mint in parallel
  936. async fn transfer_parallel(
  937. &self,
  938. target_mint_url: &MintUrl,
  939. total_amount: Amount,
  940. source_mints: Vec<(MintUrl, Amount)>,
  941. ) -> Result<(), Error> {
  942. let mut remaining_amount = total_amount;
  943. let mut transfer_tasks = Vec::new();
  944. // Create transfer tasks for each source wallet
  945. for (source_mint_url, available_balance) in source_mints {
  946. if remaining_amount == Amount::ZERO {
  947. break;
  948. }
  949. let transfer_amount = std::cmp::min(remaining_amount, available_balance);
  950. remaining_amount -= transfer_amount;
  951. let self_clone = self.clone();
  952. let source_mint_url = source_mint_url.clone();
  953. let target_mint_url = target_mint_url.clone();
  954. // Spawn parallel transfer task
  955. let task = spawn(async move {
  956. self_clone
  957. .transfer(
  958. &source_mint_url,
  959. &target_mint_url,
  960. TransferMode::ExactReceive(transfer_amount),
  961. )
  962. .await
  963. .map(|result| result.amount_received)
  964. });
  965. transfer_tasks.push(task);
  966. }
  967. // Wait for all transfers to complete
  968. let mut total_transferred = Amount::ZERO;
  969. for task in transfer_tasks {
  970. match task.await {
  971. Ok(Ok(amount)) => {
  972. total_transferred += amount;
  973. }
  974. Ok(Err(e)) => {
  975. tracing::error!("Transfer failed: {}", e);
  976. return Err(e);
  977. }
  978. Err(e) => {
  979. tracing::error!("Transfer task panicked: {}", e);
  980. return Err(Error::Internal);
  981. }
  982. }
  983. }
  984. // Check if we transferred less than expected (accounting for fees)
  985. // We don't return an error here as fees are expected
  986. if total_transferred < total_amount {
  987. let fee_paid = total_amount - total_transferred;
  988. tracing::info!(
  989. "Transfer completed with fees: requested {}, received {}, total fees {}",
  990. total_amount,
  991. total_transferred,
  992. fee_paid
  993. );
  994. }
  995. Ok(())
  996. }
  997. /// Mint quote for wallet
  998. #[instrument(skip(self))]
  999. pub async fn mint_quote(
  1000. &self,
  1001. mint_url: &MintUrl,
  1002. amount: Amount,
  1003. description: Option<String>,
  1004. ) -> Result<MintQuote, Error> {
  1005. let wallets = self.wallets.read().await;
  1006. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1007. mint_url: mint_url.to_string(),
  1008. })?;
  1009. wallet.mint_quote(amount, description).await
  1010. }
  1011. /// Check a specific mint quote status
  1012. #[instrument(skip(self))]
  1013. pub async fn check_mint_quote(
  1014. &self,
  1015. mint_url: &MintUrl,
  1016. quote_id: &str,
  1017. ) -> Result<MintQuote, Error> {
  1018. let wallets = self.wallets.read().await;
  1019. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1020. mint_url: mint_url.to_string(),
  1021. })?;
  1022. // Check the quote state from the mint
  1023. wallet.mint_quote_state(quote_id).await?;
  1024. // Get the updated quote from local storage
  1025. let quote = wallet
  1026. .localstore
  1027. .get_mint_quote(quote_id)
  1028. .await
  1029. .map_err(Error::Database)?
  1030. .ok_or(Error::UnknownQuote)?;
  1031. Ok(quote)
  1032. }
  1033. /// Check all mint quotes
  1034. /// If quote is paid, wallet will mint
  1035. #[instrument(skip(self))]
  1036. pub async fn check_all_mint_quotes(&self, mint_url: Option<MintUrl>) -> Result<Amount, Error> {
  1037. let mut total_amount = Amount::ZERO;
  1038. match mint_url {
  1039. Some(mint_url) => {
  1040. let wallets = self.wallets.read().await;
  1041. let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  1042. mint_url: mint_url.to_string(),
  1043. })?;
  1044. total_amount = wallet.check_all_mint_quotes().await?;
  1045. }
  1046. None => {
  1047. for (_, wallet) in self.wallets.read().await.iter() {
  1048. let amount = wallet.check_all_mint_quotes().await?;
  1049. total_amount += amount;
  1050. }
  1051. }
  1052. }
  1053. Ok(total_amount)
  1054. }
  1055. /// Mint a specific quote
  1056. #[instrument(skip(self))]
  1057. pub async fn mint(
  1058. &self,
  1059. mint_url: &MintUrl,
  1060. quote_id: &str,
  1061. conditions: Option<SpendingConditions>,
  1062. ) -> Result<Proofs, Error> {
  1063. let wallets = self.wallets.read().await;
  1064. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1065. mint_url: mint_url.to_string(),
  1066. })?;
  1067. wallet
  1068. .mint(quote_id, SplitTarget::default(), conditions)
  1069. .await
  1070. }
  1071. /// Wait for a mint quote to be paid and automatically mint the proofs
  1072. #[cfg(not(target_arch = "wasm32"))]
  1073. #[instrument(skip(self))]
  1074. pub async fn wait_for_mint_quote(
  1075. &self,
  1076. mint_url: &MintUrl,
  1077. quote_id: &str,
  1078. split_target: SplitTarget,
  1079. conditions: Option<SpendingConditions>,
  1080. timeout_secs: u64,
  1081. ) -> Result<Proofs, Error> {
  1082. let wallets = self.wallets.read().await;
  1083. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1084. mint_url: mint_url.to_string(),
  1085. })?;
  1086. // Get the mint quote from local storage
  1087. let quote = wallet
  1088. .localstore
  1089. .get_mint_quote(quote_id)
  1090. .await
  1091. .map_err(Error::Database)?
  1092. .ok_or(Error::UnknownQuote)?;
  1093. // Wait for the quote to be paid and mint the proofs
  1094. let timeout_duration = tokio::time::Duration::from_secs(timeout_secs);
  1095. wallet
  1096. .wait_and_mint_quote(quote, split_target, conditions, timeout_duration)
  1097. .await
  1098. }
  1099. /// Receive token with multi-mint options
  1100. ///
  1101. /// This method can:
  1102. /// - Receive tokens from trusted mints (already added to the wallet)
  1103. /// - Optionally receive from untrusted mints by adding them to the wallet
  1104. /// - Optionally transfer tokens from untrusted mints to a trusted mint (and remove the untrusted mint)
  1105. ///
  1106. /// # Examples
  1107. /// ```no_run
  1108. /// # use cdk::wallet::{MultiMintWallet, MultiMintReceiveOptions};
  1109. /// # use cdk::mint_url::MintUrl;
  1110. /// # async fn example(wallet: MultiMintWallet) -> Result<(), Box<dyn std::error::Error>> {
  1111. /// // Receive from a trusted mint
  1112. /// let token = "cashuAey...";
  1113. /// let amount = wallet
  1114. /// .receive(token, MultiMintReceiveOptions::default())
  1115. /// .await?;
  1116. ///
  1117. /// // Receive from untrusted mint and add it to the wallet
  1118. /// let options = MultiMintReceiveOptions::default().allow_untrusted(true);
  1119. /// let amount = wallet.receive(token, options).await?;
  1120. ///
  1121. /// // Receive from untrusted mint, transfer to trusted mint, then remove untrusted mint
  1122. /// let trusted_mint: MintUrl = "https://trusted.mint".parse()?;
  1123. /// let options = MultiMintReceiveOptions::default().transfer_to_mint(Some(trusted_mint));
  1124. /// let amount = wallet.receive(token, options).await?;
  1125. /// # Ok(())
  1126. /// # }
  1127. /// ```
  1128. #[instrument(skip_all)]
  1129. pub async fn receive(
  1130. &self,
  1131. encoded_token: &str,
  1132. opts: MultiMintReceiveOptions,
  1133. ) -> Result<Amount, Error> {
  1134. let token_data = Token::from_str(encoded_token)?;
  1135. let unit = token_data.unit().unwrap_or_default();
  1136. // Ensure the token uses the same currency unit as this wallet
  1137. if unit != self.unit {
  1138. return Err(Error::MultiMintCurrencyUnitMismatch {
  1139. expected: self.unit.clone(),
  1140. found: unit,
  1141. });
  1142. }
  1143. let mint_url = token_data.mint_url()?;
  1144. let is_trusted = self.has_mint(&mint_url).await;
  1145. // If mint is not trusted and we don't allow untrusted mints, error
  1146. if !is_trusted && !opts.allow_untrusted {
  1147. return Err(Error::UnknownMint {
  1148. mint_url: mint_url.to_string(),
  1149. });
  1150. }
  1151. // If mint is untrusted and we need to transfer, ensure we have a target mint
  1152. let should_transfer = !is_trusted && opts.transfer_to_mint.is_some();
  1153. // Add the untrusted mint temporarily if needed
  1154. if !is_trusted {
  1155. self.add_mint(mint_url.clone()).await?;
  1156. }
  1157. let wallets = self.wallets.read().await;
  1158. let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  1159. mint_url: mint_url.to_string(),
  1160. })?;
  1161. // We need the keysets information to properly convert from token proof to proof
  1162. let keysets_info = match self
  1163. .localstore
  1164. .get_mint_keysets(token_data.mint_url()?)
  1165. .await?
  1166. {
  1167. Some(keysets_info) => keysets_info,
  1168. // Hit the keysets endpoint if we don't have the keysets for this Mint
  1169. None => wallet.load_mint_keysets().await?,
  1170. };
  1171. let proofs = token_data.proofs(&keysets_info)?;
  1172. let mut amount_received = Amount::ZERO;
  1173. match wallet
  1174. .receive_proofs(proofs, opts.receive_options, token_data.memo().clone())
  1175. .await
  1176. {
  1177. Ok(amount) => {
  1178. amount_received += amount;
  1179. }
  1180. Err(err) => {
  1181. // If we added the mint temporarily for transfer only, remove it before returning error
  1182. if !is_trusted && opts.transfer_to_mint.is_some() {
  1183. drop(wallets);
  1184. self.remove_mint(&mint_url).await;
  1185. }
  1186. return Err(err);
  1187. }
  1188. }
  1189. drop(wallets);
  1190. // If we should transfer to a trusted mint, do so now
  1191. if should_transfer {
  1192. if let Some(target_mint) = opts.transfer_to_mint {
  1193. // Ensure target mint exists and is trusted
  1194. if !self.has_mint(&target_mint).await {
  1195. // Clean up untrusted mint if we're only using it for transfer
  1196. self.remove_mint(&mint_url).await;
  1197. return Err(Error::UnknownMint {
  1198. mint_url: target_mint.to_string(),
  1199. });
  1200. }
  1201. // Transfer the entire balance from the untrusted mint to the target mint
  1202. // Use FullBalance mode for efficient transfer of all funds
  1203. let transfer_result = self
  1204. .transfer(&mint_url, &target_mint, TransferMode::FullBalance)
  1205. .await;
  1206. // Handle transfer result - log details but don't fail if balance was zero
  1207. match transfer_result {
  1208. Ok(result) => {
  1209. if result.amount_sent > Amount::ZERO {
  1210. tracing::info!(
  1211. "Transferred {} sats from untrusted mint {} to trusted mint {} (received: {}, fees: {})",
  1212. result.amount_sent,
  1213. mint_url,
  1214. target_mint,
  1215. result.amount_received,
  1216. result.fees_paid
  1217. );
  1218. }
  1219. }
  1220. Err(Error::InsufficientFunds) => {
  1221. // No balance to transfer, which is fine
  1222. tracing::debug!("No balance to transfer from untrusted mint {}", mint_url);
  1223. }
  1224. Err(e) => return Err(e),
  1225. }
  1226. // Remove the untrusted mint after transfer
  1227. self.remove_mint(&mint_url).await;
  1228. }
  1229. }
  1230. // Note: If allow_untrusted is true but no transfer is requested,
  1231. // the untrusted mint is kept in the wallet (as intended)
  1232. Ok(amount_received)
  1233. }
  1234. /// Restore
  1235. #[instrument(skip(self))]
  1236. pub async fn restore(&self, mint_url: &MintUrl) -> Result<Amount, Error> {
  1237. let wallets = self.wallets.read().await;
  1238. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1239. mint_url: mint_url.to_string(),
  1240. })?;
  1241. wallet.restore().await
  1242. }
  1243. /// Verify token matches p2pk conditions
  1244. #[instrument(skip(self, token))]
  1245. pub async fn verify_token_p2pk(
  1246. &self,
  1247. token: &Token,
  1248. conditions: SpendingConditions,
  1249. ) -> Result<(), Error> {
  1250. let mint_url = token.mint_url()?;
  1251. let wallets = self.wallets.read().await;
  1252. let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  1253. mint_url: mint_url.to_string(),
  1254. })?;
  1255. wallet.verify_token_p2pk(token, conditions).await
  1256. }
  1257. /// Verifies all proofs in token have valid dleq proof
  1258. #[instrument(skip(self, token))]
  1259. pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
  1260. let mint_url = token.mint_url()?;
  1261. let wallets = self.wallets.read().await;
  1262. let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
  1263. mint_url: mint_url.to_string(),
  1264. })?;
  1265. wallet.verify_token_dleq(token).await
  1266. }
  1267. /// Create a melt quote for a specific mint
  1268. #[instrument(skip(self, bolt11))]
  1269. pub async fn melt_quote(
  1270. &self,
  1271. mint_url: &MintUrl,
  1272. bolt11: String,
  1273. options: Option<MeltOptions>,
  1274. ) -> Result<crate::wallet::types::MeltQuote, Error> {
  1275. let wallets = self.wallets.read().await;
  1276. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1277. mint_url: mint_url.to_string(),
  1278. })?;
  1279. wallet.melt_quote(bolt11, options).await
  1280. }
  1281. /// Melt (pay invoice) from a specific mint using a quote ID
  1282. #[instrument(skip(self))]
  1283. pub async fn melt_with_mint(
  1284. &self,
  1285. mint_url: &MintUrl,
  1286. quote_id: &str,
  1287. ) -> Result<Melted, Error> {
  1288. let wallets = self.wallets.read().await;
  1289. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1290. mint_url: mint_url.to_string(),
  1291. })?;
  1292. wallet.melt(quote_id).await
  1293. }
  1294. /// Melt specific proofs from a specific mint using a quote ID
  1295. ///
  1296. /// This method allows melting proofs that may not be in the wallet's database,
  1297. /// similar to how `receive_proofs` handles external proofs. The proofs will be
  1298. /// added to the database and used for the melt operation.
  1299. ///
  1300. /// # Arguments
  1301. ///
  1302. /// * `mint_url` - The mint to use for the melt operation
  1303. /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
  1304. /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
  1305. ///
  1306. /// # Returns
  1307. ///
  1308. /// A `Melted` result containing the payment details and any change proofs
  1309. #[instrument(skip(self, proofs))]
  1310. pub async fn melt_proofs(
  1311. &self,
  1312. mint_url: &MintUrl,
  1313. quote_id: &str,
  1314. proofs: Proofs,
  1315. ) -> Result<Melted, Error> {
  1316. let wallets = self.wallets.read().await;
  1317. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1318. mint_url: mint_url.to_string(),
  1319. })?;
  1320. wallet.melt_proofs(quote_id, proofs).await
  1321. }
  1322. /// Check a specific melt quote status
  1323. #[instrument(skip(self))]
  1324. pub async fn check_melt_quote(
  1325. &self,
  1326. mint_url: &MintUrl,
  1327. quote_id: &str,
  1328. ) -> Result<MeltQuote, Error> {
  1329. let wallets = self.wallets.read().await;
  1330. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1331. mint_url: mint_url.to_string(),
  1332. })?;
  1333. // Check the quote state from the mint
  1334. wallet.melt_quote_status(quote_id).await?;
  1335. // Get the updated quote from local storage
  1336. let quote = wallet
  1337. .localstore
  1338. .get_melt_quote(quote_id)
  1339. .await
  1340. .map_err(Error::Database)?
  1341. .ok_or(Error::UnknownQuote)?;
  1342. Ok(quote)
  1343. }
  1344. /// Create MPP (Multi-Path Payment) melt quotes from multiple mints
  1345. ///
  1346. /// This function allows manual specification of which mints and amounts to use for MPP.
  1347. /// Returns a vector of (MintUrl, MeltQuote) pairs.
  1348. #[instrument(skip(self, bolt11))]
  1349. pub async fn mpp_melt_quote(
  1350. &self,
  1351. bolt11: String,
  1352. mint_amounts: Vec<(MintUrl, Amount)>,
  1353. ) -> Result<Vec<(MintUrl, crate::wallet::types::MeltQuote)>, Error> {
  1354. let mut quotes = Vec::new();
  1355. let mut tasks = Vec::new();
  1356. // Spawn parallel tasks to get quotes from each mint
  1357. for (mint_url, amount) in mint_amounts {
  1358. let wallets = self.wallets.read().await;
  1359. let wallet = wallets
  1360. .get(&mint_url)
  1361. .ok_or(Error::UnknownMint {
  1362. mint_url: mint_url.to_string(),
  1363. })?
  1364. .clone();
  1365. drop(wallets);
  1366. let bolt11_clone = bolt11.clone();
  1367. let mint_url_clone = mint_url.clone();
  1368. // Convert amount to millisats for MeltOptions
  1369. let amount_msat = u64::from(amount) * 1000;
  1370. let options = Some(MeltOptions::new_mpp(amount_msat));
  1371. let task = spawn(async move {
  1372. let quote = wallet.melt_quote(bolt11_clone, options).await;
  1373. (mint_url_clone, quote)
  1374. });
  1375. tasks.push(task);
  1376. }
  1377. // Collect all quote results
  1378. for task in tasks {
  1379. match task.await {
  1380. Ok((mint_url, Ok(quote))) => {
  1381. quotes.push((mint_url, quote));
  1382. }
  1383. Ok((mint_url, Err(e))) => {
  1384. tracing::error!("Failed to get melt quote from {}: {}", mint_url, e);
  1385. return Err(e);
  1386. }
  1387. Err(e) => {
  1388. tracing::error!("Task failed: {}", e);
  1389. return Err(Error::Internal);
  1390. }
  1391. }
  1392. }
  1393. Ok(quotes)
  1394. }
  1395. /// Execute MPP melts using previously obtained quotes
  1396. #[instrument(skip(self))]
  1397. pub async fn mpp_melt(
  1398. &self,
  1399. quotes: Vec<(MintUrl, String)>, // (mint_url, quote_id)
  1400. ) -> Result<Vec<(MintUrl, Melted)>, Error> {
  1401. let mut results = Vec::new();
  1402. let mut tasks = Vec::new();
  1403. for (mint_url, quote_id) in quotes {
  1404. let wallets = self.wallets.read().await;
  1405. let wallet = wallets
  1406. .get(&mint_url)
  1407. .ok_or(Error::UnknownMint {
  1408. mint_url: mint_url.to_string(),
  1409. })?
  1410. .clone();
  1411. drop(wallets);
  1412. let mint_url_clone = mint_url.clone();
  1413. let task = spawn(async move {
  1414. let melted = wallet.melt(&quote_id).await;
  1415. (mint_url_clone, melted)
  1416. });
  1417. tasks.push(task);
  1418. }
  1419. // Collect all melt results
  1420. for task in tasks {
  1421. match task.await {
  1422. Ok((mint_url, Ok(melted))) => {
  1423. results.push((mint_url, melted));
  1424. }
  1425. Ok((mint_url, Err(e))) => {
  1426. tracing::error!("Failed to melt from {}: {}", mint_url, e);
  1427. return Err(e);
  1428. }
  1429. Err(e) => {
  1430. tracing::error!("Task failed: {}", e);
  1431. return Err(Error::Internal);
  1432. }
  1433. }
  1434. }
  1435. Ok(results)
  1436. }
  1437. /// Melt (pay invoice) with automatic wallet selection (deprecated, use specific mint functions for better control)
  1438. ///
  1439. /// Automatically selects the best wallet to pay from based on:
  1440. /// - Available balance
  1441. /// - Fees
  1442. ///
  1443. /// # Examples
  1444. /// ```no_run
  1445. /// # use cdk::wallet::MultiMintWallet;
  1446. /// # use cdk::Amount;
  1447. /// # use std::sync::Arc;
  1448. /// # async fn example(wallet: Arc<MultiMintWallet>) -> Result<(), Box<dyn std::error::Error>> {
  1449. /// // Pay a lightning invoice from any mint with sufficient balance
  1450. /// let invoice = "lnbc100n1p...";
  1451. ///
  1452. /// let result = wallet.melt(invoice, None, None).await?;
  1453. /// println!("Paid {} sats, fee was {} sats", result.amount, result.fee_paid);
  1454. /// # Ok(())
  1455. /// # }
  1456. /// ```
  1457. #[instrument(skip(self, bolt11))]
  1458. pub async fn melt(
  1459. &self,
  1460. bolt11: &str,
  1461. options: Option<MeltOptions>,
  1462. max_fee: Option<Amount>,
  1463. ) -> Result<Melted, Error> {
  1464. // Parse the invoice to get the amount
  1465. let invoice = bolt11
  1466. .parse::<crate::Bolt11Invoice>()
  1467. .map_err(Error::Invoice)?;
  1468. let amount = invoice
  1469. .amount_milli_satoshis()
  1470. .map(|msats| Amount::from(msats / 1000))
  1471. .ok_or(Error::InvoiceAmountUndefined)?;
  1472. let wallets = self.wallets.read().await;
  1473. let mut eligible_wallets = Vec::new();
  1474. for (mint_url, wallet) in wallets.iter() {
  1475. let balance = wallet.total_balance().await?;
  1476. if balance >= amount {
  1477. eligible_wallets.push((mint_url.clone(), wallet.clone()));
  1478. }
  1479. }
  1480. if eligible_wallets.is_empty() {
  1481. return Err(Error::InsufficientFunds);
  1482. }
  1483. // Try to get quotes from eligible wallets and select the best one
  1484. let mut best_quote = None;
  1485. let mut best_wallet = None;
  1486. for (_, wallet) in eligible_wallets.iter() {
  1487. match wallet.melt_quote(bolt11.to_string(), options).await {
  1488. Ok(quote) => {
  1489. if let Some(max_fee) = max_fee {
  1490. if quote.fee_reserve > max_fee {
  1491. continue;
  1492. }
  1493. }
  1494. if best_quote.is_none() {
  1495. best_quote = Some(quote);
  1496. best_wallet = Some(wallet.clone());
  1497. } else if let Some(ref existing_quote) = best_quote {
  1498. if quote.fee_reserve < existing_quote.fee_reserve {
  1499. best_quote = Some(quote);
  1500. best_wallet = Some(wallet.clone());
  1501. }
  1502. }
  1503. }
  1504. Err(_) => continue,
  1505. }
  1506. }
  1507. if let (Some(quote), Some(wallet)) = (best_quote, best_wallet) {
  1508. return wallet.melt(&quote.id).await;
  1509. }
  1510. Err(Error::InsufficientFunds)
  1511. }
  1512. /// Swap proofs with automatic wallet selection
  1513. #[instrument(skip(self))]
  1514. pub async fn swap(
  1515. &self,
  1516. amount: Option<Amount>,
  1517. conditions: Option<SpendingConditions>,
  1518. ) -> Result<Option<Proofs>, Error> {
  1519. // Find a wallet that has proofs
  1520. let wallets = self.wallets.read().await;
  1521. for (_, wallet) in wallets.iter() {
  1522. let balance = wallet.total_balance().await?;
  1523. if balance > Amount::ZERO {
  1524. // Try to swap with this wallet
  1525. let proofs = wallet.get_unspent_proofs().await?;
  1526. if !proofs.is_empty() {
  1527. return wallet
  1528. .swap(amount, SplitTarget::default(), proofs, conditions, false)
  1529. .await;
  1530. }
  1531. }
  1532. }
  1533. Err(Error::InsufficientFunds)
  1534. }
  1535. /// Consolidate proofs from multiple wallets into fewer, larger proofs
  1536. /// This can help reduce the number of proofs and optimize wallet performance
  1537. #[instrument(skip(self))]
  1538. pub async fn consolidate(&self) -> Result<Amount, Error> {
  1539. let mut total_consolidated = Amount::ZERO;
  1540. let wallets = self.wallets.read().await;
  1541. for (mint_url, wallet) in wallets.iter() {
  1542. // Get all unspent proofs for this wallet
  1543. let proofs = wallet.get_unspent_proofs().await?;
  1544. if proofs.len() > 1 {
  1545. // Consolidate by swapping all proofs for a single set
  1546. let proofs_amount = proofs.total_amount()?;
  1547. // Swap for optimized proof set
  1548. match wallet
  1549. .swap(
  1550. Some(proofs_amount),
  1551. SplitTarget::default(),
  1552. proofs,
  1553. None,
  1554. false,
  1555. )
  1556. .await
  1557. {
  1558. Ok(_) => {
  1559. total_consolidated += proofs_amount;
  1560. }
  1561. Err(e) => {
  1562. tracing::warn!(
  1563. "Failed to consolidate proofs for mint {:?}: {}",
  1564. mint_url,
  1565. e
  1566. );
  1567. }
  1568. }
  1569. }
  1570. }
  1571. Ok(total_consolidated)
  1572. }
  1573. /// Mint blind auth tokens for a specific mint
  1574. ///
  1575. /// This is a convenience method that calls the underlying wallet's mint_blind_auth.
  1576. #[cfg(feature = "auth")]
  1577. #[instrument(skip_all)]
  1578. pub async fn mint_blind_auth(
  1579. &self,
  1580. mint_url: &MintUrl,
  1581. amount: Amount,
  1582. ) -> Result<Proofs, Error> {
  1583. let wallets = self.wallets.read().await;
  1584. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1585. mint_url: mint_url.to_string(),
  1586. })?;
  1587. wallet.mint_blind_auth(amount).await
  1588. }
  1589. /// Get unspent auth proofs for a specific mint
  1590. ///
  1591. /// This is a convenience method that calls the underlying wallet's get_unspent_auth_proofs.
  1592. #[cfg(feature = "auth")]
  1593. #[instrument(skip_all)]
  1594. pub async fn get_unspent_auth_proofs(
  1595. &self,
  1596. mint_url: &MintUrl,
  1597. ) -> Result<Vec<cdk_common::AuthProof>, Error> {
  1598. let wallets = self.wallets.read().await;
  1599. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1600. mint_url: mint_url.to_string(),
  1601. })?;
  1602. wallet.get_unspent_auth_proofs().await
  1603. }
  1604. /// Set Clear Auth Token (CAT) for authentication at a specific mint
  1605. ///
  1606. /// This is a convenience method that calls the underlying wallet's set_cat.
  1607. #[cfg(feature = "auth")]
  1608. #[instrument(skip_all)]
  1609. pub async fn set_cat(&self, mint_url: &MintUrl, cat: String) -> Result<(), Error> {
  1610. let wallets = self.wallets.read().await;
  1611. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1612. mint_url: mint_url.to_string(),
  1613. })?;
  1614. wallet.set_cat(cat).await
  1615. }
  1616. /// Set refresh token for authentication at a specific mint
  1617. ///
  1618. /// This is a convenience method that calls the underlying wallet's set_refresh_token.
  1619. #[cfg(feature = "auth")]
  1620. #[instrument(skip_all)]
  1621. pub async fn set_refresh_token(
  1622. &self,
  1623. mint_url: &MintUrl,
  1624. refresh_token: String,
  1625. ) -> Result<(), Error> {
  1626. let wallets = self.wallets.read().await;
  1627. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1628. mint_url: mint_url.to_string(),
  1629. })?;
  1630. wallet.set_refresh_token(refresh_token).await
  1631. }
  1632. /// Refresh CAT token for a specific mint
  1633. ///
  1634. /// This is a convenience method that calls the underlying wallet's refresh_access_token.
  1635. #[cfg(feature = "auth")]
  1636. #[instrument(skip(self))]
  1637. pub async fn refresh_access_token(&self, mint_url: &MintUrl) -> Result<(), Error> {
  1638. let wallets = self.wallets.read().await;
  1639. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1640. mint_url: mint_url.to_string(),
  1641. })?;
  1642. wallet.refresh_access_token().await
  1643. }
  1644. /// Query mint for current mint information
  1645. ///
  1646. /// This is a convenience method that calls the underlying wallet's fetch_mint_info.
  1647. #[instrument(skip(self))]
  1648. pub async fn fetch_mint_info(
  1649. &self,
  1650. mint_url: &MintUrl,
  1651. ) -> Result<Option<crate::nuts::MintInfo>, Error> {
  1652. let wallets = self.wallets.read().await;
  1653. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1654. mint_url: mint_url.to_string(),
  1655. })?;
  1656. wallet.fetch_mint_info().await
  1657. }
  1658. /// Get mint info for all wallets
  1659. ///
  1660. /// This method loads the mint info for each wallet in the MultiMintWallet
  1661. /// and returns a map of mint URLs to their corresponding mint info.
  1662. ///
  1663. /// Uses cached mint info when available, only fetching from the mint if the cache
  1664. /// has expired.
  1665. #[instrument(skip(self))]
  1666. pub async fn get_all_mint_info(
  1667. &self,
  1668. ) -> Result<BTreeMap<MintUrl, crate::nuts::MintInfo>, Error> {
  1669. let mut mint_infos = BTreeMap::new();
  1670. for (mint_url, wallet) in self.wallets.read().await.iter() {
  1671. let mint_info = wallet.load_mint_info().await?;
  1672. mint_infos.insert(mint_url.clone(), mint_info);
  1673. }
  1674. Ok(mint_infos)
  1675. }
  1676. /// Melt Quote for BIP353 human-readable address
  1677. ///
  1678. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  1679. /// and then creates a melt quote for that offer at the specified mint.
  1680. ///
  1681. /// # Arguments
  1682. ///
  1683. /// * `mint_url` - The mint to use for creating the melt quote
  1684. /// * `bip353_address` - Human-readable address in the format "user@domain.com"
  1685. /// * `amount_msat` - Amount to pay in millisatoshis
  1686. ///
  1687. /// # Returns
  1688. ///
  1689. /// A `MeltQuote` that can be used to execute the payment
  1690. #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
  1691. #[instrument(skip(self, amount_msat))]
  1692. pub async fn melt_bip353_quote(
  1693. &self,
  1694. mint_url: &MintUrl,
  1695. bip353_address: &str,
  1696. amount_msat: impl Into<Amount>,
  1697. ) -> Result<crate::wallet::types::MeltQuote, Error> {
  1698. let wallets = self.wallets.read().await;
  1699. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1700. mint_url: mint_url.to_string(),
  1701. })?;
  1702. wallet.melt_bip353_quote(bip353_address, amount_msat).await
  1703. }
  1704. /// Melt Quote for Lightning address
  1705. ///
  1706. /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
  1707. /// and then creates a melt quote for that invoice at the specified mint.
  1708. ///
  1709. /// # Arguments
  1710. ///
  1711. /// * `mint_url` - The mint to use for creating the melt quote
  1712. /// * `lightning_address` - Lightning address in the format "user@domain.com"
  1713. /// * `amount_msat` - Amount to pay in millisatoshis
  1714. ///
  1715. /// # Returns
  1716. ///
  1717. /// A `MeltQuote` that can be used to execute the payment
  1718. #[instrument(skip(self, amount_msat))]
  1719. pub async fn melt_lightning_address_quote(
  1720. &self,
  1721. mint_url: &MintUrl,
  1722. lightning_address: &str,
  1723. amount_msat: impl Into<Amount>,
  1724. ) -> Result<crate::wallet::types::MeltQuote, Error> {
  1725. let wallets = self.wallets.read().await;
  1726. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1727. mint_url: mint_url.to_string(),
  1728. })?;
  1729. wallet
  1730. .melt_lightning_address_quote(lightning_address, amount_msat)
  1731. .await
  1732. }
  1733. /// Get a melt quote for a human-readable address
  1734. ///
  1735. /// This method accepts a human-readable address that could be either a BIP353 address
  1736. /// or a Lightning address. It intelligently determines which to try based on mint support:
  1737. ///
  1738. /// 1. If the mint supports Bolt12, it tries BIP353 first
  1739. /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
  1740. /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
  1741. /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
  1742. ///
  1743. /// # Arguments
  1744. ///
  1745. /// * `mint_url` - The mint to use for creating the melt quote
  1746. /// * `address` - Human-readable address (BIP353 or Lightning address)
  1747. /// * `amount_msat` - Amount to pay in millisatoshis
  1748. #[cfg(all(feature = "bip353", feature = "wallet", not(target_arch = "wasm32")))]
  1749. #[instrument(skip(self, amount_msat))]
  1750. pub async fn melt_human_readable_quote(
  1751. &self,
  1752. mint_url: &MintUrl,
  1753. address: &str,
  1754. amount_msat: impl Into<Amount>,
  1755. ) -> Result<crate::wallet::types::MeltQuote, Error> {
  1756. let wallets = self.wallets.read().await;
  1757. let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
  1758. mint_url: mint_url.to_string(),
  1759. })?;
  1760. wallet.melt_human_readable_quote(address, amount_msat).await
  1761. }
  1762. }
  1763. impl Drop for MultiMintWallet {
  1764. fn drop(&mut self) {
  1765. self.seed.zeroize();
  1766. }
  1767. }
  1768. /// Multi-Mint Receive Options
  1769. ///
  1770. /// Controls how tokens are received, especially from untrusted mints
  1771. #[derive(Debug, Clone, Default)]
  1772. pub struct MultiMintReceiveOptions {
  1773. /// Whether to allow receiving from untrusted (not yet added) mints
  1774. pub allow_untrusted: bool,
  1775. /// Mint to transfer tokens to from untrusted mints (None means keep in original mint)
  1776. pub transfer_to_mint: Option<MintUrl>,
  1777. /// Base receive options to apply to the wallet receive
  1778. pub receive_options: ReceiveOptions,
  1779. }
  1780. impl MultiMintReceiveOptions {
  1781. /// Create new default options
  1782. pub fn new() -> Self {
  1783. Default::default()
  1784. }
  1785. /// Allow receiving from untrusted mints
  1786. pub fn allow_untrusted(mut self, allow: bool) -> Self {
  1787. self.allow_untrusted = allow;
  1788. self
  1789. }
  1790. /// Set mint to transfer tokens to from untrusted mints
  1791. pub fn transfer_to_mint(mut self, mint_url: Option<MintUrl>) -> Self {
  1792. self.transfer_to_mint = mint_url;
  1793. self
  1794. }
  1795. /// Set the base receive options for the wallet operation
  1796. pub fn receive_options(mut self, options: ReceiveOptions) -> Self {
  1797. self.receive_options = options;
  1798. self
  1799. }
  1800. }
  1801. /// Multi-Mint Send Options
  1802. ///
  1803. /// Controls transfer behavior when the target mint doesn't have sufficient balance
  1804. #[derive(Debug, Clone, Default)]
  1805. pub struct MultiMintSendOptions {
  1806. /// Whether to allow transferring funds from other mints to the sending mint
  1807. /// if the sending mint doesn't have sufficient balance
  1808. pub allow_transfer: bool,
  1809. /// Maximum amount to transfer from other mints (optional limit)
  1810. pub max_transfer_amount: Option<Amount>,
  1811. /// Specific mints allowed for transfers (empty means all mints allowed)
  1812. pub allowed_mints: Vec<MintUrl>,
  1813. /// Specific mints to exclude from transfers
  1814. pub excluded_mints: Vec<MintUrl>,
  1815. /// Base send options to apply to the wallet send
  1816. pub send_options: SendOptions,
  1817. }
  1818. impl MultiMintSendOptions {
  1819. /// Create new default options
  1820. pub fn new() -> Self {
  1821. Default::default()
  1822. }
  1823. /// Enable transferring funds from other mints if needed
  1824. pub fn allow_transfer(mut self, allow: bool) -> Self {
  1825. self.allow_transfer = allow;
  1826. self
  1827. }
  1828. /// Set maximum amount to transfer from other mints
  1829. pub fn max_transfer_amount(mut self, amount: Amount) -> Self {
  1830. self.max_transfer_amount = Some(amount);
  1831. self
  1832. }
  1833. /// Add a mint to the allowed list for transfers
  1834. pub fn allow_mint(mut self, mint_url: MintUrl) -> Self {
  1835. self.allowed_mints.push(mint_url);
  1836. self
  1837. }
  1838. /// Set all allowed mints for transfers
  1839. pub fn allowed_mints(mut self, mints: Vec<MintUrl>) -> Self {
  1840. self.allowed_mints = mints;
  1841. self
  1842. }
  1843. /// Add a mint to exclude from transfers
  1844. pub fn exclude_mint(mut self, mint_url: MintUrl) -> Self {
  1845. self.excluded_mints.push(mint_url);
  1846. self
  1847. }
  1848. /// Set all excluded mints for transfers
  1849. pub fn excluded_mints(mut self, mints: Vec<MintUrl>) -> Self {
  1850. self.excluded_mints = mints;
  1851. self
  1852. }
  1853. /// Set the base send options for the wallet operation
  1854. pub fn send_options(mut self, options: SendOptions) -> Self {
  1855. self.send_options = options;
  1856. self
  1857. }
  1858. }
  1859. #[cfg(test)]
  1860. mod tests {
  1861. use std::sync::Arc;
  1862. use cdk_common::database::WalletDatabase;
  1863. use super::*;
  1864. async fn create_test_multi_wallet() -> MultiMintWallet {
  1865. let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
  1866. cdk_sqlite::wallet::memory::empty()
  1867. .await
  1868. .expect("Failed to create in-memory database"),
  1869. );
  1870. let seed = [0u8; 64];
  1871. MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
  1872. .await
  1873. .expect("Failed to create MultiMintWallet")
  1874. }
  1875. #[tokio::test]
  1876. async fn test_total_balance_empty() {
  1877. let multi_wallet = create_test_multi_wallet().await;
  1878. let balance = multi_wallet.total_balance().await.unwrap();
  1879. assert_eq!(balance, Amount::ZERO);
  1880. }
  1881. #[tokio::test]
  1882. async fn test_prepare_send_insufficient_funds() {
  1883. use std::str::FromStr;
  1884. let multi_wallet = create_test_multi_wallet().await;
  1885. let mint_url = MintUrl::from_str("https://mint1.example.com").unwrap();
  1886. let options = MultiMintSendOptions::new();
  1887. let result = multi_wallet
  1888. .prepare_send(mint_url, Amount::from(1000), options)
  1889. .await;
  1890. assert!(result.is_err());
  1891. }
  1892. #[tokio::test]
  1893. async fn test_consolidate_empty() {
  1894. let multi_wallet = create_test_multi_wallet().await;
  1895. let result = multi_wallet.consolidate().await.unwrap();
  1896. assert_eq!(result, Amount::ZERO);
  1897. }
  1898. #[tokio::test]
  1899. async fn test_multi_mint_wallet_creation() {
  1900. let multi_wallet = create_test_multi_wallet().await;
  1901. assert!(multi_wallet.wallets.try_read().is_ok());
  1902. }
  1903. #[tokio::test]
  1904. async fn test_multi_mint_send_options() {
  1905. use std::str::FromStr;
  1906. let mint1 = MintUrl::from_str("https://mint1.example.com").unwrap();
  1907. let mint2 = MintUrl::from_str("https://mint2.example.com").unwrap();
  1908. let mint3 = MintUrl::from_str("https://mint3.example.com").unwrap();
  1909. let options = MultiMintSendOptions::new()
  1910. .allow_transfer(true)
  1911. .max_transfer_amount(Amount::from(500))
  1912. .allow_mint(mint1.clone())
  1913. .allow_mint(mint2.clone())
  1914. .exclude_mint(mint3.clone())
  1915. .send_options(SendOptions::default());
  1916. assert!(options.allow_transfer);
  1917. assert_eq!(options.max_transfer_amount, Some(Amount::from(500)));
  1918. assert_eq!(options.allowed_mints, vec![mint1, mint2]);
  1919. assert_eq!(options.excluded_mints, vec![mint3]);
  1920. }
  1921. #[tokio::test]
  1922. async fn test_get_mint_keysets_unknown_mint() {
  1923. use std::str::FromStr;
  1924. let multi_wallet = create_test_multi_wallet().await;
  1925. let mint_url = MintUrl::from_str("https://unknown-mint.example.com").unwrap();
  1926. // Should error when trying to get keysets for a mint that hasn't been added
  1927. let result = multi_wallet.get_mint_keysets(&mint_url).await;
  1928. assert!(result.is_err());
  1929. match result {
  1930. Err(Error::UnknownMint { mint_url: url }) => {
  1931. assert!(url.contains("unknown-mint.example.com"));
  1932. }
  1933. _ => panic!("Expected UnknownMint error"),
  1934. }
  1935. }
  1936. #[tokio::test]
  1937. async fn test_multi_mint_receive_options() {
  1938. use std::str::FromStr;
  1939. let mint_url = MintUrl::from_str("https://trusted.mint.example.com").unwrap();
  1940. // Test default options
  1941. let default_opts = MultiMintReceiveOptions::default();
  1942. assert!(!default_opts.allow_untrusted);
  1943. assert!(default_opts.transfer_to_mint.is_none());
  1944. // Test builder pattern
  1945. let opts = MultiMintReceiveOptions::new()
  1946. .allow_untrusted(true)
  1947. .transfer_to_mint(Some(mint_url.clone()));
  1948. assert!(opts.allow_untrusted);
  1949. assert_eq!(opts.transfer_to_mint, Some(mint_url));
  1950. }
  1951. #[tokio::test]
  1952. async fn test_get_token_data_unknown_mint() {
  1953. use std::str::FromStr;
  1954. let multi_wallet = create_test_multi_wallet().await;
  1955. // Create a token from a mint that isn't in the wallet
  1956. // This is a valid token structure pointing to an unknown mint
  1957. let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
  1958. let token = Token::from_str(token_str).unwrap();
  1959. // Should error because the mint (localhost:3338) hasn't been added
  1960. let result = multi_wallet.get_token_data(&token).await;
  1961. assert!(result.is_err());
  1962. match result {
  1963. Err(Error::UnknownMint { mint_url }) => {
  1964. assert!(mint_url.contains("localhost:3338"));
  1965. }
  1966. _ => panic!("Expected UnknownMint error"),
  1967. }
  1968. }
  1969. #[test]
  1970. fn test_token_data_struct() {
  1971. use std::str::FromStr;
  1972. let mint_url = MintUrl::from_str("https://example.mint.com").unwrap();
  1973. let proofs = vec![];
  1974. let memo = Some("Test memo".to_string());
  1975. let token_data = TokenData {
  1976. value: Amount::ZERO,
  1977. mint_url: mint_url.clone(),
  1978. proofs: proofs.clone(),
  1979. memo: memo.clone(),
  1980. unit: CurrencyUnit::Sat,
  1981. redeem_fee: None,
  1982. };
  1983. assert_eq!(token_data.mint_url, mint_url);
  1984. assert_eq!(token_data.proofs.len(), 0);
  1985. assert_eq!(token_data.memo, memo);
  1986. // Test with no memo
  1987. let token_data_no_memo = TokenData {
  1988. value: Amount::ZERO,
  1989. mint_url: mint_url.clone(),
  1990. proofs: vec![],
  1991. memo: None,
  1992. unit: CurrencyUnit::Sat,
  1993. redeem_fee: None,
  1994. };
  1995. assert!(token_data_no_memo.memo.is_none());
  1996. }
  1997. }