multi_mint_wallet.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. //! FFI MultiMintWallet bindings
  2. use std::collections::HashMap;
  3. use std::str::FromStr;
  4. use std::sync::Arc;
  5. use bip39::Mnemonic;
  6. use cdk::wallet::multi_mint_wallet::{
  7. MultiMintReceiveOptions as CdkMultiMintReceiveOptions,
  8. MultiMintSendOptions as CdkMultiMintSendOptions, MultiMintWallet as CdkMultiMintWallet,
  9. TransferMode as CdkTransferMode, TransferResult as CdkTransferResult,
  10. };
  11. use crate::error::FfiError;
  12. use crate::token::Token;
  13. use crate::types::*;
  14. /// FFI-compatible MultiMintWallet
  15. #[derive(uniffi::Object)]
  16. pub struct MultiMintWallet {
  17. inner: Arc<CdkMultiMintWallet>,
  18. }
  19. #[uniffi::export(async_runtime = "tokio")]
  20. impl MultiMintWallet {
  21. /// Create a new MultiMintWallet from mnemonic using WalletDatabase trait
  22. #[uniffi::constructor]
  23. pub fn new(
  24. unit: CurrencyUnit,
  25. mnemonic: String,
  26. db: Arc<dyn crate::database::WalletDatabase>,
  27. ) -> Result<Self, FfiError> {
  28. // Parse mnemonic and generate seed without passphrase
  29. let m = Mnemonic::parse(&mnemonic)
  30. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  31. let seed = m.to_seed_normalized("");
  32. // Convert the FFI database trait to a CDK database implementation
  33. let localstore = crate::database::create_cdk_database_from_ffi(db);
  34. let wallet = match tokio::runtime::Handle::try_current() {
  35. Ok(handle) => tokio::task::block_in_place(|| {
  36. handle.block_on(async move {
  37. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  38. })
  39. }),
  40. Err(_) => {
  41. // No current runtime, create a new one
  42. tokio::runtime::Runtime::new()
  43. .map_err(|e| FfiError::Database {
  44. msg: format!("Failed to create runtime: {}", e),
  45. })?
  46. .block_on(async move {
  47. CdkMultiMintWallet::new(localstore, seed, unit.into()).await
  48. })
  49. }
  50. }?;
  51. Ok(Self {
  52. inner: Arc::new(wallet),
  53. })
  54. }
  55. /// Create a new MultiMintWallet with proxy configuration
  56. #[uniffi::constructor]
  57. pub fn new_with_proxy(
  58. unit: CurrencyUnit,
  59. mnemonic: String,
  60. db: Arc<dyn crate::database::WalletDatabase>,
  61. proxy_url: String,
  62. ) -> Result<Self, FfiError> {
  63. // Parse mnemonic and generate seed without passphrase
  64. let m = Mnemonic::parse(&mnemonic)
  65. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  66. let seed = m.to_seed_normalized("");
  67. // Convert the FFI database trait to a CDK database implementation
  68. let localstore = crate::database::create_cdk_database_from_ffi(db);
  69. // Parse proxy URL
  70. let proxy_url =
  71. url::Url::parse(&proxy_url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;
  72. let wallet = match tokio::runtime::Handle::try_current() {
  73. Ok(handle) => tokio::task::block_in_place(|| {
  74. handle.block_on(async move {
  75. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  76. .await
  77. })
  78. }),
  79. Err(_) => {
  80. // No current runtime, create a new one
  81. tokio::runtime::Runtime::new()
  82. .map_err(|e| FfiError::Database {
  83. msg: format!("Failed to create runtime: {}", e),
  84. })?
  85. .block_on(async move {
  86. CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
  87. .await
  88. })
  89. }
  90. }?;
  91. Ok(Self {
  92. inner: Arc::new(wallet),
  93. })
  94. }
  95. /// Get the currency unit for this wallet
  96. pub fn unit(&self) -> CurrencyUnit {
  97. self.inner.unit().clone().into()
  98. }
  99. /// Add a mint to this MultiMintWallet
  100. pub async fn add_mint(
  101. &self,
  102. mint_url: MintUrl,
  103. target_proof_count: Option<u32>,
  104. ) -> Result<(), FfiError> {
  105. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  106. self.inner
  107. .add_mint(cdk_mint_url, target_proof_count.map(|c| c as usize))
  108. .await?;
  109. Ok(())
  110. }
  111. /// Remove mint from MultiMintWallet
  112. pub async fn remove_mint(&self, mint_url: MintUrl) {
  113. let url_str = mint_url.url.clone();
  114. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().unwrap_or_else(|_| {
  115. // If conversion fails, we can't remove the mint, but we shouldn't panic
  116. // This is a best-effort operation
  117. cdk::mint_url::MintUrl::from_str(&url_str).unwrap_or_else(|_| {
  118. // Last resort: create a dummy URL that won't match anything
  119. cdk::mint_url::MintUrl::from_str("https://invalid.mint").unwrap()
  120. })
  121. });
  122. self.inner.remove_mint(&cdk_mint_url).await;
  123. }
  124. /// Check if mint is in wallet
  125. pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
  126. if let Ok(cdk_mint_url) = mint_url.try_into() {
  127. self.inner.has_mint(&cdk_mint_url).await
  128. } else {
  129. false
  130. }
  131. }
  132. /// Get wallet balances for all mints
  133. pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
  134. let balances = self.inner.get_balances().await?;
  135. let mut balance_map = HashMap::new();
  136. for (mint_url, amount) in balances {
  137. balance_map.insert(mint_url.to_string(), amount.into());
  138. }
  139. Ok(balance_map)
  140. }
  141. /// Get total balance across all mints
  142. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  143. let total = self.inner.total_balance().await?;
  144. Ok(total.into())
  145. }
  146. /// List proofs for all mints
  147. pub async fn list_proofs(&self) -> Result<ProofsByMint, FfiError> {
  148. let proofs = self.inner.list_proofs().await?;
  149. let mut proofs_by_mint = HashMap::new();
  150. for (mint_url, mint_proofs) in proofs {
  151. let ffi_proofs: Vec<Arc<Proof>> = mint_proofs
  152. .into_iter()
  153. .map(|p| Arc::new(p.into()))
  154. .collect();
  155. proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
  156. }
  157. Ok(proofs_by_mint)
  158. }
  159. /// Receive token
  160. pub async fn receive(
  161. &self,
  162. token: Arc<Token>,
  163. options: MultiMintReceiveOptions,
  164. ) -> Result<Amount, FfiError> {
  165. let amount = self
  166. .inner
  167. .receive(&token.to_string(), options.into())
  168. .await?;
  169. Ok(amount.into())
  170. }
  171. /// Restore wallets for a specific mint
  172. pub async fn restore(&self, mint_url: MintUrl) -> Result<Amount, FfiError> {
  173. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  174. let amount = self.inner.restore(&cdk_mint_url).await?;
  175. Ok(amount.into())
  176. }
  177. /// Prepare a send operation from a specific mint
  178. pub async fn prepare_send(
  179. &self,
  180. mint_url: MintUrl,
  181. amount: Amount,
  182. options: MultiMintSendOptions,
  183. ) -> Result<Arc<PreparedSend>, FfiError> {
  184. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  185. let prepared = self
  186. .inner
  187. .prepare_send(cdk_mint_url, amount.into(), options.into())
  188. .await?;
  189. Ok(Arc::new(prepared.into()))
  190. }
  191. /// Get a mint quote from a specific mint
  192. pub async fn mint_quote(
  193. &self,
  194. mint_url: MintUrl,
  195. amount: Amount,
  196. description: Option<String>,
  197. ) -> Result<MintQuote, FfiError> {
  198. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  199. let quote = self
  200. .inner
  201. .mint_quote(&cdk_mint_url, amount.into(), description)
  202. .await?;
  203. Ok(quote.into())
  204. }
  205. /// Mint tokens at a specific mint
  206. pub async fn mint(
  207. &self,
  208. mint_url: MintUrl,
  209. quote_id: String,
  210. spending_conditions: Option<SpendingConditions>,
  211. ) -> Result<Proofs, FfiError> {
  212. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  213. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  214. let proofs = self
  215. .inner
  216. .mint(&cdk_mint_url, &quote_id, conditions)
  217. .await?;
  218. Ok(proofs.into_iter().map(|p| Arc::new(p.into())).collect())
  219. }
  220. /// Get a melt quote from a specific mint
  221. pub async fn melt_quote(
  222. &self,
  223. mint_url: MintUrl,
  224. request: String,
  225. options: Option<MeltOptions>,
  226. ) -> Result<MeltQuote, FfiError> {
  227. let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
  228. let cdk_options = options.map(Into::into);
  229. let quote = self
  230. .inner
  231. .melt_quote(&cdk_mint_url, request, cdk_options)
  232. .await?;
  233. Ok(quote.into())
  234. }
  235. /// Melt tokens (pay a bolt11 invoice)
  236. pub async fn melt(
  237. &self,
  238. bolt11: String,
  239. options: Option<MeltOptions>,
  240. max_fee: Option<Amount>,
  241. ) -> Result<Melted, FfiError> {
  242. let cdk_options = options.map(Into::into);
  243. let cdk_max_fee = max_fee.map(Into::into);
  244. let melted = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
  245. Ok(melted.into())
  246. }
  247. /// Transfer funds between mints
  248. pub async fn transfer(
  249. &self,
  250. source_mint: MintUrl,
  251. target_mint: MintUrl,
  252. transfer_mode: TransferMode,
  253. ) -> Result<TransferResult, FfiError> {
  254. let source_cdk: cdk::mint_url::MintUrl = source_mint.try_into()?;
  255. let target_cdk: cdk::mint_url::MintUrl = target_mint.try_into()?;
  256. let result = self
  257. .inner
  258. .transfer(&source_cdk, &target_cdk, transfer_mode.into())
  259. .await?;
  260. Ok(result.into())
  261. }
  262. /// Swap proofs with automatic wallet selection
  263. pub async fn swap(
  264. &self,
  265. amount: Option<Amount>,
  266. spending_conditions: Option<SpendingConditions>,
  267. ) -> Result<Option<Proofs>, FfiError> {
  268. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  269. let result = self.inner.swap(amount.map(Into::into), conditions).await?;
  270. Ok(result.map(|proofs| proofs.into_iter().map(|p| Arc::new(p.into())).collect()))
  271. }
  272. /// List transactions from all mints
  273. pub async fn list_transactions(
  274. &self,
  275. direction: Option<TransactionDirection>,
  276. ) -> Result<Vec<Transaction>, FfiError> {
  277. let cdk_direction = direction.map(Into::into);
  278. let transactions = self.inner.list_transactions(cdk_direction).await?;
  279. Ok(transactions.into_iter().map(Into::into).collect())
  280. }
  281. /// Check all mint quotes and mint if paid
  282. pub async fn check_all_mint_quotes(
  283. &self,
  284. mint_url: Option<MintUrl>,
  285. ) -> Result<Amount, FfiError> {
  286. let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
  287. let amount = self.inner.check_all_mint_quotes(cdk_mint_url).await?;
  288. Ok(amount.into())
  289. }
  290. /// Consolidate proofs across all mints
  291. pub async fn consolidate(&self) -> Result<Amount, FfiError> {
  292. let amount = self.inner.consolidate().await?;
  293. Ok(amount.into())
  294. }
  295. /// Get list of mint URLs
  296. pub async fn get_mint_urls(&self) -> Vec<String> {
  297. let wallets = self.inner.get_wallets().await;
  298. wallets.iter().map(|w| w.mint_url.to_string()).collect()
  299. }
  300. /// Verify token DLEQ proofs
  301. pub async fn verify_token_dleq(&self, token: Arc<Token>) -> Result<(), FfiError> {
  302. let cdk_token = token.inner.clone();
  303. self.inner.verify_token_dleq(&cdk_token).await?;
  304. Ok(())
  305. }
  306. }
  307. /// Transfer mode for mint-to-mint transfers
  308. #[derive(Debug, Clone, uniffi::Enum)]
  309. pub enum TransferMode {
  310. /// Transfer exact amount to target (target receives specified amount)
  311. ExactReceive { amount: Amount },
  312. /// Transfer all available balance (source will be emptied)
  313. FullBalance,
  314. }
  315. impl From<TransferMode> for CdkTransferMode {
  316. fn from(mode: TransferMode) -> Self {
  317. match mode {
  318. TransferMode::ExactReceive { amount } => CdkTransferMode::ExactReceive(amount.into()),
  319. TransferMode::FullBalance => CdkTransferMode::FullBalance,
  320. }
  321. }
  322. }
  323. /// Result of a transfer operation with detailed breakdown
  324. #[derive(Debug, Clone, uniffi::Record)]
  325. pub struct TransferResult {
  326. /// Amount deducted from source mint
  327. pub amount_sent: Amount,
  328. /// Amount received at target mint
  329. pub amount_received: Amount,
  330. /// Total fees paid for the transfer
  331. pub fees_paid: Amount,
  332. /// Remaining balance in source mint after transfer
  333. pub source_balance_after: Amount,
  334. /// New balance in target mint after transfer
  335. pub target_balance_after: Amount,
  336. }
  337. impl From<CdkTransferResult> for TransferResult {
  338. fn from(result: CdkTransferResult) -> Self {
  339. Self {
  340. amount_sent: result.amount_sent.into(),
  341. amount_received: result.amount_received.into(),
  342. fees_paid: result.fees_paid.into(),
  343. source_balance_after: result.source_balance_after.into(),
  344. target_balance_after: result.target_balance_after.into(),
  345. }
  346. }
  347. }
  348. /// Options for receiving tokens in multi-mint context
  349. #[derive(Debug, Clone, Default, uniffi::Record)]
  350. pub struct MultiMintReceiveOptions {
  351. /// Whether to allow receiving from untrusted (not yet added) mints
  352. pub allow_untrusted: bool,
  353. /// Mint URL to transfer tokens to from untrusted mints (None means keep in original mint)
  354. pub transfer_to_mint: Option<MintUrl>,
  355. /// Base receive options to apply to the wallet receive
  356. pub receive_options: ReceiveOptions,
  357. }
  358. impl From<MultiMintReceiveOptions> for CdkMultiMintReceiveOptions {
  359. fn from(options: MultiMintReceiveOptions) -> Self {
  360. let mut opts = CdkMultiMintReceiveOptions::new();
  361. opts.allow_untrusted = options.allow_untrusted;
  362. opts.transfer_to_mint = options.transfer_to_mint.and_then(|url| url.try_into().ok());
  363. opts.receive_options = options.receive_options.into();
  364. opts
  365. }
  366. }
  367. /// Options for sending tokens in multi-mint context
  368. #[derive(Debug, Clone, Default, uniffi::Record)]
  369. pub struct MultiMintSendOptions {
  370. /// Whether to allow transferring funds from other mints if needed
  371. pub allow_transfer: bool,
  372. /// Maximum amount to transfer from other mints (optional limit)
  373. pub max_transfer_amount: Option<Amount>,
  374. /// Specific mint URLs allowed for transfers (empty means all mints allowed)
  375. pub allowed_mints: Vec<MintUrl>,
  376. /// Specific mint URLs to exclude from transfers
  377. pub excluded_mints: Vec<MintUrl>,
  378. /// Base send options to apply to the wallet send
  379. pub send_options: SendOptions,
  380. }
  381. impl From<MultiMintSendOptions> for CdkMultiMintSendOptions {
  382. fn from(options: MultiMintSendOptions) -> Self {
  383. let mut opts = CdkMultiMintSendOptions::new();
  384. opts.allow_transfer = options.allow_transfer;
  385. opts.max_transfer_amount = options.max_transfer_amount.map(Into::into);
  386. opts.allowed_mints = options
  387. .allowed_mints
  388. .into_iter()
  389. .filter_map(|url| url.try_into().ok())
  390. .collect();
  391. opts.excluded_mints = options
  392. .excluded_mints
  393. .into_iter()
  394. .filter_map(|url| url.try_into().ok())
  395. .collect();
  396. opts.send_options = options.send_options.into();
  397. opts
  398. }
  399. }
  400. /// Type alias for balances by mint URL
  401. pub type BalanceMap = HashMap<String, Amount>;
  402. /// Type alias for proofs by mint URL
  403. pub type ProofsByMint = HashMap<String, Vec<Arc<Proof>>>;