wallet.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. //! FFI Wallet bindings
  2. use std::str::FromStr;
  3. use std::sync::Arc;
  4. use bip39::Mnemonic;
  5. use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
  6. use crate::error::FfiError;
  7. use crate::token::Token;
  8. use crate::types::*;
  9. /// FFI-compatible Wallet
  10. #[derive(uniffi::Object)]
  11. pub struct Wallet {
  12. inner: Arc<CdkWallet>,
  13. }
  14. impl Wallet {
  15. /// Create a Wallet from an existing CDK wallet (internal use only)
  16. pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
  17. Self { inner }
  18. }
  19. }
  20. #[uniffi::export(async_runtime = "tokio")]
  21. impl Wallet {
  22. /// Create a new Wallet from mnemonic using WalletDatabaseFfi trait
  23. #[uniffi::constructor]
  24. pub fn new(
  25. mint_url: String,
  26. unit: CurrencyUnit,
  27. mnemonic: String,
  28. db: Arc<dyn crate::database::WalletDatabase>,
  29. config: WalletConfig,
  30. ) -> Result<Self, FfiError> {
  31. // Parse mnemonic and generate seed without passphrase
  32. let m = Mnemonic::parse(&mnemonic)
  33. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  34. let seed = m.to_seed_normalized("");
  35. // Convert the FFI database trait to a CDK database implementation
  36. let localstore = crate::database::create_cdk_database_from_ffi(db);
  37. let wallet =
  38. CdkWalletBuilder::new()
  39. .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
  40. FfiError::InvalidUrl { msg: e.to_string() }
  41. })?)
  42. .unit(unit.into())
  43. .localstore(localstore)
  44. .seed(seed)
  45. .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
  46. .build()
  47. .map_err(FfiError::from)?;
  48. Ok(Self {
  49. inner: Arc::new(wallet),
  50. })
  51. }
  52. /// Get the mint URL
  53. pub fn mint_url(&self) -> MintUrl {
  54. self.inner.mint_url.clone().into()
  55. }
  56. /// Get the currency unit
  57. pub fn unit(&self) -> CurrencyUnit {
  58. self.inner.unit.clone().into()
  59. }
  60. /// Set metadata cache TTL (time-to-live) in seconds
  61. ///
  62. /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
  63. /// before requiring a refresh from the mint server.
  64. ///
  65. /// # Arguments
  66. ///
  67. /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used.
  68. ///
  69. /// # Example
  70. ///
  71. /// ```ignore
  72. /// // Cache expires after 5 minutes
  73. /// wallet.set_metadata_cache_ttl(Some(300));
  74. ///
  75. /// // Cache never expires (default)
  76. /// wallet.set_metadata_cache_ttl(None);
  77. /// ```
  78. pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
  79. let ttl = ttl_secs.map(std::time::Duration::from_secs);
  80. self.inner.set_metadata_cache_ttl(ttl);
  81. }
  82. /// Get total balance
  83. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  84. let balance = self.inner.total_balance().await?;
  85. Ok(balance.into())
  86. }
  87. /// Get total pending balance
  88. pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
  89. let balance = self.inner.total_pending_balance().await?;
  90. Ok(balance.into())
  91. }
  92. /// Get total reserved balance
  93. pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
  94. let balance = self.inner.total_reserved_balance().await?;
  95. Ok(balance.into())
  96. }
  97. /// Get mint info
  98. pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
  99. let info = self.inner.fetch_mint_info().await?;
  100. Ok(info.map(Into::into))
  101. }
  102. /// Load mint info
  103. ///
  104. /// This will get mint info from cache if it is fresh
  105. pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
  106. let info = self.inner.load_mint_info().await?;
  107. Ok(info.into())
  108. }
  109. /// Receive tokens
  110. pub async fn receive(
  111. &self,
  112. token: std::sync::Arc<Token>,
  113. options: ReceiveOptions,
  114. ) -> Result<Amount, FfiError> {
  115. let amount = self
  116. .inner
  117. .receive(&token.to_string(), options.into())
  118. .await?;
  119. Ok(amount.into())
  120. }
  121. /// Restore wallet from seed
  122. pub async fn restore(&self) -> Result<Amount, FfiError> {
  123. let amount = self.inner.restore().await?;
  124. Ok(amount.into())
  125. }
  126. /// Verify token DLEQ proofs
  127. pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
  128. let cdk_token = token.inner.clone();
  129. self.inner.verify_token_dleq(&cdk_token).await?;
  130. Ok(())
  131. }
  132. /// Receive proofs directly
  133. pub async fn receive_proofs(
  134. &self,
  135. proofs: Proofs,
  136. options: ReceiveOptions,
  137. memo: Option<String>,
  138. ) -> Result<Amount, FfiError> {
  139. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  140. proofs.into_iter().map(|p| p.try_into()).collect();
  141. let cdk_proofs = cdk_proofs?;
  142. let amount = self
  143. .inner
  144. .receive_proofs(cdk_proofs, options.into(), memo)
  145. .await?;
  146. Ok(amount.into())
  147. }
  148. /// Prepare a send operation
  149. pub async fn prepare_send(
  150. &self,
  151. amount: Amount,
  152. options: SendOptions,
  153. ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
  154. let prepared = self
  155. .inner
  156. .prepare_send(amount.into(), options.into())
  157. .await?;
  158. Ok(std::sync::Arc::new(prepared.into()))
  159. }
  160. /// Get a mint quote
  161. pub async fn mint_quote(
  162. &self,
  163. amount: Amount,
  164. description: Option<String>,
  165. ) -> Result<MintQuote, FfiError> {
  166. let quote = self.inner.mint_quote(amount.into(), description).await?;
  167. Ok(quote.into())
  168. }
  169. /// Mint tokens
  170. pub async fn mint(
  171. &self,
  172. quote_id: String,
  173. amount_split_target: SplitTarget,
  174. spending_conditions: Option<SpendingConditions>,
  175. ) -> Result<Proofs, FfiError> {
  176. // Convert spending conditions if provided
  177. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  178. let proofs = self
  179. .inner
  180. .mint(&quote_id, amount_split_target.into(), conditions)
  181. .await?;
  182. Ok(proofs.into_iter().map(|p| p.into()).collect())
  183. }
  184. /// Get a melt quote
  185. pub async fn melt_quote(
  186. &self,
  187. request: String,
  188. options: Option<MeltOptions>,
  189. ) -> Result<MeltQuote, FfiError> {
  190. let cdk_options = options.map(Into::into);
  191. let quote = self.inner.melt_quote(request, cdk_options).await?;
  192. Ok(quote.into())
  193. }
  194. /// Melt tokens
  195. pub async fn melt(&self, quote_id: String) -> Result<Melted, FfiError> {
  196. let melted = self.inner.melt(&quote_id).await?;
  197. Ok(melted.into())
  198. }
  199. /// Melt specific proofs
  200. ///
  201. /// This method allows melting proofs that may not be in the wallet's database,
  202. /// similar to how `receive_proofs` handles external proofs. The proofs will be
  203. /// added to the database and used for the melt operation.
  204. ///
  205. /// # Arguments
  206. ///
  207. /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
  208. /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
  209. ///
  210. /// # Returns
  211. ///
  212. /// A `Melted` result containing the payment details and any change proofs
  213. pub async fn melt_proofs(&self, quote_id: String, proofs: Proofs) -> Result<Melted, FfiError> {
  214. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  215. proofs.into_iter().map(|p| p.try_into()).collect();
  216. let cdk_proofs = cdk_proofs?;
  217. let melted = self.inner.melt_proofs(&quote_id, cdk_proofs).await?;
  218. Ok(melted.into())
  219. }
  220. /// Get a quote for a bolt12 mint
  221. pub async fn mint_bolt12_quote(
  222. &self,
  223. amount: Option<Amount>,
  224. description: Option<String>,
  225. ) -> Result<MintQuote, FfiError> {
  226. let quote = self
  227. .inner
  228. .mint_bolt12_quote(amount.map(Into::into), description)
  229. .await?;
  230. Ok(quote.into())
  231. }
  232. /// Mint tokens using bolt12
  233. pub async fn mint_bolt12(
  234. &self,
  235. quote_id: String,
  236. amount: Option<Amount>,
  237. amount_split_target: SplitTarget,
  238. spending_conditions: Option<SpendingConditions>,
  239. ) -> Result<Proofs, FfiError> {
  240. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  241. let proofs = self
  242. .inner
  243. .mint_bolt12(
  244. &quote_id,
  245. amount.map(Into::into),
  246. amount_split_target.into(),
  247. conditions,
  248. )
  249. .await?;
  250. Ok(proofs.into_iter().map(|p| p.into()).collect())
  251. }
  252. /// Get a quote for a bolt12 melt
  253. pub async fn melt_bolt12_quote(
  254. &self,
  255. request: String,
  256. options: Option<MeltOptions>,
  257. ) -> Result<MeltQuote, FfiError> {
  258. let cdk_options = options.map(Into::into);
  259. let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?;
  260. Ok(quote.into())
  261. }
  262. /// Swap proofs
  263. pub async fn swap(
  264. &self,
  265. amount: Option<Amount>,
  266. amount_split_target: SplitTarget,
  267. input_proofs: Proofs,
  268. spending_conditions: Option<SpendingConditions>,
  269. include_fees: bool,
  270. ) -> Result<Option<Proofs>, FfiError> {
  271. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  272. input_proofs.into_iter().map(|p| p.try_into()).collect();
  273. let cdk_proofs = cdk_proofs?;
  274. // Convert spending conditions if provided
  275. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  276. let result = self
  277. .inner
  278. .swap(
  279. amount.map(Into::into),
  280. amount_split_target.into(),
  281. cdk_proofs,
  282. conditions,
  283. include_fees,
  284. )
  285. .await?;
  286. Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
  287. }
  288. /// Get proofs by states
  289. pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
  290. let mut all_proofs = Vec::new();
  291. for state in states {
  292. let proofs = match state {
  293. ProofState::Unspent => self.inner.get_unspent_proofs().await?,
  294. ProofState::Pending => self.inner.get_pending_proofs().await?,
  295. ProofState::Reserved => self.inner.get_reserved_proofs().await?,
  296. ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
  297. ProofState::Spent => {
  298. // CDK doesn't have a method to get spent proofs directly
  299. // They are removed from the database when spent
  300. continue;
  301. }
  302. };
  303. for proof in proofs {
  304. all_proofs.push(proof.into());
  305. }
  306. }
  307. Ok(all_proofs)
  308. }
  309. /// Check if proofs are spent
  310. pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
  311. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  312. proofs.into_iter().map(|p| p.try_into()).collect();
  313. let cdk_proofs = cdk_proofs?;
  314. let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
  315. // Convert ProofState to bool (spent = true, unspent = false)
  316. let spent_bools = proof_states
  317. .into_iter()
  318. .map(|proof_state| {
  319. matches!(
  320. proof_state.state,
  321. cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
  322. )
  323. })
  324. .collect();
  325. Ok(spent_bools)
  326. }
  327. /// List transactions
  328. pub async fn list_transactions(
  329. &self,
  330. direction: Option<TransactionDirection>,
  331. ) -> Result<Vec<Transaction>, FfiError> {
  332. let cdk_direction = direction.map(Into::into);
  333. let transactions = self.inner.list_transactions(cdk_direction).await?;
  334. Ok(transactions.into_iter().map(Into::into).collect())
  335. }
  336. /// Get transaction by ID
  337. pub async fn get_transaction(
  338. &self,
  339. id: TransactionId,
  340. ) -> Result<Option<Transaction>, FfiError> {
  341. let cdk_id = id.try_into()?;
  342. let transaction = self.inner.get_transaction(cdk_id).await?;
  343. Ok(transaction.map(Into::into))
  344. }
  345. /// Get proofs for a transaction by transaction ID
  346. ///
  347. /// This retrieves all proofs associated with a transaction by looking up
  348. /// the transaction's Y values and fetching the corresponding proofs.
  349. pub async fn get_proofs_for_transaction(
  350. &self,
  351. id: TransactionId,
  352. ) -> Result<Vec<Proof>, FfiError> {
  353. let cdk_id = id.try_into()?;
  354. let proofs = self.inner.get_proofs_for_transaction(cdk_id).await?;
  355. Ok(proofs.into_iter().map(Into::into).collect())
  356. }
  357. /// Revert a transaction
  358. pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
  359. let cdk_id = id.try_into()?;
  360. self.inner.revert_transaction(cdk_id).await?;
  361. Ok(())
  362. }
  363. /// Subscribe to wallet events
  364. pub async fn subscribe(
  365. &self,
  366. params: SubscribeParams,
  367. ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
  368. let cdk_params: cdk::nuts::nut17::Params<Arc<String>> = params.clone().into();
  369. let sub_id = cdk_params.id.to_string();
  370. let active_sub = self.inner.subscribe(cdk_params).await;
  371. Ok(std::sync::Arc::new(ActiveSubscription::new(
  372. active_sub, sub_id,
  373. )))
  374. }
  375. /// Refresh keysets from the mint
  376. pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
  377. let keysets = self.inner.refresh_keysets().await?;
  378. Ok(keysets.into_iter().map(Into::into).collect())
  379. }
  380. /// Get the active keyset for the wallet's unit
  381. pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
  382. let keyset = self.inner.get_active_keyset().await?;
  383. Ok(keyset.into())
  384. }
  385. /// Get fees for a specific keyset ID
  386. pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
  387. let id = cdk::nuts::Id::from_str(&keyset_id)
  388. .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
  389. Ok(self
  390. .inner
  391. .get_keyset_fees_and_amounts_by_id(id)
  392. .await?
  393. .fee())
  394. }
  395. /// Reclaim unspent proofs (mark them as unspent in the database)
  396. pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), FfiError> {
  397. let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
  398. proofs.iter().map(|p| p.clone().try_into()).collect();
  399. let cdk_proofs = cdk_proofs?;
  400. self.inner.reclaim_unspent(cdk_proofs).await?;
  401. Ok(())
  402. }
  403. /// Check all pending proofs and return the total amount reclaimed
  404. pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
  405. let amount = self.inner.check_all_pending_proofs().await?;
  406. Ok(amount.into())
  407. }
  408. /// Calculate fee for a given number of proofs with the specified keyset
  409. pub async fn calculate_fee(
  410. &self,
  411. proof_count: u32,
  412. keyset_id: String,
  413. ) -> Result<Amount, FfiError> {
  414. let id = cdk::nuts::Id::from_str(&keyset_id)
  415. .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
  416. let fee = self
  417. .inner
  418. .get_keyset_count_fee(&id, proof_count as u64)
  419. .await?;
  420. Ok(fee.into())
  421. }
  422. }
  423. /// BIP353 methods for Wallet
  424. #[cfg(not(target_arch = "wasm32"))]
  425. #[uniffi::export(async_runtime = "tokio")]
  426. impl Wallet {
  427. /// Get a quote for a BIP353 melt
  428. ///
  429. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  430. /// and then creates a melt quote for that offer.
  431. pub async fn melt_bip353_quote(
  432. &self,
  433. bip353_address: String,
  434. amount_msat: Amount,
  435. ) -> Result<MeltQuote, FfiError> {
  436. let cdk_amount: cdk::Amount = amount_msat.into();
  437. let quote = self
  438. .inner
  439. .melt_bip353_quote(&bip353_address, cdk_amount)
  440. .await?;
  441. Ok(quote.into())
  442. }
  443. /// Get a quote for a Lightning address melt
  444. ///
  445. /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
  446. /// and then creates a melt quote for that invoice.
  447. pub async fn melt_lightning_address_quote(
  448. &self,
  449. lightning_address: String,
  450. amount_msat: Amount,
  451. ) -> Result<MeltQuote, FfiError> {
  452. let cdk_amount: cdk::Amount = amount_msat.into();
  453. let quote = self
  454. .inner
  455. .melt_lightning_address_quote(&lightning_address, cdk_amount)
  456. .await?;
  457. Ok(quote.into())
  458. }
  459. /// Get a quote for a human-readable address melt
  460. ///
  461. /// This method accepts a human-readable address that could be either a BIP353 address
  462. /// or a Lightning address. It intelligently determines which to try based on mint support:
  463. ///
  464. /// 1. If the mint supports Bolt12, it tries BIP353 first
  465. /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
  466. /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
  467. /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
  468. pub async fn melt_human_readable(
  469. &self,
  470. address: String,
  471. amount_msat: Amount,
  472. ) -> Result<MeltQuote, FfiError> {
  473. let cdk_amount: cdk::Amount = amount_msat.into();
  474. let quote = self
  475. .inner
  476. .melt_human_readable_quote(&address, cdk_amount)
  477. .await?;
  478. Ok(quote.into())
  479. }
  480. }
  481. /// Auth methods for Wallet
  482. #[uniffi::export(async_runtime = "tokio")]
  483. impl Wallet {
  484. /// Set Clear Auth Token (CAT) for authentication
  485. pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
  486. self.inner.set_cat(cat).await?;
  487. Ok(())
  488. }
  489. /// Set refresh token for authentication
  490. pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
  491. self.inner.set_refresh_token(refresh_token).await?;
  492. Ok(())
  493. }
  494. /// Refresh access token using the stored refresh token
  495. pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
  496. self.inner.refresh_access_token().await?;
  497. Ok(())
  498. }
  499. /// Mint blind auth tokens
  500. pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
  501. let proofs = self.inner.mint_blind_auth(amount.into()).await?;
  502. Ok(proofs.into_iter().map(|p| p.into()).collect())
  503. }
  504. /// Get unspent auth proofs
  505. pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
  506. let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
  507. Ok(auth_proofs.into_iter().map(Into::into).collect())
  508. }
  509. }
  510. /// Configuration for creating wallets
  511. #[derive(Debug, Clone, uniffi::Record)]
  512. pub struct WalletConfig {
  513. pub target_proof_count: Option<u32>,
  514. }
  515. /// Generates a new random mnemonic phrase
  516. #[uniffi::export]
  517. pub fn generate_mnemonic() -> Result<String, FfiError> {
  518. let mnemonic =
  519. Mnemonic::generate(12).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  520. Ok(mnemonic.to_string())
  521. }
  522. /// Converts a mnemonic phrase to its entropy bytes
  523. #[uniffi::export]
  524. pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
  525. let m =
  526. Mnemonic::parse(&mnemonic).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  527. Ok(m.to_entropy())
  528. }