mod.rs 19 KB

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