wallet.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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. #[uniffi::export(async_runtime = "tokio")]
  15. impl Wallet {
  16. /// Create a new Wallet from mnemonic using WalletDatabase trait
  17. #[uniffi::constructor]
  18. pub fn new(
  19. mint_url: String,
  20. unit: CurrencyUnit,
  21. mnemonic: String,
  22. db: Arc<dyn crate::database::WalletDatabase>,
  23. config: WalletConfig,
  24. ) -> Result<Self, FfiError> {
  25. // Parse mnemonic and generate seed without passphrase
  26. let m = Mnemonic::parse(&mnemonic)
  27. .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  28. let seed = m.to_seed_normalized("");
  29. // Convert the FFI database trait to a CDK database implementation
  30. let localstore = crate::database::create_cdk_database_from_ffi(db);
  31. let wallet =
  32. CdkWalletBuilder::new()
  33. .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
  34. FfiError::InvalidUrl { msg: e.to_string() }
  35. })?)
  36. .unit(unit.into())
  37. .localstore(localstore)
  38. .seed(seed)
  39. .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
  40. .build()
  41. .map_err(FfiError::from)?;
  42. Ok(Self {
  43. inner: Arc::new(wallet),
  44. })
  45. }
  46. /// Get the mint URL
  47. pub fn mint_url(&self) -> MintUrl {
  48. self.inner.mint_url.clone().into()
  49. }
  50. /// Get the currency unit
  51. pub fn unit(&self) -> CurrencyUnit {
  52. self.inner.unit.clone().into()
  53. }
  54. /// Get total balance
  55. pub async fn total_balance(&self) -> Result<Amount, FfiError> {
  56. let balance = self.inner.total_balance().await?;
  57. Ok(balance.into())
  58. }
  59. /// Get total pending balance
  60. pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
  61. let balance = self.inner.total_pending_balance().await?;
  62. Ok(balance.into())
  63. }
  64. /// Get total reserved balance
  65. pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
  66. let balance = self.inner.total_reserved_balance().await?;
  67. Ok(balance.into())
  68. }
  69. /// Get mint info
  70. pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
  71. let info = self.inner.fetch_mint_info().await?;
  72. Ok(info.map(Into::into))
  73. }
  74. /// Receive tokens
  75. pub async fn receive(
  76. &self,
  77. token: std::sync::Arc<Token>,
  78. options: ReceiveOptions,
  79. ) -> Result<Amount, FfiError> {
  80. let amount = self
  81. .inner
  82. .receive(&token.to_string(), options.into())
  83. .await?;
  84. Ok(amount.into())
  85. }
  86. /// Restore wallet from seed
  87. pub async fn restore(&self) -> Result<Amount, FfiError> {
  88. let amount = self.inner.restore().await?;
  89. Ok(amount.into())
  90. }
  91. /// Verify token DLEQ proofs
  92. pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
  93. let cdk_token = token.inner.clone();
  94. self.inner.verify_token_dleq(&cdk_token).await?;
  95. Ok(())
  96. }
  97. /// Receive proofs directly
  98. pub async fn receive_proofs(
  99. &self,
  100. proofs: Proofs,
  101. options: ReceiveOptions,
  102. memo: Option<String>,
  103. ) -> Result<Amount, FfiError> {
  104. let cdk_proofs: Vec<cdk::nuts::Proof> =
  105. proofs.into_iter().map(|p| p.inner.clone()).collect();
  106. let amount = self
  107. .inner
  108. .receive_proofs(cdk_proofs, options.into(), memo)
  109. .await?;
  110. Ok(amount.into())
  111. }
  112. /// Prepare a send operation
  113. pub async fn prepare_send(
  114. &self,
  115. amount: Amount,
  116. options: SendOptions,
  117. ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
  118. let prepared = self
  119. .inner
  120. .prepare_send(amount.into(), options.into())
  121. .await?;
  122. Ok(std::sync::Arc::new(prepared.into()))
  123. }
  124. /// Get a mint quote
  125. pub async fn mint_quote(
  126. &self,
  127. amount: Amount,
  128. description: Option<String>,
  129. ) -> Result<MintQuote, FfiError> {
  130. let quote = self.inner.mint_quote(amount.into(), description).await?;
  131. Ok(quote.into())
  132. }
  133. /// Mint tokens
  134. pub async fn mint(
  135. &self,
  136. quote_id: String,
  137. amount_split_target: SplitTarget,
  138. spending_conditions: Option<SpendingConditions>,
  139. ) -> Result<Proofs, FfiError> {
  140. // Convert spending conditions if provided
  141. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  142. let proofs = self
  143. .inner
  144. .mint(&quote_id, amount_split_target.into(), conditions)
  145. .await?;
  146. Ok(proofs
  147. .into_iter()
  148. .map(|p| std::sync::Arc::new(p.into()))
  149. .collect())
  150. }
  151. /// Get a melt quote
  152. pub async fn melt_quote(
  153. &self,
  154. request: String,
  155. options: Option<MeltOptions>,
  156. ) -> Result<MeltQuote, FfiError> {
  157. let cdk_options = options.map(Into::into);
  158. let quote = self.inner.melt_quote(request, cdk_options).await?;
  159. Ok(quote.into())
  160. }
  161. /// Melt tokens
  162. pub async fn melt(&self, quote_id: String) -> Result<Melted, FfiError> {
  163. let melted = self.inner.melt(&quote_id).await?;
  164. Ok(melted.into())
  165. }
  166. /// Get a quote for a bolt12 mint
  167. pub async fn mint_bolt12_quote(
  168. &self,
  169. amount: Option<Amount>,
  170. description: Option<String>,
  171. ) -> Result<MintQuote, FfiError> {
  172. let quote = self
  173. .inner
  174. .mint_bolt12_quote(amount.map(Into::into), description)
  175. .await?;
  176. Ok(quote.into())
  177. }
  178. /// Mint tokens using bolt12
  179. pub async fn mint_bolt12(
  180. &self,
  181. quote_id: String,
  182. amount: Option<Amount>,
  183. amount_split_target: SplitTarget,
  184. spending_conditions: Option<SpendingConditions>,
  185. ) -> Result<Proofs, FfiError> {
  186. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  187. let proofs = self
  188. .inner
  189. .mint_bolt12(
  190. &quote_id,
  191. amount.map(Into::into),
  192. amount_split_target.into(),
  193. conditions,
  194. )
  195. .await?;
  196. Ok(proofs
  197. .into_iter()
  198. .map(|p| std::sync::Arc::new(p.into()))
  199. .collect())
  200. }
  201. /// Get a quote for a bolt12 melt
  202. pub async fn melt_bolt12_quote(
  203. &self,
  204. request: String,
  205. options: Option<MeltOptions>,
  206. ) -> Result<MeltQuote, FfiError> {
  207. let cdk_options = options.map(Into::into);
  208. let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?;
  209. Ok(quote.into())
  210. }
  211. /// Swap proofs
  212. pub async fn swap(
  213. &self,
  214. amount: Option<Amount>,
  215. amount_split_target: SplitTarget,
  216. input_proofs: Proofs,
  217. spending_conditions: Option<SpendingConditions>,
  218. include_fees: bool,
  219. ) -> Result<Option<Proofs>, FfiError> {
  220. let cdk_proofs: Vec<cdk::nuts::Proof> =
  221. input_proofs.into_iter().map(|p| p.inner.clone()).collect();
  222. // Convert spending conditions if provided
  223. let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
  224. let result = self
  225. .inner
  226. .swap(
  227. amount.map(Into::into),
  228. amount_split_target.into(),
  229. cdk_proofs,
  230. conditions,
  231. include_fees,
  232. )
  233. .await?;
  234. Ok(result.map(|proofs| {
  235. proofs
  236. .into_iter()
  237. .map(|p| std::sync::Arc::new(p.into()))
  238. .collect()
  239. }))
  240. }
  241. /// Get proofs by states
  242. pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
  243. let mut all_proofs = Vec::new();
  244. for state in states {
  245. let proofs = match state {
  246. ProofState::Unspent => self.inner.get_unspent_proofs().await?,
  247. ProofState::Pending => self.inner.get_pending_proofs().await?,
  248. ProofState::Reserved => self.inner.get_reserved_proofs().await?,
  249. ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
  250. ProofState::Spent => {
  251. // CDK doesn't have a method to get spent proofs directly
  252. // They are removed from the database when spent
  253. continue;
  254. }
  255. };
  256. for proof in proofs {
  257. all_proofs.push(std::sync::Arc::new(proof.into()));
  258. }
  259. }
  260. Ok(all_proofs)
  261. }
  262. /// Check if proofs are spent
  263. pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
  264. let cdk_proofs: Vec<cdk::nuts::Proof> =
  265. proofs.into_iter().map(|p| p.inner.clone()).collect();
  266. let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
  267. // Convert ProofState to bool (spent = true, unspent = false)
  268. let spent_bools = proof_states
  269. .into_iter()
  270. .map(|proof_state| {
  271. matches!(
  272. proof_state.state,
  273. cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
  274. )
  275. })
  276. .collect();
  277. Ok(spent_bools)
  278. }
  279. /// List transactions
  280. pub async fn list_transactions(
  281. &self,
  282. direction: Option<TransactionDirection>,
  283. ) -> Result<Vec<Transaction>, FfiError> {
  284. let cdk_direction = direction.map(Into::into);
  285. let transactions = self.inner.list_transactions(cdk_direction).await?;
  286. Ok(transactions.into_iter().map(Into::into).collect())
  287. }
  288. /// Get transaction by ID
  289. pub async fn get_transaction(
  290. &self,
  291. id: TransactionId,
  292. ) -> Result<Option<Transaction>, FfiError> {
  293. let cdk_id = id.try_into()?;
  294. let transaction = self.inner.get_transaction(cdk_id).await?;
  295. Ok(transaction.map(Into::into))
  296. }
  297. /// Revert a transaction
  298. pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
  299. let cdk_id = id.try_into()?;
  300. self.inner.revert_transaction(cdk_id).await?;
  301. Ok(())
  302. }
  303. /// Subscribe to wallet events
  304. pub async fn subscribe(
  305. &self,
  306. params: SubscribeParams,
  307. ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
  308. let cdk_params: cdk::nuts::nut17::Params<cdk::pub_sub::SubId> = params.clone().into();
  309. let sub_id = cdk_params.id.to_string();
  310. let active_sub = self.inner.subscribe(cdk_params).await;
  311. Ok(std::sync::Arc::new(ActiveSubscription::new(
  312. active_sub, sub_id,
  313. )))
  314. }
  315. /// Refresh keysets from the mint
  316. pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
  317. let keysets = self.inner.refresh_keysets().await?;
  318. Ok(keysets.into_iter().map(Into::into).collect())
  319. }
  320. /// Get the active keyset for the wallet's unit
  321. pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
  322. let keyset = self.inner.get_active_keyset().await?;
  323. Ok(keyset.into())
  324. }
  325. /// Get fees for a specific keyset ID
  326. pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
  327. let id = cdk::nuts::Id::from_str(&keyset_id)
  328. .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
  329. Ok(self
  330. .inner
  331. .get_keyset_fees_and_amounts_by_id(id)
  332. .await?
  333. .fee())
  334. }
  335. /// Reclaim unspent proofs (mark them as unspent in the database)
  336. pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), FfiError> {
  337. let cdk_proofs: Vec<cdk::nuts::Proof> = proofs.iter().map(|p| p.inner.clone()).collect();
  338. self.inner.reclaim_unspent(cdk_proofs).await?;
  339. Ok(())
  340. }
  341. /// Check all pending proofs and return the total amount reclaimed
  342. pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
  343. let amount = self.inner.check_all_pending_proofs().await?;
  344. Ok(amount.into())
  345. }
  346. /// Calculate fee for a given number of proofs with the specified keyset
  347. pub async fn calculate_fee(
  348. &self,
  349. proof_count: u32,
  350. keyset_id: String,
  351. ) -> Result<Amount, FfiError> {
  352. let id = cdk::nuts::Id::from_str(&keyset_id)
  353. .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
  354. let fee_and_amounts = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
  355. let total_fee = (proof_count as u64 * fee_and_amounts.fee()) / 1000; // fee is per thousand
  356. Ok(Amount::new(total_fee))
  357. }
  358. }
  359. /// BIP353 methods for Wallet
  360. #[cfg(not(target_arch = "wasm32"))]
  361. #[uniffi::export(async_runtime = "tokio")]
  362. impl Wallet {
  363. /// Get a quote for a BIP353 melt
  364. ///
  365. /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
  366. /// and then creates a melt quote for that offer.
  367. pub async fn melt_bip353_quote(
  368. &self,
  369. bip353_address: String,
  370. amount_msat: Amount,
  371. ) -> Result<MeltQuote, FfiError> {
  372. let cdk_amount: cdk::Amount = amount_msat.into();
  373. let quote = self
  374. .inner
  375. .melt_bip353_quote(&bip353_address, cdk_amount)
  376. .await?;
  377. Ok(quote.into())
  378. }
  379. }
  380. /// Auth methods for Wallet
  381. #[uniffi::export(async_runtime = "tokio")]
  382. impl Wallet {
  383. /// Set Clear Auth Token (CAT) for authentication
  384. pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
  385. self.inner.set_cat(cat).await?;
  386. Ok(())
  387. }
  388. /// Set refresh token for authentication
  389. pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
  390. self.inner.set_refresh_token(refresh_token).await?;
  391. Ok(())
  392. }
  393. /// Refresh access token using the stored refresh token
  394. pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
  395. self.inner.refresh_access_token().await?;
  396. Ok(())
  397. }
  398. /// Mint blind auth tokens
  399. pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
  400. let proofs = self.inner.mint_blind_auth(amount.into()).await?;
  401. Ok(proofs
  402. .into_iter()
  403. .map(|p| std::sync::Arc::new(p.into()))
  404. .collect())
  405. }
  406. /// Get unspent auth proofs
  407. pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
  408. let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
  409. Ok(auth_proofs.into_iter().map(Into::into).collect())
  410. }
  411. }
  412. /// Configuration for creating wallets
  413. #[derive(Debug, Clone, uniffi::Record)]
  414. pub struct WalletConfig {
  415. pub target_proof_count: Option<u32>,
  416. }
  417. /// Generates a new random mnemonic phrase
  418. #[uniffi::export]
  419. pub fn generate_mnemonic() -> Result<String, FfiError> {
  420. let mnemonic =
  421. Mnemonic::generate(12).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  422. Ok(mnemonic.to_string())
  423. }
  424. /// Converts a mnemonic phrase to its entropy bytes
  425. #[uniffi::export]
  426. pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
  427. let m =
  428. Mnemonic::parse(&mnemonic).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
  429. Ok(m.to_entropy())
  430. }