wallet.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. //! Cashu Wallet
  2. use std::collections::{HashMap, HashSet};
  3. use std::str::FromStr;
  4. use bip39::Mnemonic;
  5. use cashu::dhke::{construct_proofs, unblind_message};
  6. #[cfg(feature = "nut07")]
  7. use cashu::nuts::nut00::mint;
  8. use cashu::nuts::{
  9. BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, PreMintSecrets, PreSwap, Proof, Proofs,
  10. SwapRequest, Token,
  11. };
  12. #[cfg(feature = "nut07")]
  13. use cashu::types::ProofsStatus;
  14. use cashu::types::{MeltQuote, Melted, MintQuote};
  15. use cashu::url::UncheckedUrl;
  16. use cashu::{Amount, Bolt11Invoice};
  17. use thiserror::Error;
  18. use tracing::warn;
  19. use crate::client::Client;
  20. use crate::localstore::LocalStore;
  21. use crate::utils::unix_time;
  22. #[derive(Debug, Error)]
  23. pub enum Error {
  24. /// Insufficient Funds
  25. #[error("Insufficient Funds")]
  26. InsufficientFunds,
  27. #[error("`{0}`")]
  28. Cashu(#[from] cashu::error::wallet::Error),
  29. #[error("`{0}`")]
  30. Client(#[from] crate::client::Error),
  31. /// Cashu Url Error
  32. #[error("`{0}`")]
  33. CashuUrl(#[from] cashu::url::Error),
  34. #[error("Quote Expired")]
  35. QuoteExpired,
  36. #[error("Quote Unknown")]
  37. QuoteUnknown,
  38. #[error("`{0}`")]
  39. LocalStore(#[from] super::localstore::Error),
  40. #[error("`{0}`")]
  41. Custom(String),
  42. }
  43. #[derive(Clone, Debug)]
  44. pub struct BackupInfo {
  45. mnemonic: Mnemonic,
  46. counter: HashMap<Id, u64>,
  47. }
  48. #[derive(Clone, Debug)]
  49. pub struct Wallet<C: Client, L: LocalStore> {
  50. pub client: C,
  51. localstore: L,
  52. backup_info: Option<BackupInfo>,
  53. }
  54. impl<C: Client, L: LocalStore> Wallet<C, L> {
  55. pub async fn new(
  56. client: C,
  57. localstore: L,
  58. mint_quotes: Vec<MintQuote>,
  59. melt_quotes: Vec<MeltQuote>,
  60. backup_info: Option<BackupInfo>,
  61. mint_keys: Vec<Keys>,
  62. ) -> Self {
  63. for quote in mint_quotes {
  64. localstore.add_mint_quote(quote).await.ok();
  65. }
  66. for quote in melt_quotes {
  67. localstore.add_melt_quote(quote).await.ok();
  68. }
  69. for keys in mint_keys {
  70. localstore.add_keys(keys).await.ok();
  71. }
  72. Self {
  73. backup_info,
  74. client,
  75. localstore,
  76. }
  77. }
  78. /// Back up seed
  79. pub fn mnemonic(&self) -> Option<Mnemonic> {
  80. self.backup_info.clone().map(|b| b.mnemonic)
  81. }
  82. /// Back up keyset counters
  83. pub fn keyset_counters(&self) -> Option<HashMap<Id, u64>> {
  84. self.backup_info.clone().map(|b| b.counter)
  85. }
  86. /// Check if a proof is spent
  87. #[cfg(feature = "nut07")]
  88. pub async fn check_proofs_spent(
  89. &self,
  90. mint_url: UncheckedUrl,
  91. proofs: Proofs,
  92. ) -> Result<ProofsStatus, Error> {
  93. let spendable = self
  94. .client
  95. .post_check_spendable(
  96. mint_url.try_into()?,
  97. proofs
  98. .clone()
  99. .into_iter()
  100. .map(|p| p.into())
  101. .collect::<mint::Proofs>()
  102. .clone(),
  103. )
  104. .await?;
  105. // Separate proofs in spent and unspent based on mint response
  106. let (spendable, spent): (Vec<_>, Vec<_>) = proofs
  107. .iter()
  108. .zip(spendable.spendable.iter())
  109. .partition(|(_, &b)| b);
  110. Ok(ProofsStatus {
  111. spendable: spendable.into_iter().map(|(s, _)| s).cloned().collect(),
  112. spent: spent.into_iter().map(|(s, _)| s).cloned().collect(),
  113. })
  114. }
  115. /*
  116. // TODO: This should be create token
  117. // the requited proofs for the token amount may already be in the wallet and mint is not needed
  118. // Mint a token
  119. pub async fn mint_token(
  120. &mut self,
  121. mint_url: UncheckedUrl,
  122. amount: Amount,
  123. memo: Option<String>,
  124. unit: Option<CurrencyUnit>,
  125. ) -> Result<Token, Error> {
  126. let quote = self
  127. .mint_quote(
  128. mint_url.clone(),
  129. amount,
  130. unit.clone()
  131. .ok_or(Error::Custom("Unit required".to_string()))?,
  132. )
  133. .await?;
  134. let proofs = self.mint(mint_url.clone(), &quote.id).await?;
  135. let token = Token::new(mint_url.clone(), proofs, memo, unit);
  136. Ok(token?)
  137. }
  138. */
  139. /// Mint Quote
  140. pub async fn mint_quote(
  141. &mut self,
  142. mint_url: UncheckedUrl,
  143. amount: Amount,
  144. unit: CurrencyUnit,
  145. ) -> Result<MintQuote, Error> {
  146. let quote_res = self
  147. .client
  148. .post_mint_quote(mint_url.try_into()?, amount, unit.clone())
  149. .await?;
  150. let quote = MintQuote {
  151. id: quote_res.quote.clone(),
  152. amount,
  153. unit: unit.clone(),
  154. request: Bolt11Invoice::from_str(&quote_res.request).unwrap(),
  155. paid: quote_res.paid,
  156. expiry: quote_res.expiry,
  157. };
  158. self.localstore.add_mint_quote(quote.clone()).await?;
  159. Ok(quote)
  160. }
  161. async fn active_mint_keyset(
  162. &mut self,
  163. mint_url: &UncheckedUrl,
  164. unit: &CurrencyUnit,
  165. ) -> Result<Option<Id>, Error> {
  166. if let Some(keysets) = self.localstore.get_mint_keysets(mint_url.clone()).await? {
  167. for keyset in keysets {
  168. if keyset.unit.eq(unit) && keyset.active {
  169. return Ok(Some(keyset.id));
  170. }
  171. }
  172. } else {
  173. let keysets = self.client.get_mint_keysets(mint_url.try_into()?).await?;
  174. self.localstore
  175. .add_mint_keysets(mint_url.clone(), keysets.keysets.into_iter().collect())
  176. .await?;
  177. }
  178. Ok(None)
  179. }
  180. async fn active_keys(
  181. &mut self,
  182. mint_url: &UncheckedUrl,
  183. unit: &CurrencyUnit,
  184. ) -> Result<Option<Keys>, Error> {
  185. let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?.unwrap();
  186. let mut keys = None;
  187. if let Some(k) = self.localstore.get_keys(&active_keyset_id).await? {
  188. keys = Some(k.clone())
  189. } else {
  190. let keysets = self.client.get_mint_keys(mint_url.try_into()?).await?;
  191. for keyset in keysets {
  192. if keyset.id.eq(&active_keyset_id) {
  193. keys = Some(keyset.keys.clone())
  194. }
  195. self.localstore.add_keys(keyset.keys).await?;
  196. }
  197. }
  198. Ok(keys)
  199. }
  200. /// Mint
  201. pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result<Amount, Error> {
  202. let quote_info = self.localstore.get_mint_quote(quote_id).await?;
  203. let quote_info = if let Some(quote) = quote_info {
  204. if quote.expiry.le(&unix_time()) {
  205. return Err(Error::QuoteExpired);
  206. }
  207. quote.clone()
  208. } else {
  209. return Err(Error::QuoteUnknown);
  210. };
  211. let active_keyset_id = self
  212. .active_mint_keyset(&mint_url, &quote_info.unit)
  213. .await?
  214. .unwrap();
  215. let premint_secrets = match &self.backup_info {
  216. Some(backup_info) => PreMintSecrets::from_seed(
  217. active_keyset_id,
  218. *backup_info.counter.get(&active_keyset_id).unwrap_or(&0),
  219. &backup_info.mnemonic,
  220. quote_info.amount,
  221. )?,
  222. None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?,
  223. };
  224. let mint_res = self
  225. .client
  226. .post_mint(
  227. mint_url.clone().try_into()?,
  228. quote_id,
  229. premint_secrets.clone(),
  230. )
  231. .await?;
  232. let keys = self.localstore.get_keys(&active_keyset_id).await?.unwrap();
  233. let proofs = construct_proofs(
  234. mint_res.signatures,
  235. premint_secrets.rs(),
  236. premint_secrets.secrets(),
  237. &keys,
  238. )?;
  239. let minted_amount = proofs.iter().map(|p| p.amount).sum();
  240. // Remove filled quote from store
  241. self.localstore.remove_mint_quote(&quote_info.id).await?;
  242. // Add new proofs to store
  243. self.localstore.add_proofs(mint_url, proofs).await?;
  244. Ok(minted_amount)
  245. }
  246. /// Receive
  247. pub async fn receive(&mut self, encoded_token: &str) -> Result<(), Error> {
  248. let token_data = Token::from_str(encoded_token)?;
  249. let unit = token_data.unit.unwrap_or_default();
  250. let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
  251. for token in token_data.token {
  252. if token.proofs.is_empty() {
  253. continue;
  254. }
  255. let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?;
  256. // TODO: if none fetch keyset for mint
  257. let keys = self.localstore.get_keys(&active_keyset_id.unwrap()).await?;
  258. // Sum amount of all proofs
  259. let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
  260. let pre_swap = self
  261. .create_swap(&token.mint, &unit, Some(amount), token.proofs)
  262. .await?;
  263. let swap_response = self
  264. .client
  265. .post_split(token.mint.clone().try_into()?, pre_swap.split_request)
  266. .await?;
  267. // Proof to keep
  268. let p = construct_proofs(
  269. swap_response.signatures,
  270. pre_swap.pre_mint_secrets.rs(),
  271. pre_swap.pre_mint_secrets.secrets(),
  272. &keys.unwrap(),
  273. )?;
  274. let mint_proofs = proofs.entry(token.mint).or_default();
  275. mint_proofs.extend(p);
  276. }
  277. for (mint, proofs) in proofs {
  278. self.localstore.add_proofs(mint, proofs).await?;
  279. }
  280. Ok(())
  281. }
  282. /// Create Split Payload
  283. async fn create_swap(
  284. &mut self,
  285. mint_url: &UncheckedUrl,
  286. unit: &CurrencyUnit,
  287. amount: Option<Amount>,
  288. proofs: Proofs,
  289. ) -> Result<PreSwap, Error> {
  290. // Since split is used to get the needed combination of tokens for a specific
  291. // amount first blinded messages are created for the amount
  292. let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?.unwrap();
  293. let pre_mint_secrets = if let Some(amount) = amount {
  294. let mut desired_messages = PreMintSecrets::random(active_keyset_id, amount)?;
  295. let change_amount = proofs.iter().map(|p| p.amount).sum::<Amount>() - amount;
  296. let change_messages = PreMintSecrets::random(active_keyset_id, change_amount)?;
  297. // Combine the BlindedMessages totoalling the desired amount with change
  298. desired_messages.combine(change_messages);
  299. // Sort the premint secrets to avoid finger printing
  300. desired_messages.sort_secrets();
  301. desired_messages
  302. } else {
  303. let value = proofs.iter().map(|p| p.amount).sum();
  304. PreMintSecrets::random(active_keyset_id, value)?
  305. };
  306. let split_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages());
  307. Ok(PreSwap {
  308. pre_mint_secrets,
  309. split_request,
  310. })
  311. }
  312. pub async fn process_swap_response(
  313. &self,
  314. blinded_messages: PreMintSecrets,
  315. promises: Vec<BlindedSignature>,
  316. ) -> Result<Proofs, Error> {
  317. let mut proofs = vec![];
  318. for (promise, premint) in promises.iter().zip(blinded_messages) {
  319. let a = self
  320. .localstore
  321. .get_keys(&promise.keyset_id)
  322. .await?
  323. .unwrap()
  324. .amount_key(promise.amount)
  325. .unwrap()
  326. .to_owned();
  327. let blinded_c = promise.c.clone();
  328. let unblinded_sig = unblind_message(blinded_c, premint.r.into(), a).unwrap();
  329. let proof = Proof {
  330. keyset_id: promise.keyset_id,
  331. amount: promise.amount,
  332. secret: premint.secret,
  333. c: unblinded_sig,
  334. };
  335. proofs.push(proof);
  336. }
  337. Ok(proofs)
  338. }
  339. /// Send
  340. pub async fn send(
  341. &mut self,
  342. mint_url: &UncheckedUrl,
  343. unit: &CurrencyUnit,
  344. amount: Amount,
  345. ) -> Result<Proofs, Error> {
  346. let proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
  347. let pre_swap = self
  348. .create_swap(mint_url, unit, Some(amount), proofs.clone())
  349. .await?;
  350. let swap_response = self
  351. .client
  352. .post_split(mint_url.clone().try_into()?, pre_swap.split_request)
  353. .await?;
  354. let mut keep_proofs = Proofs::new();
  355. let mut send_proofs = Proofs::new();
  356. let mut post_swap_proofs = construct_proofs(
  357. swap_response.signatures,
  358. pre_swap.pre_mint_secrets.rs(),
  359. pre_swap.pre_mint_secrets.secrets(),
  360. &self.active_keys(mint_url, unit).await?.unwrap(),
  361. )?;
  362. post_swap_proofs.reverse();
  363. for proof in post_swap_proofs {
  364. if (proof.amount + send_proofs.iter().map(|p| p.amount).sum()).gt(&amount) {
  365. keep_proofs.push(proof);
  366. } else {
  367. send_proofs.push(proof);
  368. }
  369. }
  370. let send_amount: Amount = send_proofs.iter().map(|p| p.amount).sum();
  371. if send_amount.ne(&amount) {
  372. warn!(
  373. "Send amount proofs is {:?} expected {:?}",
  374. send_amount, amount
  375. );
  376. }
  377. self.localstore
  378. .remove_proofs(mint_url.clone(), &proofs)
  379. .await?;
  380. self.localstore
  381. .add_proofs(mint_url.clone(), keep_proofs)
  382. .await?;
  383. // REVIEW: send proofs are not added to the store since they should be
  384. // used. but if they are not they will be lost. There should likely be a
  385. // pendiing proof store
  386. Ok(send_proofs)
  387. }
  388. /// Melt Quote
  389. pub async fn melt_quote(
  390. &mut self,
  391. mint_url: UncheckedUrl,
  392. unit: CurrencyUnit,
  393. request: Bolt11Invoice,
  394. ) -> Result<MeltQuote, Error> {
  395. let quote_res = self
  396. .client
  397. .post_melt_quote(mint_url.clone().try_into()?, unit.clone(), request.clone())
  398. .await?;
  399. let quote = MeltQuote {
  400. id: quote_res.quote,
  401. amount: quote_res.amount.into(),
  402. request,
  403. unit,
  404. fee_reserve: quote_res.fee_reserve.into(),
  405. paid: quote_res.paid,
  406. expiry: quote_res.expiry,
  407. };
  408. self.localstore.add_melt_quote(quote.clone()).await?;
  409. Ok(quote)
  410. }
  411. // Select proofs
  412. async fn select_proofs(
  413. &self,
  414. mint_url: UncheckedUrl,
  415. unit: &CurrencyUnit,
  416. amount: Amount,
  417. ) -> Result<Proofs, Error> {
  418. let mint_proofs = self
  419. .localstore
  420. .get_proofs(mint_url.clone())
  421. .await?
  422. .ok_or(Error::InsufficientFunds)?;
  423. let mint_keysets = self.localstore.get_mint_keysets(mint_url).await?.unwrap();
  424. let (active, inactive): (HashSet<KeySetInfo>, HashSet<KeySetInfo>) = mint_keysets
  425. .into_iter()
  426. .filter(|p| p.unit.eq(unit))
  427. .partition(|x| x.active);
  428. let active: HashSet<Id> = active.iter().map(|k| k.id).collect();
  429. let inactive: HashSet<Id> = inactive.iter().map(|k| k.id).collect();
  430. let mut active_proofs: Proofs = Vec::new();
  431. let mut inactive_proofs: Proofs = Vec::new();
  432. for proof in mint_proofs {
  433. if active.contains(&proof.keyset_id) {
  434. active_proofs.push(proof);
  435. } else if inactive.contains(&proof.keyset_id) {
  436. inactive_proofs.push(proof);
  437. }
  438. }
  439. active_proofs.reverse();
  440. inactive_proofs.reverse();
  441. inactive_proofs.append(&mut active_proofs);
  442. let proofs = inactive_proofs;
  443. let mut selected_proofs: Proofs = Vec::new();
  444. for proof in proofs {
  445. if selected_proofs.iter().map(|p| p.amount).sum::<Amount>() < amount {
  446. selected_proofs.push(proof);
  447. }
  448. }
  449. if selected_proofs.iter().map(|p| p.amount).sum::<Amount>() < amount {
  450. return Err(Error::InsufficientFunds);
  451. }
  452. Ok(selected_proofs)
  453. }
  454. /// Melt
  455. pub async fn melt(&mut self, mint_url: &UncheckedUrl, quote_id: &str) -> Result<Melted, Error> {
  456. let quote_info = self.localstore.get_melt_quote(quote_id).await?;
  457. let quote_info = if let Some(quote) = quote_info {
  458. if quote.expiry.le(&unix_time()) {
  459. return Err(Error::QuoteExpired);
  460. }
  461. quote.clone()
  462. } else {
  463. return Err(Error::QuoteUnknown);
  464. };
  465. let blinded = PreMintSecrets::blank(
  466. self.active_mint_keyset(mint_url, &quote_info.unit)
  467. .await?
  468. .unwrap(),
  469. quote_info.fee_reserve,
  470. )?;
  471. let proofs = self
  472. .select_proofs(mint_url.clone(), &quote_info.unit, quote_info.amount)
  473. .await?;
  474. let melt_response = self
  475. .client
  476. .post_melt(
  477. mint_url.clone().try_into()?,
  478. quote_id.to_string(),
  479. proofs,
  480. Some(blinded.blinded_messages()),
  481. )
  482. .await?;
  483. let change_proofs = match melt_response.change {
  484. Some(change) => Some(construct_proofs(
  485. change,
  486. blinded.rs(),
  487. blinded.secrets(),
  488. &self.active_keys(mint_url, &quote_info.unit).await?.unwrap(),
  489. )?),
  490. None => None,
  491. };
  492. let melted = Melted {
  493. paid: true,
  494. preimage: melt_response.payment_preimage,
  495. change: change_proofs,
  496. };
  497. self.localstore.remove_melt_quote(&quote_info.id).await?;
  498. Ok(melted)
  499. }
  500. pub fn proofs_to_token(
  501. &self,
  502. mint_url: UncheckedUrl,
  503. proofs: Proofs,
  504. memo: Option<String>,
  505. unit: Option<CurrencyUnit>,
  506. ) -> Result<String, Error> {
  507. Ok(Token::new(mint_url, proofs, memo, unit)?.to_string())
  508. }
  509. }
  510. /*
  511. #[cfg(test)]
  512. mod tests {
  513. use std::collections::{HashMap, HashSet};
  514. use super::*;
  515. use crate::client::Client;
  516. use crate::mint::Mint;
  517. use cashu::nuts::nut04;
  518. #[test]
  519. fn test_wallet() {
  520. let mut mint = Mint::new(
  521. "supersecretsecret",
  522. "0/0/0/0",
  523. HashMap::new(),
  524. HashSet::new(),
  525. 32,
  526. );
  527. let keys = mint.active_keyset_pubkeys();
  528. let client = Client::new("https://cashu-rs.thesimplekid.space/").unwrap();
  529. let wallet = Wallet::new(client, keys.keys);
  530. let blinded_messages = BlindedMessages::random(Amount::from_sat(64)).unwrap();
  531. let mint_request = nut04::MintRequest {
  532. outputs: blinded_messages.blinded_messages.clone(),
  533. };
  534. let res = mint.process_mint_request(mint_request).unwrap();
  535. let proofs = wallet
  536. .process_split_response(blinded_messages, res.promises)
  537. .unwrap();
  538. for proof in &proofs {
  539. mint.verify_proof(proof).unwrap();
  540. }
  541. let split = wallet.create_split(proofs.clone()).unwrap();
  542. let split_request = split.split_payload;
  543. let split_response = mint.process_split_request(split_request).unwrap();
  544. let p = split_response.promises;
  545. let snd_proofs = wallet
  546. .process_split_response(split.blinded_messages, p.unwrap())
  547. .unwrap();
  548. let mut error = false;
  549. for proof in &snd_proofs {
  550. if let Err(err) = mint.verify_proof(proof) {
  551. println!("{err}{:?}", serde_json::to_string(proof));
  552. error = true;
  553. }
  554. }
  555. if error {
  556. panic!()
  557. }
  558. }
  559. }
  560. */