client.rs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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 crate::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof};
  7. use crate::nuts::nut01::Keys;
  8. use crate::nuts::nut03::RequestMintResponse;
  9. use crate::nuts::nut04::{MintRequest, PostMintResponse};
  10. use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
  11. use crate::nuts::nut06::{SplitRequest, SplitResponse};
  12. use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
  13. use crate::nuts::nut08::{MeltRequest, MeltResponse};
  14. use crate::nuts::nut09::MintInfo;
  15. use crate::nuts::*;
  16. use crate::utils;
  17. use crate::Amount;
  18. pub use crate::Bolt11Invoice;
  19. #[derive(Debug)]
  20. pub enum Error {
  21. InvoiceNotPaid,
  22. LightingWalletNotResponding(Option<String>),
  23. /// Parse Url Error
  24. UrlParseError(url::ParseError),
  25. /// Serde Json error
  26. SerdeJsonError(serde_json::Error),
  27. /// Min req error
  28. MinReqError(minreq::Error),
  29. /// Custom Error
  30. Custom(String),
  31. }
  32. impl From<url::ParseError> for Error {
  33. fn from(err: url::ParseError) -> Error {
  34. Error::UrlParseError(err)
  35. }
  36. }
  37. impl From<serde_json::Error> for Error {
  38. fn from(err: serde_json::Error) -> Error {
  39. Error::SerdeJsonError(err)
  40. }
  41. }
  42. impl From<minreq::Error> for Error {
  43. fn from(err: minreq::Error) -> Error {
  44. Error::MinReqError(err)
  45. }
  46. }
  47. impl std::error::Error for Error {}
  48. impl fmt::Display for Error {
  49. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  50. match self {
  51. Error::InvoiceNotPaid => write!(f, "Invoice not paid"),
  52. Error::LightingWalletNotResponding(mint) => {
  53. write!(
  54. f,
  55. "Lightning Wallet not responding: {}",
  56. mint.clone().unwrap_or("".to_string())
  57. )
  58. }
  59. Error::UrlParseError(err) => write!(f, "{}", err),
  60. Error::SerdeJsonError(err) => write!(f, "{}", err),
  61. Error::MinReqError(err) => write!(f, "{}", err),
  62. Error::Custom(message) => write!(f, "{}", message),
  63. }
  64. }
  65. }
  66. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  67. pub struct MintErrorResponse {
  68. code: u32,
  69. error: String,
  70. }
  71. impl Error {
  72. pub fn from_json(json: &str) -> Result<Self, Error> {
  73. let mint_res: MintErrorResponse = serde_json::from_str(json)?;
  74. let mint_error = match mint_res.error {
  75. error if error.starts_with("Lightning invoice not paid yet.") => Error::InvoiceNotPaid,
  76. error if error.starts_with("Lightning wallet not responding") => {
  77. let mint = utils::extract_url_from_error(&error);
  78. Error::LightingWalletNotResponding(mint)
  79. }
  80. error => Error::Custom(error),
  81. };
  82. Ok(mint_error)
  83. }
  84. }
  85. #[derive(Debug, Clone)]
  86. pub struct Client {
  87. pub mint_url: Url,
  88. }
  89. impl Client {
  90. pub fn new(mint_url: &str) -> Result<Self, Error> {
  91. // HACK
  92. let mut mint_url = String::from(mint_url);
  93. if !mint_url.ends_with('/') {
  94. mint_url.push('/');
  95. }
  96. let mint_url = Url::parse(&mint_url)?;
  97. Ok(Self { mint_url })
  98. }
  99. /// Get Mint Keys [NUT-01]
  100. pub async fn get_keys(&self) -> Result<Keys, Error> {
  101. let url = self.mint_url.join("keys")?;
  102. let keys = minreq::get(url).send()?.json::<Value>()?;
  103. let keys: Keys = serde_json::from_str(&keys.to_string())?;
  104. /*
  105. let keys: BTreeMap<u64, String> = match serde_json::from_value(keys.clone()) {
  106. Ok(keys) => keys,
  107. Err(_err) => {
  108. return Err(Error::CustomError(format!(
  109. "url: {}, {}",
  110. url,
  111. serde_json::to_string(&keys)?
  112. )))
  113. }
  114. };
  115. let mint_keys: BTreeMap<u64, PublicKey> = keys
  116. .into_iter()
  117. .filter_map(|(k, v)| {
  118. let key = hex::decode(v).ok()?;
  119. let public_key = PublicKey::from_sec1_bytes(&key).ok()?;
  120. Some((k, public_key))
  121. })
  122. .collect();
  123. */
  124. Ok(keys)
  125. }
  126. /// Get Keysets [NUT-02]
  127. pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
  128. let url = self.mint_url.join("keysets")?;
  129. let res = minreq::get(url).send()?.json::<Value>()?;
  130. let response: Result<nut02::Response, serde_json::Error> =
  131. serde_json::from_value(res.clone());
  132. match response {
  133. Ok(res) => Ok(res),
  134. Err(_) => Err(Error::from_json(&res.to_string())?),
  135. }
  136. }
  137. /// Request Mint [NUT-03]
  138. pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
  139. let mut url = self.mint_url.join("mint")?;
  140. url.query_pairs_mut()
  141. .append_pair("amount", &amount.to_sat().to_string());
  142. let res = minreq::get(url).send()?.json::<Value>()?;
  143. let response: Result<RequestMintResponse, serde_json::Error> =
  144. serde_json::from_value(res.clone());
  145. match response {
  146. Ok(res) => Ok(res),
  147. Err(_) => Err(Error::from_json(&res.to_string())?),
  148. }
  149. }
  150. /// Mint Tokens [NUT-04]
  151. pub async fn mint(
  152. &self,
  153. blinded_messages: BlindedMessages,
  154. hash: &str,
  155. ) -> Result<PostMintResponse, Error> {
  156. let mut url = self.mint_url.join("mint")?;
  157. url.query_pairs_mut().append_pair("hash", hash);
  158. let request = MintRequest {
  159. outputs: blinded_messages.blinded_messages,
  160. };
  161. let res = minreq::post(url)
  162. .with_json(&request)?
  163. .send()?
  164. .json::<Value>()?;
  165. let response: Result<PostMintResponse, serde_json::Error> =
  166. serde_json::from_value(res.clone());
  167. match response {
  168. Ok(res) => Ok(res),
  169. Err(_) => Err(Error::from_json(&res.to_string())?),
  170. }
  171. }
  172. /// Check Max expected fee [NUT-05]
  173. pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
  174. let url = self.mint_url.join("checkfees")?;
  175. let request = CheckFeesRequest { pr: invoice };
  176. let res = minreq::post(url)
  177. .with_json(&request)?
  178. .send()?
  179. .json::<Value>()?;
  180. let response: Result<CheckFeesResponse, serde_json::Error> =
  181. serde_json::from_value(res.clone());
  182. match response {
  183. Ok(res) => Ok(res),
  184. Err(_) => Err(Error::from_json(&res.to_string())?),
  185. }
  186. }
  187. /// Melt [NUT-05]
  188. /// [Nut-08] Lightning fee return if outputs defined
  189. pub async fn melt(
  190. &self,
  191. proofs: Vec<Proof>,
  192. invoice: Bolt11Invoice,
  193. outputs: Option<Vec<BlindedMessage>>,
  194. ) -> Result<MeltResponse, Error> {
  195. let url = self.mint_url.join("melt")?;
  196. let request = MeltRequest {
  197. proofs,
  198. pr: invoice,
  199. outputs,
  200. };
  201. let value = minreq::post(url)
  202. .with_json(&request)?
  203. .send()?
  204. .json::<Value>()?;
  205. let response: Result<MeltResponse, serde_json::Error> =
  206. serde_json::from_value(value.clone());
  207. match response {
  208. Ok(res) => Ok(res),
  209. Err(_) => Err(Error::from_json(&value.to_string())?),
  210. }
  211. }
  212. /// Split Token [NUT-06]
  213. pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
  214. let url = self.mint_url.join("split")?;
  215. let res = minreq::post(url)
  216. .with_json(&split_request)?
  217. .send()?
  218. .json::<Value>()?;
  219. let response: Result<SplitResponse, serde_json::Error> =
  220. serde_json::from_value(res.clone());
  221. match response {
  222. Ok(res) => Ok(res),
  223. Err(_) => Err(Error::from_json(&res.to_string())?),
  224. }
  225. }
  226. /// Spendable check [NUT-07]
  227. pub async fn check_spendable(
  228. &self,
  229. proofs: &Vec<nut00::mint::Proof>,
  230. ) -> Result<CheckSpendableResponse, Error> {
  231. let url = self.mint_url.join("check")?;
  232. let request = CheckSpendableRequest {
  233. proofs: proofs.to_owned(),
  234. };
  235. let res = minreq::post(url)
  236. .with_json(&request)?
  237. .send()?
  238. .json::<Value>()?;
  239. let response: Result<CheckSpendableResponse, serde_json::Error> =
  240. serde_json::from_value(res.clone());
  241. match response {
  242. Ok(res) => Ok(res),
  243. Err(_) => Err(Error::from_json(&res.to_string())?),
  244. }
  245. }
  246. /// Get Mint Info [NUT-09]
  247. pub async fn get_info(&self) -> Result<MintInfo, Error> {
  248. let url = self.mint_url.join("info")?;
  249. let res = minreq::get(url).send()?.json::<Value>()?;
  250. let response: Result<MintInfo, serde_json::Error> = serde_json::from_value(res.clone());
  251. match response {
  252. Ok(res) => Ok(res),
  253. Err(_) => Err(Error::from_json(&res.to_string())?),
  254. }
  255. }
  256. }
  257. #[cfg(test)]
  258. mod tests {
  259. use super::*;
  260. #[test]
  261. fn test_decode_error() {
  262. let err = r#"{"code":0,"error":"Lightning invoice not paid yet."}"#;
  263. let error = Error::from_json(err).unwrap();
  264. match error {
  265. Error::InvoiceNotPaid => {}
  266. _ => panic!("Wrong error"),
  267. }
  268. let err = r#"{"code": 0, "error": "Lightning wallet not responding: Failed to connect to https://legend.lnbits.com due to: All connection attempts failed"}"#;
  269. let error = Error::from_json(err).unwrap();
  270. match error {
  271. Error::LightingWalletNotResponding(mint) => {
  272. assert_eq!(mint, Some("https://legend.lnbits.com".to_string()));
  273. }
  274. _ => panic!("Wrong error"),
  275. }
  276. }
  277. }