mod.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. //! Client to connet to mint
  2. use std::fmt;
  3. use std::str::FromStr;
  4. use cashu::url::UncheckedUrl;
  5. use serde::{Deserialize, Serialize};
  6. use serde_json::Value;
  7. use url::Url;
  8. use cashu::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof};
  9. use cashu::nuts::nut01::Keys;
  10. use cashu::nuts::nut03::RequestMintResponse;
  11. use cashu::nuts::nut04::{MintRequest, PostMintResponse};
  12. use cashu::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
  13. use cashu::nuts::nut06::{SplitRequest, SplitResponse};
  14. use cashu::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
  15. use cashu::nuts::nut08::{MeltRequest, MeltResponse};
  16. use cashu::nuts::nut09::MintInfo;
  17. use cashu::nuts::*;
  18. use cashu::utils;
  19. use cashu::Amount;
  20. #[cfg(target_arch = "wasm32")]
  21. use gloo::net::http::Request;
  22. #[cfg(feature = "blocking")]
  23. pub mod blocking;
  24. pub use cashu::Bolt11Invoice;
  25. #[derive(Debug)]
  26. pub enum Error {
  27. InvoiceNotPaid,
  28. LightingWalletNotResponding(Option<String>),
  29. /// Parse Url Error
  30. UrlParse(url::ParseError),
  31. /// Serde Json error
  32. SerdeJson(serde_json::Error),
  33. /// Cashu Url Error
  34. CashuUrl(cashu::url::Error),
  35. /// Min req error
  36. #[cfg(not(target_arch = "wasm32"))]
  37. MinReq(minreq::Error),
  38. #[cfg(target_arch = "wasm32")]
  39. Gloo(String),
  40. /// Custom Error
  41. Custom(String),
  42. }
  43. impl From<url::ParseError> for Error {
  44. fn from(err: url::ParseError) -> Error {
  45. Error::UrlParse(err)
  46. }
  47. }
  48. impl From<serde_json::Error> for Error {
  49. fn from(err: serde_json::Error) -> Error {
  50. Error::SerdeJson(err)
  51. }
  52. }
  53. impl From<cashu::url::Error> for Error {
  54. fn from(err: cashu::url::Error) -> Error {
  55. Error::CashuUrl(err)
  56. }
  57. }
  58. #[cfg(not(target_arch = "wasm32"))]
  59. impl From<minreq::Error> for Error {
  60. fn from(err: minreq::Error) -> Error {
  61. Error::MinReq(err)
  62. }
  63. }
  64. impl std::error::Error for Error {}
  65. impl fmt::Display for Error {
  66. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  67. match self {
  68. Error::InvoiceNotPaid => write!(f, "Invoice not paid"),
  69. Error::LightingWalletNotResponding(mint) => {
  70. write!(
  71. f,
  72. "Lightning Wallet not responding: {}",
  73. mint.clone().unwrap_or("".to_string())
  74. )
  75. }
  76. Error::UrlParse(err) => write!(f, "{}", err),
  77. Error::SerdeJson(err) => write!(f, "{}", err),
  78. #[cfg(not(target_arch = "wasm32"))]
  79. Error::MinReq(err) => write!(f, "{}", err),
  80. Error::CashuUrl(err) => write!(f, "{}", err),
  81. #[cfg(target_arch = "wasm32")]
  82. Error::Gloo(err) => write!(f, "{}", err),
  83. Error::Custom(message) => write!(f, "{}", message),
  84. }
  85. }
  86. }
  87. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  88. pub struct MintErrorResponse {
  89. code: u32,
  90. error: Option<String>,
  91. detail: Option<String>,
  92. }
  93. impl Error {
  94. pub fn from_json(json: &str) -> Result<Self, Error> {
  95. let mint_res: MintErrorResponse = serde_json::from_str(json)?;
  96. let err = mint_res
  97. .error
  98. .as_deref()
  99. .or(mint_res.detail.as_deref())
  100. .unwrap_or_default();
  101. let mint_error = match err {
  102. error if error.starts_with("Lightning invoice not paid yet.") => Error::InvoiceNotPaid,
  103. error if error.starts_with("Lightning wallet not responding") => {
  104. let mint = utils::extract_url_from_error(error);
  105. Error::LightingWalletNotResponding(mint)
  106. }
  107. error => Error::Custom(error.to_owned()),
  108. };
  109. Ok(mint_error)
  110. }
  111. }
  112. #[derive(Debug, Clone)]
  113. pub struct Client {
  114. pub mint_url: UncheckedUrl,
  115. }
  116. impl Client {
  117. pub fn new(mint_url: &str) -> Result<Self, Error> {
  118. let mint_url = UncheckedUrl::from_str(mint_url)?;
  119. let _: Url = (&mint_url).try_into()?;
  120. Ok(Self { mint_url })
  121. }
  122. /// Get Mint Keys [NUT-01]
  123. #[cfg(not(target_arch = "wasm32"))]
  124. pub async fn get_keys(&self) -> Result<Keys, Error> {
  125. let url = self.mint_url.join("keys")?;
  126. let keys = minreq::get(url).send()?.json::<Value>()?;
  127. let keys: Keys = serde_json::from_str(&keys.to_string())?;
  128. /*
  129. let keys: BTreeMap<u64, String> = match serde_json::from_value(keys.clone()) {
  130. Ok(keys) => keys,
  131. Err(_err) => {
  132. return Err(Error::CustomError(format!(
  133. "url: {}, {}",
  134. url,
  135. serde_json::to_string(&keys)?
  136. )))
  137. }
  138. };
  139. let mint_keys: BTreeMap<u64, PublicKey> = keys
  140. .into_iter()
  141. .filter_map(|(k, v)| {
  142. let key = hex::decode(v).ok()?;
  143. let public_key = PublicKey::from_sec1_bytes(&key).ok()?;
  144. Some((k, public_key))
  145. })
  146. .collect();
  147. */
  148. Ok(keys)
  149. }
  150. /// Get Mint Keys [NUT-01]
  151. #[cfg(target_arch = "wasm32")]
  152. pub async fn get_keys(&self) -> Result<Keys, Error> {
  153. let url = self.mint_url.join("keys")?;
  154. let keys = Request::get(url.as_str())
  155. .send()
  156. .await
  157. .map_err(|err| Error::Gloo(err.to_string()))?
  158. .json::<Value>()
  159. .await
  160. .map_err(|err| Error::Gloo(err.to_string()))?;
  161. let keys: Keys = serde_json::from_str(&keys.to_string())?;
  162. /*
  163. let keys: BTreeMap<u64, String> = match serde_json::from_value(keys.clone()) {
  164. Ok(keys) => keys,
  165. Err(_err) => {
  166. return Err(Error::CustomError(format!(
  167. "url: {}, {}",
  168. url,
  169. serde_json::to_string(&keys)?
  170. )))
  171. }
  172. };
  173. let mint_keys: BTreeMap<u64, PublicKey> = keys
  174. .into_iter()
  175. .filter_map(|(k, v)| {
  176. let key = hex::decode(v).ok()?;
  177. let public_key = PublicKey::from_sec1_bytes(&key).ok()?;
  178. Some((k, public_key))
  179. })
  180. .collect();
  181. */
  182. Ok(keys)
  183. }
  184. /// Get Keysets [NUT-02]
  185. #[cfg(not(target_arch = "wasm32"))]
  186. pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
  187. let url = self.mint_url.join("keysets")?;
  188. let res = minreq::get(url).send()?.json::<Value>()?;
  189. let response: Result<nut02::Response, serde_json::Error> =
  190. serde_json::from_value(res.clone());
  191. match response {
  192. Ok(res) => Ok(res),
  193. Err(_) => Err(Error::from_json(&res.to_string())?),
  194. }
  195. }
  196. /// Get Keysets [NUT-02]
  197. #[cfg(target_arch = "wasm32")]
  198. pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
  199. let url = self.mint_url.join("keysets")?;
  200. let res = Request::get(url.as_str())
  201. .send()
  202. .await
  203. .map_err(|err| Error::Gloo(err.to_string()))?
  204. .json::<Value>()
  205. .await
  206. .map_err(|err| Error::Gloo(err.to_string()))?;
  207. let response: Result<nut02::Response, serde_json::Error> =
  208. serde_json::from_value(res.clone());
  209. match response {
  210. Ok(res) => Ok(res),
  211. Err(_) => Err(Error::from_json(&res.to_string())?),
  212. }
  213. }
  214. /// Request Mint [NUT-03]
  215. #[cfg(not(target_arch = "wasm32"))]
  216. pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
  217. let mut url = self.mint_url.join("mint")?;
  218. url.query_pairs_mut()
  219. .append_pair("amount", &amount.to_sat().to_string());
  220. let res = minreq::get(url).send()?.json::<Value>()?;
  221. let response: Result<RequestMintResponse, serde_json::Error> =
  222. serde_json::from_value(res.clone());
  223. match response {
  224. Ok(res) => Ok(res),
  225. Err(_) => Err(Error::from_json(&res.to_string())?),
  226. }
  227. }
  228. /// Request Mint [NUT-03]
  229. #[cfg(target_arch = "wasm32")]
  230. pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
  231. let mut url = self.mint_url.join("mint")?;
  232. url.query_pairs_mut()
  233. .append_pair("amount", &amount.to_sat().to_string());
  234. let res = Request::get(url.as_str())
  235. .send()
  236. .await
  237. .map_err(|err| Error::Gloo(err.to_string()))?
  238. .json::<Value>()
  239. .await
  240. .map_err(|err| Error::Gloo(err.to_string()))?;
  241. let response: Result<RequestMintResponse, serde_json::Error> =
  242. serde_json::from_value(res.clone());
  243. match response {
  244. Ok(res) => Ok(res),
  245. Err(_) => Err(Error::from_json(&res.to_string())?),
  246. }
  247. }
  248. /// Mint Tokens [NUT-04]
  249. #[cfg(not(target_arch = "wasm32"))]
  250. pub async fn mint(
  251. &self,
  252. blinded_messages: BlindedMessages,
  253. hash: &str,
  254. ) -> Result<PostMintResponse, Error> {
  255. let mut url = self.mint_url.join("mint")?;
  256. url.query_pairs_mut().append_pair("hash", hash);
  257. let request = MintRequest {
  258. outputs: blinded_messages.blinded_messages,
  259. };
  260. let res = minreq::post(url)
  261. .with_json(&request)?
  262. .send()?
  263. .json::<Value>()?;
  264. let response: Result<PostMintResponse, serde_json::Error> =
  265. serde_json::from_value(res.clone());
  266. match response {
  267. Ok(res) => Ok(res),
  268. Err(_) => Err(Error::from_json(&res.to_string())?),
  269. }
  270. }
  271. /// Mint Tokens [NUT-04]
  272. #[cfg(target_arch = "wasm32")]
  273. pub async fn mint(
  274. &self,
  275. blinded_messages: BlindedMessages,
  276. hash: &str,
  277. ) -> Result<PostMintResponse, Error> {
  278. let mut url = self.mint_url.join("mint")?;
  279. url.query_pairs_mut().append_pair("hash", hash);
  280. let request = MintRequest {
  281. outputs: blinded_messages.blinded_messages,
  282. };
  283. let res = Request::post(url.as_str())
  284. .json(&request)
  285. .map_err(|err| Error::Gloo(err.to_string()))?
  286. .send()
  287. .await
  288. .map_err(|err| Error::Gloo(err.to_string()))?
  289. .json::<Value>()
  290. .await
  291. .map_err(|err| Error::Gloo(err.to_string()))?;
  292. let response: Result<PostMintResponse, serde_json::Error> =
  293. serde_json::from_value(res.clone());
  294. match response {
  295. Ok(res) => Ok(res),
  296. Err(_) => Err(Error::from_json(&res.to_string())?),
  297. }
  298. }
  299. /// Check Max expected fee [NUT-05]
  300. #[cfg(not(target_arch = "wasm32"))]
  301. pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
  302. let url = self.mint_url.join("checkfees")?;
  303. let request = CheckFeesRequest { pr: invoice };
  304. let res = minreq::post(url)
  305. .with_json(&request)?
  306. .send()?
  307. .json::<Value>()?;
  308. let response: Result<CheckFeesResponse, serde_json::Error> =
  309. serde_json::from_value(res.clone());
  310. match response {
  311. Ok(res) => Ok(res),
  312. Err(_) => Err(Error::from_json(&res.to_string())?),
  313. }
  314. }
  315. /// Check Max expected fee [NUT-05]
  316. #[cfg(target_arch = "wasm32")]
  317. pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
  318. let url = self.mint_url.join("checkfees")?;
  319. let request = CheckFeesRequest { pr: invoice };
  320. let res = Request::post(url.as_str())
  321. .json(&request)
  322. .map_err(|err| Error::Gloo(err.to_string()))?
  323. .send()
  324. .await
  325. .map_err(|err| Error::Gloo(err.to_string()))?
  326. .json::<Value>()
  327. .await
  328. .map_err(|err| Error::Gloo(err.to_string()))?;
  329. let response: Result<CheckFeesResponse, serde_json::Error> =
  330. serde_json::from_value(res.clone());
  331. match response {
  332. Ok(res) => Ok(res),
  333. Err(_) => Err(Error::from_json(&res.to_string())?),
  334. }
  335. }
  336. /// Melt [NUT-05]
  337. /// [Nut-08] Lightning fee return if outputs defined
  338. #[cfg(not(target_arch = "wasm32"))]
  339. pub async fn melt(
  340. &self,
  341. proofs: Vec<Proof>,
  342. invoice: Bolt11Invoice,
  343. outputs: Option<Vec<BlindedMessage>>,
  344. ) -> Result<MeltResponse, Error> {
  345. let url = self.mint_url.join("melt")?;
  346. let request = MeltRequest {
  347. proofs,
  348. pr: invoice,
  349. outputs,
  350. };
  351. let value = minreq::post(url)
  352. .with_json(&request)?
  353. .send()?
  354. .json::<Value>()?;
  355. let response: Result<MeltResponse, serde_json::Error> =
  356. serde_json::from_value(value.clone());
  357. match response {
  358. Ok(res) => Ok(res),
  359. Err(_) => Err(Error::from_json(&value.to_string())?),
  360. }
  361. }
  362. /// Melt [NUT-05]
  363. /// [Nut-08] Lightning fee return if outputs defined
  364. #[cfg(target_arch = "wasm32")]
  365. pub async fn melt(
  366. &self,
  367. proofs: Vec<Proof>,
  368. invoice: Bolt11Invoice,
  369. outputs: Option<Vec<BlindedMessage>>,
  370. ) -> Result<MeltResponse, Error> {
  371. let url = self.mint_url.join("melt")?;
  372. let request = MeltRequest {
  373. proofs,
  374. pr: invoice,
  375. outputs,
  376. };
  377. let value = Request::post(url.as_str())
  378. .json(&request)
  379. .map_err(|err| Error::Gloo(err.to_string()))?
  380. .send()
  381. .await
  382. .map_err(|err| Error::Gloo(err.to_string()))?
  383. .json::<Value>()
  384. .await
  385. .map_err(|err| Error::Gloo(err.to_string()))?;
  386. let response: Result<MeltResponse, serde_json::Error> =
  387. serde_json::from_value(value.clone());
  388. match response {
  389. Ok(res) => Ok(res),
  390. Err(_) => Err(Error::from_json(&value.to_string())?),
  391. }
  392. }
  393. /// Split Token [NUT-06]
  394. #[cfg(not(target_arch = "wasm32"))]
  395. pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
  396. let url = self.mint_url.join("split")?;
  397. let res = minreq::post(url)
  398. .with_json(&split_request)?
  399. .send()?
  400. .json::<Value>()?;
  401. let response: Result<SplitResponse, serde_json::Error> =
  402. serde_json::from_value(res.clone());
  403. match response {
  404. Ok(res) if res.promises.is_some() => Ok(res),
  405. _ => Err(Error::from_json(&res.to_string())?),
  406. }
  407. }
  408. /// Split Token [NUT-06]
  409. #[cfg(target_arch = "wasm32")]
  410. pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
  411. let url = self.mint_url.join("split")?;
  412. let res = Request::post(url.as_str())
  413. .json(&split_request)
  414. .map_err(|err| Error::Gloo(err.to_string()))?
  415. .send()
  416. .await
  417. .map_err(|err| Error::Gloo(err.to_string()))?
  418. .json::<Value>()
  419. .await
  420. .map_err(|err| Error::Gloo(err.to_string()))?;
  421. let response: Result<SplitResponse, serde_json::Error> =
  422. serde_json::from_value(res.clone());
  423. match response {
  424. Ok(res) => Ok(res),
  425. Err(_) => Err(Error::from_json(&res.to_string())?),
  426. }
  427. }
  428. /// Spendable check [NUT-07]
  429. #[cfg(not(target_arch = "wasm32"))]
  430. pub async fn check_spendable(
  431. &self,
  432. proofs: &Vec<nut00::mint::Proof>,
  433. ) -> Result<CheckSpendableResponse, Error> {
  434. let url = self.mint_url.join("check")?;
  435. let request = CheckSpendableRequest {
  436. proofs: proofs.to_owned(),
  437. };
  438. let res = minreq::post(url)
  439. .with_json(&request)?
  440. .send()?
  441. .json::<Value>()?;
  442. let response: Result<CheckSpendableResponse, serde_json::Error> =
  443. serde_json::from_value(res.clone());
  444. match response {
  445. Ok(res) => Ok(res),
  446. Err(_) => Err(Error::from_json(&res.to_string())?),
  447. }
  448. }
  449. /// Spendable check [NUT-07]
  450. #[cfg(target_arch = "wasm32")]
  451. pub async fn check_spendable(
  452. &self,
  453. proofs: &Vec<nut00::mint::Proof>,
  454. ) -> Result<CheckSpendableResponse, Error> {
  455. let url = self.mint_url.join("check")?;
  456. let request = CheckSpendableRequest {
  457. proofs: proofs.to_owned(),
  458. };
  459. let res = Request::post(url.as_str())
  460. .json(&request)
  461. .map_err(|err| Error::Gloo(err.to_string()))?
  462. .send()
  463. .await
  464. .map_err(|err| Error::Gloo(err.to_string()))?
  465. .json::<Value>()
  466. .await
  467. .map_err(|err| Error::Gloo(err.to_string()))?;
  468. let response: Result<CheckSpendableResponse, serde_json::Error> =
  469. serde_json::from_value(res.clone());
  470. match response {
  471. Ok(res) => Ok(res),
  472. Err(_) => Err(Error::from_json(&res.to_string())?),
  473. }
  474. }
  475. /// Get Mint Info [NUT-09]
  476. #[cfg(not(target_arch = "wasm32"))]
  477. pub async fn get_info(&self) -> Result<MintInfo, Error> {
  478. let url = self.mint_url.join("info")?;
  479. let res = minreq::get(url).send()?.json::<Value>()?;
  480. let response: Result<MintInfo, serde_json::Error> = serde_json::from_value(res.clone());
  481. match response {
  482. Ok(res) => Ok(res),
  483. Err(_) => Err(Error::from_json(&res.to_string())?),
  484. }
  485. }
  486. /// Get Mint Info [NUT-09]
  487. #[cfg(target_arch = "wasm32")]
  488. pub async fn get_info(&self) -> Result<MintInfo, Error> {
  489. let url = self.mint_url.join("info")?;
  490. let res = Request::get(url.as_str())
  491. .send()
  492. .await
  493. .map_err(|err| Error::Gloo(err.to_string()))?
  494. .json::<Value>()
  495. .await
  496. .map_err(|err| Error::Gloo(err.to_string()))?;
  497. let response: Result<MintInfo, serde_json::Error> = serde_json::from_value(res.clone());
  498. match response {
  499. Ok(res) => Ok(res),
  500. Err(_) => Err(Error::from_json(&res.to_string())?),
  501. }
  502. }
  503. }
  504. #[cfg(test)]
  505. mod tests {
  506. use super::*;
  507. #[test]
  508. fn test_decode_error() {
  509. let err = r#"{"code":0,"error":"Lightning invoice not paid yet."}"#;
  510. let error = Error::from_json(err).unwrap();
  511. match error {
  512. Error::InvoiceNotPaid => {}
  513. _ => panic!("Wrong error"),
  514. }
  515. let err = r#"{"code": 0, "error": "Lightning wallet not responding: Failed to connect to https://legend.lnbits.com due to: All connection attempts failed"}"#;
  516. let error = Error::from_json(err).unwrap();
  517. match error {
  518. Error::LightingWalletNotResponding(mint) => {
  519. assert_eq!(mint, Some("https://legend.lnbits.com".to_string()));
  520. }
  521. _ => panic!("Wrong error"),
  522. }
  523. }
  524. }