From 4a0dae6a00dbec740f1f3644a0f914d59e2683ef Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 21 Jul 2023 23:35:49 -0400 Subject: [PATCH] improve: wasm wallet client --- Cargo.toml | 9 +- src/error.rs | 11 ++ src/lib.rs | 12 +- src/wallet.rs | 7 +- src/wasm_client.rs | 376 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 src/wasm_client.rs diff --git a/Cargo.toml b/Cargo.toml index e52ed1d6..47beb2d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ bitcoin_hashes = "0.12.0" hex = "0.4.3" k256 = { version = "0.13.1", features=["arithmetic"] } lightning-invoice = { version = "0.23.0", features=["serde"] } -minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] } rand = "0.8.5" getrandom = { version = "0.2", features = ["js"] } serde = { version = "1.0.160", features = ["derive"]} @@ -34,5 +33,13 @@ url = "2.3.1" regex = "1.8.4" log = "0.4.19" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +gloo = { version = "0.8.1", features = ["net"]} + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] } + + [dev-dependencies] tokio = {version = "1.27.0", features = ["rt", "macros"] } diff --git a/src/error.rs b/src/error.rs index e301c9f3..ad1bd0b8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -80,7 +80,10 @@ pub mod wallet { #[derive(Debug)] pub enum Error { + #[cfg(not(target_arch = "wasm32"))] CrabMintError(crate::client::Error), + #[cfg(target_arch = "wasm32")] + CrabMintError(crate::wasm_client::Error), /// Serde Json error SerdeJsonError(serde_json::Error), /// From elliptic curve @@ -113,12 +116,20 @@ pub mod wallet { } } + #[cfg(not(target_arch = "wasm32"))] impl From for Error { fn from(err: crate::client::Error) -> Error { Error::CrabMintError(err) } } + #[cfg(target_arch = "wasm32")] + impl From for Error { + fn from(err: crate::wasm_client::Error) -> Error { + Error::CrabMintError(err) + } + } + impl From for Error { fn from(err: serde_json::Error) -> Error { Error::SerdeJsonError(err) diff --git a/src/lib.rs b/src/lib.rs index 5bf63af7..f075bb3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod amount; #[cfg(feature = "wallet")] -pub mod client; pub mod dhke; pub mod error; #[cfg(feature = "mint")] @@ -12,6 +11,17 @@ pub mod utils; #[cfg(feature = "wallet")] pub mod wallet; +#[cfg(all(feature = "wallet", not(target_arch = "wasm32")))] +pub mod client; +#[cfg(all(feature = "wallet", target_arch = "wasm32"))] +pub mod wasm_client; + +#[cfg(all(feature = "wallet", target_arch = "wasm32"))] +pub use wasm_client::Client; + +#[cfg(all(feature = "wallet", not(target_arch = "wasm32")))] +pub use client::Client; + pub use amount::Amount; pub use bitcoin::hashes::sha256::Hash as Sha256; pub use lightning_invoice; diff --git a/src/wallet.rs b/src/wallet.rs index 1b770518..e1f10fa1 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -14,11 +14,16 @@ use crate::types::{Melted, ProofsStatus, SendProofs}; use crate::Amount; pub use crate::Invoice; use crate::{ - client::Client, dhke::construct_proofs, error::{self, wallet::Error}, }; +#[cfg(target_arch = "wasm32")] +use crate::wasm_client::Client; + +#[cfg(not(target_arch = "wasm32"))] +use crate::client::Client; + #[derive(Clone, Debug)] pub struct Wallet { pub client: Client, diff --git a/src/wasm_client.rs b/src/wasm_client.rs new file mode 100644 index 00000000..dfa5daf3 --- /dev/null +++ b/src/wasm_client.rs @@ -0,0 +1,376 @@ +//! Client to connet to mint +use std::fmt; + +use gloo::net::http::Request; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use url::Url; + +use crate::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof}; +use crate::nuts::nut01::Keys; +use crate::nuts::nut03::RequestMintResponse; +use crate::nuts::nut04::{MintRequest, PostMintResponse}; +use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse}; +use crate::nuts::nut06::{SplitRequest, SplitResponse}; +use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse}; +use crate::nuts::nut08::{MeltRequest, MeltResponse}; +use crate::nuts::nut09::MintInfo; +use crate::nuts::*; +use crate::utils; +use crate::Amount; +pub use crate::Invoice; + +#[derive(Debug)] +pub enum Error { + InvoiceNotPaid, + LightingWalletNotResponding(Option), + /// Parse Url Error + UrlParseError(url::ParseError), + /// Serde Json error + SerdeJsonError(serde_json::Error), + /// Gloo error + GlooError(String), + /// Custom Error + Custom(String), +} + +impl From for Error { + fn from(err: url::ParseError) -> Error { + Error::UrlParseError(err) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Error { + Error::SerdeJsonError(err) + } +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvoiceNotPaid => write!(f, "Invoice not paid"), + Error::LightingWalletNotResponding(mint) => { + write!( + f, + "Lightning Wallet not responding: {}", + mint.clone().unwrap_or("".to_string()) + ) + } + Error::UrlParseError(err) => write!(f, "{}", err), + Error::SerdeJsonError(err) => write!(f, "{}", err), + Error::GlooError(err) => write!(f, "{}", err), + Error::Custom(message) => write!(f, "{}", message), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintErrorResponse { + code: u32, + error: String, +} + +impl Error { + pub fn from_json(json: &str) -> Result { + let mint_res: MintErrorResponse = serde_json::from_str(json)?; + + let mint_error = match mint_res.error { + error if error.starts_with("Lightning invoice not paid yet.") => Error::InvoiceNotPaid, + error if error.starts_with("Lightning wallet not responding") => { + let mint = utils::extract_url_from_error(&error); + Error::LightingWalletNotResponding(mint) + } + error => Error::Custom(error), + }; + Ok(mint_error) + } +} + +#[derive(Debug, Clone)] +pub struct Client { + pub mint_url: Url, +} + +impl Client { + pub fn new(mint_url: &str) -> Result { + // HACK + let mut mint_url = String::from(mint_url); + if !mint_url.ends_with('/') { + mint_url.push('/'); + } + let mint_url = Url::parse(&mint_url)?; + Ok(Self { mint_url }) + } + + /// Get Mint Keys [NUT-01] + pub async fn get_keys(&self) -> Result { + let url = self.mint_url.join("keys")?; + let keys = Request::get(url.as_str()) + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let keys: Keys = serde_json::from_str(&keys.to_string())?; + /* + let keys: BTreeMap = match serde_json::from_value(keys.clone()) { + Ok(keys) => keys, + Err(_err) => { + return Err(Error::CustomError(format!( + "url: {}, {}", + url, + serde_json::to_string(&keys)? + ))) + } + }; + + let mint_keys: BTreeMap = keys + .into_iter() + .filter_map(|(k, v)| { + let key = hex::decode(v).ok()?; + let public_key = PublicKey::from_sec1_bytes(&key).ok()?; + Some((k, public_key)) + }) + .collect(); + */ + Ok(keys) + } + + /// Get Keysets [NUT-02] + pub async fn get_keysets(&self) -> Result { + let url = self.mint_url.join("keysets")?; + let res = Request::get(url.as_str()) + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Request Mint [NUT-03] + pub async fn request_mint(&self, amount: Amount) -> Result { + let mut url = self.mint_url.join("mint")?; + url.query_pairs_mut() + .append_pair("amount", &amount.to_sat().to_string()); + + let res = Request::get(url.as_str()) + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Mint Tokens [NUT-04] + pub async fn mint( + &self, + blinded_messages: BlindedMessages, + hash: &str, + ) -> Result { + let mut url = self.mint_url.join("mint")?; + url.query_pairs_mut().append_pair("hash", hash); + + let request = MintRequest { + outputs: blinded_messages.blinded_messages, + }; + + let res = Request::post(url.as_str()) + .json(&request) + .map_err(|err| Error::GlooError(err.to_string()))? + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Check Max expected fee [NUT-05] + pub async fn check_fees(&self, invoice: Invoice) -> Result { + let url = self.mint_url.join("checkfees")?; + + let request = CheckFeesRequest { pr: invoice }; + + let res = Request::post(url.as_str()) + .json(&request) + .map_err(|err| Error::GlooError(err.to_string()))? + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Melt [NUT-05] + /// [Nut-08] Lightning fee return if outputs defined + pub async fn melt( + &self, + proofs: Vec, + invoice: Invoice, + outputs: Option>, + ) -> Result { + let url = self.mint_url.join("melt")?; + + let request = MeltRequest { + proofs, + pr: invoice, + outputs, + }; + + let value = Request::post(url.as_str()) + .json(&request) + .map_err(|err| Error::GlooError(err.to_string()))? + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(value.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&value.to_string())?), + } + } + + /// Split Token [NUT-06] + pub async fn split(&self, split_request: SplitRequest) -> Result { + let url = self.mint_url.join("split")?; + + let res = Request::post(url.as_str()) + .json(&split_request) + .map_err(|err| Error::GlooError(err.to_string()))? + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Spendable check [NUT-07] + pub async fn check_spendable( + &self, + proofs: &Vec, + ) -> Result { + let url = self.mint_url.join("check")?; + let request = CheckSpendableRequest { + proofs: proofs.to_owned(), + }; + + let res = Request::post(url.as_str()) + .json(&request) + .map_err(|err| Error::GlooError(err.to_string()))? + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + + /// Get Mint Info [NUT-09] + pub async fn get_info(&self) -> Result { + let url = self.mint_url.join("info")?; + let res = Request::get(url.as_str()) + .send() + .await + .map_err(|err| Error::GlooError(err.to_string()))? + .json::() + .await + .map_err(|err| Error::GlooError(err.to_string()))?; + + let response: Result = serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_decode_error() { + let err = r#"{"code":0,"error":"Lightning invoice not paid yet."}"#; + + let error = Error::from_json(err).unwrap(); + + match error { + Error::InvoiceNotPaid => {} + _ => panic!("Wrong error"), + } + + let err = r#"{"code": 0, "error": "Lightning wallet not responding: Failed to connect to https://legend.lnbits.com due to: All connection attempts failed"}"#; + let error = Error::from_json(err).unwrap(); + match error { + Error::LightingWalletNotResponding(mint) => { + assert_eq!(mint, Some("https://legend.lnbits.com".to_string())); + } + _ => panic!("Wrong error"), + } + } +}