wallet.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. //! Cashu Wallet
  2. use std::str::FromStr;
  3. use cashu::dhke::{construct_proofs, unblind_message};
  4. use cashu::nuts::{
  5. BlindedMessages, BlindedSignature, Keys, Proof, Proofs, RequestMintResponse, SplitPayload,
  6. SplitRequest, Token,
  7. };
  8. #[cfg(feature = "nut07")]
  9. use cashu::types::ProofsStatus;
  10. use cashu::types::{Melted, SendProofs};
  11. use cashu::url::UncheckedUrl;
  12. use cashu::Amount;
  13. pub use cashu::Bolt11Invoice;
  14. use thiserror::Error;
  15. use tracing::warn;
  16. use crate::client::Client;
  17. #[derive(Debug, Error)]
  18. pub enum Error {
  19. /// Insufficient Funds
  20. #[error("Insuddicient Funds")]
  21. InsufficientFunds,
  22. #[error("`{0}`")]
  23. Cashu(#[from] cashu::error::wallet::Error),
  24. #[error("`{0}`")]
  25. Client(#[from] crate::client::Error),
  26. /// Cashu Url Error
  27. #[error("`{0}`")]
  28. CashuUrl(#[from] cashu::url::Error),
  29. #[error("`{0}`")]
  30. Custom(String),
  31. }
  32. #[derive(Clone, Debug)]
  33. pub struct Wallet<C: Client> {
  34. pub client: C,
  35. pub mint_url: UncheckedUrl,
  36. pub mint_keys: Keys,
  37. pub balance: Amount,
  38. }
  39. impl<C: Client> Wallet<C> {
  40. pub fn new(client: C, mint_url: UncheckedUrl, mint_keys: Keys) -> Self {
  41. Self {
  42. client,
  43. mint_url,
  44. mint_keys,
  45. balance: Amount::ZERO,
  46. }
  47. }
  48. // TODO: getter method for keys that if it cant get them try again
  49. /// Check if a proof is spent
  50. #[cfg(feature = "nut07")]
  51. pub async fn check_proofs_spent(
  52. &self,
  53. proofs: Vec<cashu::nuts::nut00::mint::Proof>,
  54. ) -> Result<ProofsStatus, Error> {
  55. let spendable = self
  56. .client
  57. .post_check_spendable(self.mint_url.clone().try_into()?, proofs.clone())
  58. .await?;
  59. // Separate proofs in spent and unspent based on mint response
  60. let (spendable, spent): (Vec<_>, Vec<_>) = proofs
  61. .iter()
  62. .zip(spendable.spendable.iter())
  63. .partition(|(_, &b)| b);
  64. Ok(ProofsStatus {
  65. spendable: spendable.into_iter().map(|(s, _)| s).cloned().collect(),
  66. spent: spent.into_iter().map(|(s, _)| s).cloned().collect(),
  67. })
  68. }
  69. /// Request Token Mint
  70. pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
  71. Ok(self
  72. .client
  73. .get_request_mint(self.mint_url.clone().try_into()?, amount)
  74. .await?)
  75. }
  76. pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> {
  77. let proofs = self.mint(amount, hash).await?;
  78. let token = Token::new(self.mint_url.clone(), proofs, None);
  79. Ok(token?)
  80. }
  81. /// Mint Proofs
  82. pub async fn mint(&self, amount: Amount, hash: &str) -> Result<Proofs, Error> {
  83. let blinded_messages = BlindedMessages::random(amount)?;
  84. let mint_res = self
  85. .client
  86. .post_mint(
  87. self.mint_url.clone().try_into()?,
  88. blinded_messages.clone(),
  89. hash,
  90. )
  91. .await?;
  92. let proofs = construct_proofs(
  93. mint_res.promises,
  94. blinded_messages.rs,
  95. blinded_messages.secrets,
  96. &self.mint_keys,
  97. )?;
  98. Ok(proofs)
  99. }
  100. /// Check fee
  101. pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> {
  102. Ok(self
  103. .client
  104. .post_check_fees(self.mint_url.clone().try_into()?, invoice)
  105. .await?
  106. .fee)
  107. }
  108. /// Receive
  109. pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
  110. let token_data = Token::from_str(encoded_token)?;
  111. let mut proofs: Vec<Proofs> = vec![vec![]];
  112. for token in token_data.token {
  113. if token.proofs.is_empty() {
  114. continue;
  115. }
  116. let keys = if token.mint.to_string().eq(&self.mint_url.to_string()) {
  117. self.mint_keys.clone()
  118. } else {
  119. self.client.get_mint_keys(token.mint.try_into()?).await?
  120. };
  121. // Sum amount of all proofs
  122. let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
  123. let split_payload = self.create_split(token.proofs)?;
  124. let split_response = self
  125. .client
  126. .post_split(
  127. self.mint_url.clone().try_into()?,
  128. split_payload.split_payload,
  129. )
  130. .await?;
  131. if let Some(promises) = &split_response.promises {
  132. // Proof to keep
  133. let p = construct_proofs(
  134. promises.to_owned(),
  135. split_payload.blinded_messages.rs,
  136. split_payload.blinded_messages.secrets,
  137. &keys,
  138. )?;
  139. proofs.push(p);
  140. } else {
  141. warn!("Response missing promises");
  142. return Err(Error::Custom("Split response missing promises".to_string()));
  143. }
  144. }
  145. Ok(proofs.iter().flatten().cloned().collect())
  146. }
  147. /// Create Split Payload
  148. fn create_split(&self, proofs: Proofs) -> Result<SplitPayload, Error> {
  149. let value = proofs.iter().map(|p| p.amount).sum();
  150. let blinded_messages = BlindedMessages::random(value)?;
  151. let split_payload = SplitRequest::new(proofs, blinded_messages.blinded_messages.clone());
  152. Ok(SplitPayload {
  153. blinded_messages,
  154. split_payload,
  155. })
  156. }
  157. pub fn process_split_response(
  158. &self,
  159. blinded_messages: BlindedMessages,
  160. promises: Vec<BlindedSignature>,
  161. ) -> Result<Proofs, Error> {
  162. let BlindedMessages {
  163. blinded_messages: _,
  164. secrets,
  165. rs,
  166. amounts: _,
  167. } = blinded_messages;
  168. let secrets: Vec<_> = secrets.iter().collect();
  169. let mut proofs = vec![];
  170. for (i, promise) in promises.iter().enumerate() {
  171. let a = self
  172. .mint_keys
  173. .amount_key(promise.amount)
  174. .unwrap()
  175. .to_owned();
  176. let blinded_c = promise.c.clone();
  177. let unblinded_sig = unblind_message(blinded_c, rs[i].clone().into(), a).unwrap();
  178. let proof = Proof {
  179. id: Some(promise.id),
  180. amount: promise.amount,
  181. secret: secrets[i].clone(),
  182. c: unblinded_sig,
  183. };
  184. proofs.push(proof);
  185. }
  186. Ok(proofs)
  187. }
  188. /// Send
  189. pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> {
  190. let mut amount_available = Amount::ZERO;
  191. let mut send_proofs = SendProofs::default();
  192. for proof in proofs {
  193. let proof_value = proof.amount;
  194. if amount_available > amount {
  195. send_proofs.change_proofs.push(proof);
  196. } else {
  197. send_proofs.send_proofs.push(proof);
  198. }
  199. amount_available += proof_value;
  200. }
  201. if amount_available.lt(&amount) {
  202. println!("Not enough funds");
  203. return Err(Error::InsufficientFunds);
  204. }
  205. // If amount available is EQUAL to send amount no need to split
  206. if amount_available.eq(&amount) {
  207. return Ok(send_proofs);
  208. }
  209. let _amount_to_keep = amount_available - amount;
  210. let amount_to_send = amount;
  211. let split_payload = self.create_split(send_proofs.send_proofs)?;
  212. let split_response = self
  213. .client
  214. .post_split(
  215. self.mint_url.clone().try_into()?,
  216. split_payload.split_payload,
  217. )
  218. .await?;
  219. // If only promises assemble proofs needed for amount
  220. let keep_proofs;
  221. let send_proofs;
  222. if let Some(promises) = split_response.promises {
  223. let proofs = construct_proofs(
  224. promises,
  225. split_payload.blinded_messages.rs,
  226. split_payload.blinded_messages.secrets,
  227. &self.mint_keys,
  228. )?;
  229. let split = amount_to_send.split();
  230. keep_proofs = proofs[0..split.len()].to_vec();
  231. send_proofs = proofs[split.len()..].to_vec();
  232. } else {
  233. return Err(Error::Custom("Invalid split response".to_string()));
  234. }
  235. // println!("Send Proofs: {:#?}", send_proofs);
  236. // println!("Keep Proofs: {:#?}", keep_proofs);
  237. Ok(SendProofs {
  238. change_proofs: keep_proofs,
  239. send_proofs,
  240. })
  241. }
  242. pub async fn melt(
  243. &self,
  244. invoice: Bolt11Invoice,
  245. proofs: Proofs,
  246. fee_reserve: Amount,
  247. ) -> Result<Melted, Error> {
  248. let blinded = BlindedMessages::blank(fee_reserve)?;
  249. let melt_response = self
  250. .client
  251. .post_melt(
  252. self.mint_url.clone().try_into()?,
  253. proofs,
  254. invoice,
  255. Some(blinded.blinded_messages),
  256. )
  257. .await?;
  258. let change_proofs = match melt_response.change {
  259. Some(change) => Some(construct_proofs(
  260. change,
  261. blinded.rs,
  262. blinded.secrets,
  263. &self.mint_keys,
  264. )?),
  265. None => None,
  266. };
  267. let melted = Melted {
  268. paid: true,
  269. preimage: melt_response.preimage,
  270. change: change_proofs,
  271. };
  272. Ok(melted)
  273. }
  274. pub fn proofs_to_token(&self, proofs: Proofs, memo: Option<String>) -> Result<String, Error> {
  275. Ok(Token::new(self.mint_url.clone(), proofs, memo)?.convert_to_string()?)
  276. }
  277. }
  278. /*
  279. #[cfg(test)]
  280. mod tests {
  281. use std::collections::{HashMap, HashSet};
  282. use super::*;
  283. use crate::client::Client;
  284. use crate::mint::Mint;
  285. use cashu::nuts::nut04;
  286. #[test]
  287. fn test_wallet() {
  288. let mut mint = Mint::new(
  289. "supersecretsecret",
  290. "0/0/0/0",
  291. HashMap::new(),
  292. HashSet::new(),
  293. 32,
  294. );
  295. let keys = mint.active_keyset_pubkeys();
  296. let client = Client::new("https://cashu-rs.thesimplekid.space/").unwrap();
  297. let wallet = Wallet::new(client, keys.keys);
  298. let blinded_messages = BlindedMessages::random(Amount::from_sat(64)).unwrap();
  299. let mint_request = nut04::MintRequest {
  300. outputs: blinded_messages.blinded_messages.clone(),
  301. };
  302. let res = mint.process_mint_request(mint_request).unwrap();
  303. let proofs = wallet
  304. .process_split_response(blinded_messages, res.promises)
  305. .unwrap();
  306. for proof in &proofs {
  307. mint.verify_proof(proof).unwrap();
  308. }
  309. let split = wallet.create_split(proofs.clone()).unwrap();
  310. let split_request = split.split_payload;
  311. let split_response = mint.process_split_request(split_request).unwrap();
  312. let p = split_response.promises;
  313. let snd_proofs = wallet
  314. .process_split_response(split.blinded_messages, p.unwrap())
  315. .unwrap();
  316. let mut error = false;
  317. for proof in &snd_proofs {
  318. if let Err(err) = mint.verify_proof(proof) {
  319. println!("{err}{:?}", serde_json::to_string(proof));
  320. error = true;
  321. }
  322. }
  323. if error {
  324. panic!()
  325. }
  326. }
  327. }
  328. */