mod.rs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. //! Client to connet to mint
  2. use async_trait::async_trait;
  3. #[cfg(feature = "mint")]
  4. use cashu::nuts::nut00;
  5. #[cfg(feature = "nut07")]
  6. use cashu::nuts::CheckSpendableResponse;
  7. #[cfg(feature = "nut09")]
  8. use cashu::nuts::MintInfo;
  9. use cashu::nuts::{
  10. BlindedMessage, Keys, KeysetResponse, MeltBolt11Response, MintBolt11Response, PreMintSecrets,
  11. Proof, SwapRequest, SwapResponse,
  12. };
  13. use cashu::utils;
  14. use serde::{Deserialize, Serialize};
  15. use thiserror::Error;
  16. use url::Url;
  17. #[cfg(feature = "gloo")]
  18. pub mod gloo_client;
  19. #[cfg(not(target_arch = "wasm32"))]
  20. pub mod minreq_client;
  21. pub use cashu::Bolt11Invoice;
  22. #[derive(Debug, Error)]
  23. pub enum Error {
  24. #[error("Invoice not paid")]
  25. InvoiceNotPaid,
  26. #[error("Wallet not responding")]
  27. LightingWalletNotResponding(Option<String>),
  28. /// Parse Url Error
  29. #[error("`{0}`")]
  30. UrlParse(#[from] url::ParseError),
  31. /// Serde Json error
  32. #[error("`{0}`")]
  33. SerdeJson(#[from] serde_json::Error),
  34. /// Cashu Url Error
  35. #[error("`{0}`")]
  36. CashuUrl(#[from] cashu::url::Error),
  37. /// Min req error
  38. #[cfg(not(target_arch = "wasm32"))]
  39. #[error("`{0}`")]
  40. MinReq(#[from] minreq::Error),
  41. #[cfg(feature = "gloo")]
  42. #[error("`{0}`")]
  43. Gloo(String),
  44. /// Custom Error
  45. #[error("`{0}`")]
  46. Custom(String),
  47. }
  48. impl Error {
  49. pub fn from_json(json: &str) -> Result<Self, Error> {
  50. if let Ok(mint_res) = serde_json::from_str::<MintErrorResponse>(json) {
  51. let err = mint_res
  52. .error
  53. .as_deref()
  54. .or(mint_res.detail.as_deref())
  55. .unwrap_or_default();
  56. let mint_error = match err {
  57. error if error.starts_with("Lightning invoice not paid yet.") => {
  58. Error::InvoiceNotPaid
  59. }
  60. error if error.starts_with("Lightning wallet not responding") => {
  61. let mint = utils::extract_url_from_error(error);
  62. Error::LightingWalletNotResponding(mint)
  63. }
  64. error => Error::Custom(error.to_owned()),
  65. };
  66. Ok(mint_error)
  67. } else {
  68. Ok(Error::Custom(json.to_string()))
  69. }
  70. }
  71. }
  72. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  73. pub struct MintErrorResponse {
  74. code: u32,
  75. error: Option<String>,
  76. detail: Option<String>,
  77. }
  78. #[async_trait(?Send)]
  79. pub trait Client {
  80. async fn get_mint_keys(&self, mint_url: Url) -> Result<Keys, Error>;
  81. async fn get_mint_keysets(&self, mint_url: Url) -> Result<KeysetResponse, Error>;
  82. // TODO: Hash should have a type
  83. async fn post_mint(
  84. &self,
  85. mint_url: Url,
  86. quote: &str,
  87. premint_secrets: PreMintSecrets,
  88. ) -> Result<MintBolt11Response, Error>;
  89. async fn post_melt(
  90. &self,
  91. mint_url: Url,
  92. quote: String,
  93. inputs: Vec<Proof>,
  94. outputs: Option<Vec<BlindedMessage>>,
  95. ) -> Result<MeltBolt11Response, Error>;
  96. // REVIEW: Should be consistent aboue passing in the Request struct or the
  97. // compnatants and making it within the function. Here the struct is passed
  98. // in but in check spendable and melt the compants are passed in
  99. async fn post_split(
  100. &self,
  101. mint_url: Url,
  102. split_request: SwapRequest,
  103. ) -> Result<SwapResponse, Error>;
  104. #[cfg(feature = "nut07")]
  105. async fn post_check_spendable(
  106. &self,
  107. mint_url: Url,
  108. proofs: Vec<nut00::mint::Proof>,
  109. ) -> Result<CheckSpendableResponse, Error>;
  110. #[cfg(feature = "nut09")]
  111. async fn get_mint_info(&self, mint_url: Url) -> Result<MintInfo, Error>;
  112. }
  113. #[cfg(any(not(target_arch = "wasm32"), feature = "gloo"))]
  114. fn join_url(url: Url, path: &str) -> Result<Url, Error> {
  115. let mut url = url;
  116. if !url.path().ends_with('/') {
  117. url.path_segments_mut()
  118. .map_err(|_| Error::Custom("Url Path Segmants".to_string()))?
  119. .push(path);
  120. } else {
  121. url.path_segments_mut()
  122. .map_err(|_| Error::Custom("Url Path Segmants".to_string()))?
  123. .pop()
  124. .push(path);
  125. }
  126. Ok(url)
  127. }
  128. #[cfg(test)]
  129. mod tests {
  130. use super::*;
  131. #[test]
  132. fn test_decode_error() {
  133. let err = r#"{"code":0,"error":"Lightning invoice not paid yet."}"#;
  134. let error = Error::from_json(err).unwrap();
  135. match error {
  136. Error::InvoiceNotPaid => {}
  137. _ => panic!("Wrong error"),
  138. }
  139. let err = r#"{"code": 0, "error": "Lightning wallet not responding: Failed to connect to https://legend.lnbits.com due to: All connection attempts failed"}"#;
  140. let error = Error::from_json(err).unwrap();
  141. match error {
  142. Error::LightingWalletNotResponding(mint) => {
  143. assert_eq!(mint, Some("https://legend.lnbits.com".to_string()));
  144. }
  145. _ => panic!("Wrong error"),
  146. }
  147. }
  148. }