diff --git a/integrationtests/tests/tests.rs b/integrationtests/tests/tests.rs index 8dae27d7..1f09c971 100644 --- a/integrationtests/tests/tests.rs +++ b/integrationtests/tests/tests.rs @@ -1,7 +1,7 @@ -#![allow(unused_imports)] use moksha_core::primitives::PaymentMethod; -use moksha_wallet::client::reqwest::HttpClient; -use moksha_wallet::client::LegacyClient; + +use moksha_wallet::client::CashuClient; +use moksha_wallet::http::CrossPlatformHttpClient; use moksha_wallet::localstore::sqlite::SqliteLocalStore; use moksha_wallet::wallet::WalletBuilder; use mokshamint::lightning::lnbits::LnbitsLightningSettings; @@ -18,7 +18,7 @@ use tokio::time::{sleep_until, Instant}; #[test] pub fn test_integration() -> anyhow::Result<()> { - use mokshamint::config::{DatabaseConfig, LightningFeeConfig, ServerConfig}; + use mokshamint::config::{DatabaseConfig, ServerConfig}; let docker = clients::Cli::default(); let node = docker.run(Postgres::default()); @@ -66,14 +66,14 @@ pub fn test_integration() -> anyhow::Result<()> { // Wait for the server to start std::thread::sleep(std::time::Duration::from_millis(800)); - let client = HttpClient::default(); + let client = CrossPlatformHttpClient::new(); let mint_url = Url::parse("http://127.0.0.1:8686")?; let rt = Runtime::new()?; rt.block_on(async move { - let keys = client.get_mint_keys(&mint_url).await; + let keys = client.get_keys(&mint_url).await; assert!(keys.is_ok()); - let keysets = client.get_mint_keysets(&mint_url).await; + let keysets = client.get_keysets(&mint_url).await; assert!(keysets.is_ok()); // create wallet diff --git a/justfile b/justfile index 4d43660f..0a4bee2c 100644 --- a/justfile +++ b/justfile @@ -64,7 +64,7 @@ run-cli *ARGS: # run integrationtests run-itests: cd integrationtests && \ - cargo test -- --ignored + cargo test # build the mint docker-image build-docker: diff --git a/moksha-cli/src/main.rs b/moksha-cli/src/main.rs index 2da157d9..b6cf1956 100644 --- a/moksha-cli/src/main.rs +++ b/moksha-cli/src/main.rs @@ -4,6 +4,7 @@ use moksha_core::primitives::{ PaymentMethod, PostMeltOnchainResponse, PostMintQuoteBolt11Response, PostMintQuoteOnchainResponse, }; +use moksha_wallet::http::CrossPlatformHttpClient; use num_format::{Locale, ToFormattedString}; use std::io::Write; use std::{io::stdout, path::PathBuf}; @@ -72,7 +73,7 @@ async fn main() -> anyhow::Result<()> { }; let localstore = SqliteLocalStore::with_path(db_path.clone()).await?; - let client = moksha_wallet::client::reqwest::HttpClient::new(); + let client = CrossPlatformHttpClient::new(); let wallet = moksha_wallet::wallet::WalletBuilder::default() .with_client(client) .with_localstore(localstore) diff --git a/moksha-wallet/examples/receive_tokens.rs b/moksha-wallet/examples/receive_tokens.rs index 50c66505..38cc2fe0 100644 --- a/moksha-wallet/examples/receive_tokens.rs +++ b/moksha-wallet/examples/receive_tokens.rs @@ -2,7 +2,7 @@ use std::env::temp_dir; use moksha_core::token::TokenV3; use moksha_wallet::{ - client::reqwest::HttpClient, localstore::sqlite::SqliteLocalStore, wallet::WalletBuilder, + http::CrossPlatformHttpClient, localstore::sqlite::SqliteLocalStore, wallet::WalletBuilder, }; use url::Url; @@ -10,7 +10,7 @@ use url::Url; async fn main() -> anyhow::Result<()> { let db_path = temp_dir().join("wallet.db").to_str().unwrap().to_string(); let localstore = SqliteLocalStore::with_path(db_path).await?; - let client = HttpClient::new(); + let client = CrossPlatformHttpClient::new(); let wallet = WalletBuilder::default() .with_client(client) .with_localstore(localstore) diff --git a/moksha-wallet/src/client/wasm_client.rs b/moksha-wallet/src/client/crossplatform.rs similarity index 53% rename from moksha-wallet/src/client/wasm_client.rs rename to moksha-wallet/src/client/crossplatform.rs index e1e7fdc0..9161c641 100644 --- a/moksha-wallet/src/client/wasm_client.rs +++ b/moksha-wallet/src/client/crossplatform.rs @@ -1,11 +1,10 @@ use async_trait::async_trait; -use gloo_net::http::{Request, Response}; use moksha_core::{ blind::BlindedMessage, keyset::V1Keysets, primitives::{ - CashuErrorResponse, CurrencyUnit, GetMeltOnchainResponse, KeysResponse, MintInfoResponse, + CurrencyUnit, GetMeltOnchainResponse, KeysResponse, MintInfoResponse, PostMeltBolt11Request, PostMeltBolt11Response, PostMeltOnchainRequest, PostMeltOnchainResponse, PostMeltQuoteBolt11Request, PostMeltQuoteBolt11Response, PostMeltQuoteOnchainRequest, PostMeltQuoteOnchainResponse, PostMintBolt11Request, @@ -16,24 +15,16 @@ use moksha_core::{ proof::Proofs, }; -use crate::error::MokshaWalletError; use url::Url; -use super::Client; +use crate::{error::MokshaWalletError, http::CrossPlatformHttpClient}; -#[derive(Debug, Clone)] -pub struct WasmClient; - -impl WasmClient { - pub fn new() -> Self { - Self {} - } -} +use super::CashuClient; #[async_trait(?Send)] -impl Client for WasmClient { +impl CashuClient for CrossPlatformHttpClient { async fn get_keys(&self, mint_url: &Url) -> Result { - do_get(&mint_url.join("v1/keys")?).await + self.do_get(&mint_url.join("v1/keys")?).await } async fn get_keys_by_id( @@ -41,11 +32,12 @@ impl Client for WasmClient { mint_url: &Url, keyset_id: String, ) -> Result { - do_get(&mint_url.join(&format!("v1/keys/{}", keyset_id))?).await + self.do_get(&mint_url.join(&format!("v1/keys/{}", keyset_id))?) + .await } async fn get_keysets(&self, mint_url: &Url) -> Result { - do_get(&mint_url.join("v1/keysets")?).await + self.do_get(&mint_url.join("v1/keysets")?).await } async fn post_swap( @@ -56,7 +48,7 @@ impl Client for WasmClient { ) -> Result { let body = PostSwapRequest { inputs, outputs }; - do_post(&mint_url.join("v1/swap")?, &body).await + self.do_post(&mint_url.join("v1/swap")?, &body).await } async fn post_melt_bolt11( @@ -72,7 +64,7 @@ impl Client for WasmClient { outputs, }; - do_post(&mint_url.join("v1/melt/bolt11")?, &body).await + self.do_post(&mint_url.join("v1/melt/bolt11")?, &body).await } async fn post_melt_quote_bolt11( @@ -86,7 +78,8 @@ impl Client for WasmClient { unit, }; - do_post(&mint_url.join("v1/melt/quote/bolt11")?, &body).await + self.do_post(&mint_url.join("v1/melt/quote/bolt11")?, &body) + .await } async fn get_melt_quote_bolt11( @@ -95,7 +88,7 @@ impl Client for WasmClient { quote: String, ) -> Result { let url = mint_url.join(&format!("v1/melt/quote/bolt11/{}", quote))?; - do_get(&url).await + self.do_get(&url).await } async fn post_mint_bolt11( @@ -108,7 +101,7 @@ impl Client for WasmClient { quote, outputs: blinded_messages, }; - do_post(&mint_url.join("v1/mint/bolt11")?, &body).await + self.do_post(&mint_url.join("v1/mint/bolt11")?, &body).await } async fn post_mint_quote_bolt11( @@ -118,7 +111,8 @@ impl Client for WasmClient { unit: CurrencyUnit, ) -> Result { let body = PostMintQuoteBolt11Request { amount, unit }; - do_post(&mint_url.join("v1/mint/quote/bolt11")?, &body).await + self.do_post(&mint_url.join("v1/mint/quote/bolt11")?, &body) + .await } async fn get_mint_quote_bolt11( @@ -126,19 +120,8 @@ impl Client for WasmClient { mint_url: &Url, quote: String, ) -> Result { - do_get(&mint_url.join(&format!("v1/mint/quote/bolt11/{}", quote))?).await - } - - async fn get_info(&self, mint_url: &Url) -> Result { - do_get(&mint_url.join("v1/info")?).await - } - - async fn is_v1_supported(&self, mint_url: &Url) -> Result { - let resp = Request::get(mint_url.join("v1/info")?.as_str()) - .send() - .await?; - - Ok(resp.status() == 200) + self.do_get(&mint_url.join(&format!("v1/mint/quote/bolt11/{}", quote))?) + .await } async fn post_mint_onchain( @@ -151,7 +134,8 @@ impl Client for WasmClient { quote, outputs: blinded_messages, }; - do_post(&mint_url.join("v1/mint/btconchain")?, &body).await + self.do_post(&mint_url.join("v1/mint/btconchain")?, &body) + .await } async fn post_mint_quote_onchain( @@ -160,11 +144,9 @@ impl Client for WasmClient { amount: u64, unit: CurrencyUnit, ) -> Result { - do_post( - &mint_url.join("v1/mint/quote/btconchain")?, - &PostMintQuoteOnchainRequest { amount, unit }, - ) - .await + let body = PostMintQuoteOnchainRequest { amount, unit }; + self.do_post(&mint_url.join("v1/mint/quote/btconchain")?, &body) + .await } async fn get_mint_quote_onchain( @@ -172,7 +154,18 @@ impl Client for WasmClient { mint_url: &Url, quote: String, ) -> Result { - do_get(&mint_url.join(&format!("v1/mint/quote/btconchain/{}", quote))?).await + self.do_get(&mint_url.join(&format!("v1/mint/quote/btconchain/{}", quote))?) + .await + } + + async fn get_info(&self, mint_url: &Url) -> Result { + self.do_get(&mint_url.join("v1/info")?).await + } + + async fn is_v1_supported(&self, mint_url: &Url) -> Result { + self.get_status(&mint_url.join("v1/info")?) + .await + .map(|s| s == 200) } async fn post_melt_onchain( @@ -181,11 +174,9 @@ impl Client for WasmClient { inputs: Proofs, quote: String, ) -> Result { - do_post( - &mint_url.join("v1/melt/btconchain")?, - &PostMeltOnchainRequest { quote, inputs }, - ) - .await + let body = PostMeltOnchainRequest { quote, inputs }; + self.do_post(&mint_url.join("v1/melt/btconchain")?, &body) + .await } async fn post_melt_quote_onchain( @@ -200,7 +191,8 @@ impl Client for WasmClient { amount, unit, }; - do_post(&mint_url.join("v1/melt/quote/btconchain")?, &body).await + self.do_post(&mint_url.join("v1/melt/quote/btconchain")?, &body) + .await } async fn get_melt_quote_onchain( @@ -208,7 +200,8 @@ impl Client for WasmClient { mint_url: &Url, quote: String, ) -> Result { - do_get(&mint_url.join(&format!("/v1/melt/quote/btconchain/{quote}"))?).await + self.do_get(&mint_url.join(&format!("/v1/melt/quote/btconchain/{quote}"))?) + .await } async fn get_melt_onchain( @@ -216,63 +209,7 @@ impl Client for WasmClient { mint_url: &Url, txid: String, ) -> Result { - do_get(&mint_url.join(&format!("/v1/melt/btconchain/{txid}"))?).await - } -} - -async fn do_get(url: &Url) -> Result { - let resp = Request::get(url.as_str()).send().await?; - extract_response_data::(resp).await -} - -async fn do_post( - url: &Url, - body: &B, -) -> Result { - let resp = Request::post(url.as_str()) - .header("content-type", "application/json") - .json(body)? - .send() - .await?; - extract_response_data::(resp).await -} - -async fn extract_response_data( - response: Response, -) -> Result { - match response.status() { - 200 => { - let response_text = response.text().await.unwrap(); // FIXME handle error - match serde_json::from_str::(&response_text) { - Ok(data) => Ok(data), - Err(_) => { - let data = serde_json::from_str::(&response_text) - .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) - .unwrap(); - - // FIXME: use the error code to return a proper error - match data.detail.as_str() { - "Lightning invoice not paid yet." => { - Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) - } - _ => Err(MokshaWalletError::MintError(data.detail)), - } - } - } - } - _ => { - let txt = response.text().await.unwrap(); // FIXME handle error - let data = serde_json::from_str::(&txt) - .map_err(|_| MokshaWalletError::UnexpectedResponse(txt)) - .unwrap(); - - // FIXME: use the error code to return a proper error - match data.detail.as_str() { - "Lightning invoice not paid yet." => { - Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) - } - _ => Err(MokshaWalletError::MintError(data.detail)), - } - } + self.do_get(&mint_url.join(&format!("/v1/melt/btconchain/{txid}"))?) + .await } } diff --git a/moksha-wallet/src/client/mod.rs b/moksha-wallet/src/client/mod.rs index 87860194..47a6f813 100644 --- a/moksha-wallet/src/client/mod.rs +++ b/moksha-wallet/src/client/mod.rs @@ -1,81 +1,28 @@ -use std::collections::HashMap; - use async_trait::async_trait; use moksha_core::{ blind::BlindedMessage, - keyset::{Keysets, V1Keysets}, + keyset::V1Keysets, primitives::{ - CheckFeesResponse, CurrencyUnit, GetMeltOnchainResponse, KeysResponse, MintInfoResponse, - MintLegacyInfoResponse, PaymentRequest, PostMeltBolt11Response, PostMeltOnchainResponse, - PostMeltQuoteBolt11Response, PostMeltQuoteOnchainResponse, PostMeltResponse, - PostMintBolt11Response, PostMintOnchainResponse, PostMintQuoteBolt11Response, - PostMintQuoteOnchainResponse, PostMintResponse, PostSplitResponse, PostSwapResponse, + CurrencyUnit, GetMeltOnchainResponse, KeysResponse, MintInfoResponse, + PostMeltBolt11Response, PostMeltOnchainResponse, PostMeltQuoteBolt11Response, + PostMeltQuoteOnchainResponse, PostMintBolt11Response, PostMintOnchainResponse, + PostMintQuoteBolt11Response, PostMintQuoteOnchainResponse, PostSwapResponse, }, proof::Proofs, }; -use secp256k1::PublicKey; + use url::Url; use crate::error::MokshaWalletError; -#[cfg(not(target_arch = "wasm32"))] -pub mod reqwest; - -#[cfg(target_arch = "wasm32")] -pub mod wasm_client; - -#[async_trait(?Send)] -pub trait LegacyClient { - async fn post_split_tokens( - &self, - mint_url: &Url, - proofs: Proofs, - output: Vec, - ) -> Result; - - async fn post_mint_payment_request( - &self, - mint_url: &Url, - hash: String, - blinded_messages: Vec, - ) -> Result; - - async fn post_melt_tokens( - &self, - mint_url: &Url, - proofs: Proofs, - pr: String, - outputs: Vec, - ) -> Result; - - async fn post_checkfees( - &self, - mint_url: &Url, - pr: String, - ) -> Result; - - async fn get_mint_keys( - &self, - mint_url: &Url, - ) -> Result, MokshaWalletError>; - - async fn get_mint_keysets(&self, mint_url: &Url) -> Result; - - async fn get_mint_payment_request( - &self, - mint_url: &Url, - amount: u64, - ) -> Result; - - async fn get_info(&self, mint_url: &Url) -> Result; -} +pub mod crossplatform; #[cfg(test)] use mockall::automock; #[cfg_attr(test, automock)] #[async_trait(?Send)] -pub trait Client { +pub trait CashuClient { async fn get_keys(&self, mint_url: &Url) -> Result; async fn get_keys_by_id( diff --git a/moksha-wallet/src/client/reqwest.rs b/moksha-wallet/src/client/reqwest.rs deleted file mode 100644 index 16227bba..00000000 --- a/moksha-wallet/src/client/reqwest.rs +++ /dev/null @@ -1,442 +0,0 @@ -use std::collections::HashMap; - -use async_trait::async_trait; -use moksha_core::{ - blind::BlindedMessage, - keyset::{Keysets, V1Keysets}, - primitives::{ - CashuErrorResponse, CheckFeesRequest, CheckFeesResponse, CurrencyUnit, - GetMeltOnchainResponse, KeysResponse, MintInfoResponse, MintLegacyInfoResponse, - PaymentRequest, PostMeltBolt11Request, PostMeltBolt11Response, PostMeltOnchainRequest, - PostMeltOnchainResponse, PostMeltQuoteBolt11Request, PostMeltQuoteBolt11Response, - PostMeltQuoteOnchainRequest, PostMeltQuoteOnchainResponse, PostMeltRequest, - PostMeltResponse, PostMintBolt11Request, PostMintBolt11Response, PostMintOnchainRequest, - PostMintOnchainResponse, PostMintQuoteBolt11Request, PostMintQuoteBolt11Response, - PostMintQuoteOnchainRequest, PostMintQuoteOnchainResponse, PostMintRequest, - PostMintResponse, PostSplitRequest, PostSplitResponse, PostSwapRequest, PostSwapResponse, - }, - proof::Proofs, -}; -use reqwest::{ - header::{HeaderValue, CONTENT_TYPE}, - Response, StatusCode, -}; -use secp256k1::PublicKey; - -use crate::{client::LegacyClient, error::MokshaWalletError}; -use url::Url; - -use super::Client; - -#[derive(Debug, Clone)] -pub struct HttpClient { - request_client: reqwest::Client, -} - -impl HttpClient { - pub fn new() -> Self { - Self { - request_client: reqwest::Client::new(), - } - } - - async fn do_get( - &self, - url: &Url, - ) -> Result { - let resp = self.request_client.get(url.clone()).send().await?; - extract_response_data::(resp).await - } - - async fn do_post( - &self, - url: &Url, - body: &B, - ) -> Result { - let resp = self - .request_client - .post(url.clone()) - .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) - .body(serde_json::to_string(body)?) - .send() - .await?; - extract_response_data::(resp).await - } -} -impl Default for HttpClient { - fn default() -> Self { - Self::new() - } -} - -#[async_trait(?Send)] -impl LegacyClient for HttpClient { - async fn post_split_tokens( - &self, - mint_url: &Url, - proofs: Proofs, - outputs: Vec, - ) -> Result { - let body = serde_json::to_string(&PostSplitRequest { proofs, outputs })?; - - let resp = self - .request_client - .post(mint_url.join("split")?) - .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) - .body(body) - .send() - .await?; - - extract_response_data::(resp).await - } - - async fn post_mint_payment_request( - &self, - mint_url: &Url, - hash: String, - blinded_messages: Vec, - ) -> Result { - let url = mint_url.join(&format!("mint?hash={}", hash))?; - let body = serde_json::to_string(&PostMintRequest { - outputs: blinded_messages, - })?; - - let resp = self - .request_client - .post(url) - .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) - .body(body) - .send() - .await?; - extract_response_data::(resp).await - } - - async fn post_melt_tokens( - &self, - mint_url: &Url, - proofs: Proofs, - pr: String, - outputs: Vec, - ) -> Result { - let body = serde_json::to_string(&PostMeltRequest { - pr, - proofs, - outputs, - })?; - - let resp = self - .request_client - .post(mint_url.join("melt")?) - .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) - .body(body) - .send() - .await?; - extract_response_data::(resp).await - } - - async fn post_checkfees( - &self, - mint_url: &Url, - pr: String, - ) -> Result { - let body = serde_json::to_string(&CheckFeesRequest { pr })?; - - let resp = self - .request_client - .post(mint_url.join("checkfees")?) - .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) - .body(body) - .send() - .await?; - - extract_response_data::(resp).await - } - - async fn get_mint_keys( - &self, - mint_url: &Url, - ) -> Result, MokshaWalletError> { - let resp = self - .request_client - .get(mint_url.join("keys")?) - .send() - .await?; - extract_response_data::>(resp).await - } - - async fn get_mint_keysets(&self, mint_url: &Url) -> Result { - let resp = self - .request_client - .get(mint_url.join("keysets")?) - .send() - .await?; - extract_response_data::(resp).await - } - - async fn get_mint_payment_request( - &self, - mint_url: &Url, - amount: u64, - ) -> Result { - let url = mint_url.join(&format!("mint?amount={}", amount))?; - let resp = self.request_client.get(url).send().await?; - extract_response_data::(resp).await - } - - async fn get_info(&self, mint_url: &Url) -> Result { - let resp = self - .request_client - .get(mint_url.join("info")?) - .send() - .await?; - extract_response_data::(resp).await - } -} - -#[async_trait(?Send)] -impl Client for HttpClient { - async fn get_keys(&self, mint_url: &Url) -> Result { - self.do_get(&mint_url.join("v1/keys")?).await - } - - async fn get_keys_by_id( - &self, - mint_url: &Url, - keyset_id: String, - ) -> Result { - self.do_get(&mint_url.join(&format!("v1/keys/{}", keyset_id))?) - .await - } - - async fn get_keysets(&self, mint_url: &Url) -> Result { - self.do_get(&mint_url.join("v1/keysets")?).await - } - - async fn post_swap( - &self, - mint_url: &Url, - inputs: Proofs, - outputs: Vec, - ) -> Result { - let body = PostSwapRequest { inputs, outputs }; - - self.do_post(&mint_url.join("v1/swap")?, &body).await - } - - async fn post_melt_bolt11( - &self, - mint_url: &Url, - inputs: Proofs, - quote: String, - outputs: Vec, - ) -> Result { - let body = PostMeltBolt11Request { - quote, - inputs, - outputs, - }; - - self.do_post(&mint_url.join("v1/melt/bolt11")?, &body).await - } - - async fn post_melt_quote_bolt11( - &self, - mint_url: &Url, - payment_request: String, - unit: CurrencyUnit, - ) -> Result { - let body = PostMeltQuoteBolt11Request { - request: payment_request, - unit, - }; - - self.do_post(&mint_url.join("v1/melt/quote/bolt11")?, &body) - .await - } - - async fn get_melt_quote_bolt11( - &self, - mint_url: &Url, - quote: String, - ) -> Result { - let url = mint_url.join(&format!("v1/melt/quote/bolt11/{}", quote))?; - self.do_get(&url).await - } - - async fn post_mint_bolt11( - &self, - mint_url: &Url, - quote: String, - blinded_messages: Vec, - ) -> Result { - let body = PostMintBolt11Request { - quote, - outputs: blinded_messages, - }; - self.do_post(&mint_url.join("v1/mint/bolt11")?, &body).await - } - - async fn post_mint_quote_bolt11( - &self, - mint_url: &Url, - amount: u64, - unit: CurrencyUnit, - ) -> Result { - let body = PostMintQuoteBolt11Request { amount, unit }; - self.do_post(&mint_url.join("v1/mint/quote/bolt11")?, &body) - .await - } - - async fn get_mint_quote_bolt11( - &self, - mint_url: &Url, - quote: String, - ) -> Result { - self.do_get(&mint_url.join(&format!("v1/mint/quote/bolt11/{}", quote))?) - .await - } - - async fn post_mint_onchain( - &self, - mint_url: &Url, - quote: String, - blinded_messages: Vec, - ) -> Result { - let body = PostMintOnchainRequest { - quote, - outputs: blinded_messages, - }; - self.do_post(&mint_url.join("v1/mint/btconchain")?, &body) - .await - } - - async fn post_mint_quote_onchain( - &self, - mint_url: &Url, - amount: u64, - unit: CurrencyUnit, - ) -> Result { - let body = PostMintQuoteOnchainRequest { amount, unit }; - self.do_post(&mint_url.join("v1/mint/quote/btconchain")?, &body) - .await - } - - async fn get_mint_quote_onchain( - &self, - mint_url: &Url, - quote: String, - ) -> Result { - self.do_get(&mint_url.join(&format!("v1/mint/quote/btconchain/{}", quote))?) - .await - } - - async fn get_info(&self, mint_url: &Url) -> Result { - self.do_get(&mint_url.join("v1/info")?).await - } - - async fn is_v1_supported(&self, mint_url: &Url) -> Result { - let resp = self - .request_client - .get(mint_url.join("v1/info")?) - .send() - .await?; - Ok(resp.status() == StatusCode::OK) - } - - async fn post_melt_onchain( - &self, - mint_url: &Url, - inputs: Proofs, - quote: String, - ) -> Result { - let body = PostMeltOnchainRequest { quote, inputs }; - self.do_post(&mint_url.join("v1/melt/btconchain")?, &body) - .await - } - - async fn post_melt_quote_onchain( - &self, - mint_url: &Url, - address: String, - amount: u64, - unit: CurrencyUnit, - ) -> Result { - let body = PostMeltQuoteOnchainRequest { - address, - amount, - unit, - }; - self.do_post(&mint_url.join("v1/melt/quote/btconchain")?, &body) - .await - } - - async fn get_melt_quote_onchain( - &self, - mint_url: &Url, - quote: String, - ) -> Result { - self.do_get(&mint_url.join(&format!("/v1/melt/quote/btconchain/{quote}"))?) - .await - } - - async fn get_melt_onchain( - &self, - mint_url: &Url, - txid: String, - ) -> Result { - self.do_get(&mint_url.join(&format!("/v1/melt/btconchain/{txid}"))?) - .await - } -} - -async fn extract_response_data( - response: Response, -) -> Result { - match response.status() { - StatusCode::OK => { - let response_text = response.text().await?; - match serde_json::from_str::(&response_text) { - Ok(data) => Ok(data), - Err(_) => { - let data = serde_json::from_str::(&response_text) - .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) - .unwrap(); - - // FIXME: use the error code to return a proper error - match data.detail.as_str() { - "Lightning invoice not paid yet." => { - Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) - } - _ => Err(MokshaWalletError::MintError(data.detail)), - } - } - } - } - _ => { - let txt = response.text().await?; - let data = serde_json::from_str::(&txt) - .map_err(|_| MokshaWalletError::UnexpectedResponse(txt)) - .unwrap(); - - // FIXME: use the error code to return a proper error - match data.detail.as_str() { - "Lightning invoice not paid yet." => { - Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) - } - _ => Err(MokshaWalletError::MintError(data.detail)), - } - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_deserialize_error() -> anyhow::Result<()> { - let input = "{\"code\":0,\"detail\":\"Lightning invoice not paid yet.\"}"; - let data = serde_json::from_str::(input)?; - assert_eq!(data.code, 0); - assert_eq!(data.detail, "Lightning invoice not paid yet."); - Ok(()) - } -} diff --git a/moksha-wallet/src/http/mod.rs b/moksha-wallet/src/http/mod.rs new file mode 100644 index 00000000..88c87a9a --- /dev/null +++ b/moksha-wallet/src/http/mod.rs @@ -0,0 +1,11 @@ +#[cfg(not(target_arch = "wasm32"))] +pub mod reqwest; + +#[cfg(target_arch = "wasm32")] +pub mod wasm; + +#[derive(Debug, Clone)] +pub struct CrossPlatformHttpClient { + #[cfg(not(target_arch = "wasm32"))] + client: ::reqwest::Client, +} diff --git a/moksha-wallet/src/http/reqwest.rs b/moksha-wallet/src/http/reqwest.rs new file mode 100644 index 00000000..dd292c1d --- /dev/null +++ b/moksha-wallet/src/http/reqwest.rs @@ -0,0 +1,90 @@ +use super::CrossPlatformHttpClient; +use crate::error::MokshaWalletError; +use moksha_core::primitives::CashuErrorResponse; +use reqwest::{ + header::{HeaderValue, CONTENT_TYPE}, + Response, StatusCode, +}; +use url::Url; + +impl CrossPlatformHttpClient { + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + async fn extract_response_data( + response: Response, + ) -> Result { + match response.status() { + StatusCode::OK => { + let response_text = response.text().await?; + match serde_json::from_str::(&response_text) { + Ok(data) => Ok(data), + Err(_) => { + let data = serde_json::from_str::(&response_text) + .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) + .unwrap(); + + // FIXME: use the error code to return a proper error + match data.detail.as_str() { + "Lightning invoice not paid yet." => { + Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) + } + _ => Err(MokshaWalletError::MintError(data.detail)), + } + } + } + } + _ => { + let txt = response.text().await?; + let data = serde_json::from_str::(&txt) + .map_err(|_| MokshaWalletError::UnexpectedResponse(txt)) + .unwrap(); + + // FIXME: use the error code to return a proper error + match data.detail.as_str() { + "Lightning invoice not paid yet." => { + Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) + } + _ => Err(MokshaWalletError::MintError(data.detail)), + } + } + } + } + + pub async fn do_get( + &self, + url: &Url, + ) -> Result { + let resp = self.client.get(url.clone()).send().await?; + Self::extract_response_data::(resp).await + } + + pub async fn do_post( + &self, + url: &Url, + body: &B, + ) -> Result { + let resp = self + .client + .post(url.clone()) + .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) + .body(serde_json::to_string(body)?) + .send() + .await?; + Self::extract_response_data::(resp).await + } + + pub async fn get_status(&self, url: &Url) -> Result { + let resp = self.client.get(url.to_owned()).send().await?; + Ok(resp.status().as_u16()) + } +} + +impl Default for CrossPlatformHttpClient { + fn default() -> Self { + Self::new() + } +} diff --git a/moksha-wallet/src/http/wasm.rs b/moksha-wallet/src/http/wasm.rs new file mode 100644 index 00000000..dc1300b2 --- /dev/null +++ b/moksha-wallet/src/http/wasm.rs @@ -0,0 +1,79 @@ +use moksha_core::primitives::CashuErrorResponse; + +use crate::error::MokshaWalletError; +use url::Url; + +use super::CrossPlatformHttpClient; +use gloo_net::http::{Request, Response}; + +impl CrossPlatformHttpClient { + pub fn new() -> Self { + Self {} + } + + pub async fn do_get( + &self, + url: &Url, + ) -> Result { + let resp = Request::get(url.as_str()).send().await?; + Self::extract_response_data::(resp).await + } + + pub async fn do_post( + &self, + url: &Url, + body: &B, + ) -> Result { + let resp = Request::post(url.as_str()) + .header("content-type", "application/json") + .json(body)? + .send() + .await?; + Self::extract_response_data::(resp).await + } + + pub async fn get_status(&self, url: &Url) -> Result { + let resp = Request::get(url.as_str()).send().await?; + Ok(resp.status()) + } + + async fn extract_response_data( + response: Response, + ) -> Result { + match response.status() { + 200 => { + let response_text = response.text().await.unwrap(); // FIXME handle error + match serde_json::from_str::(&response_text) { + Ok(data) => Ok(data), + Err(_) => { + let data = serde_json::from_str::(&response_text) + .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) + .unwrap(); + + // FIXME: use the error code to return a proper error + match data.detail.as_str() { + "Lightning invoice not paid yet." => { + Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) + } + _ => Err(MokshaWalletError::MintError(data.detail)), + } + } + } + } + _ => { + let txt = response.text().await.unwrap(); // FIXME handle error + let data = serde_json::from_str::(&txt) + .map_err(|_| MokshaWalletError::UnexpectedResponse(txt)) + .unwrap(); + + // FIXME: use the error code to return a proper error + match data.detail.as_str() { + "Lightning invoice not paid yet." => { + Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) + } + _ => Err(MokshaWalletError::MintError(data.detail)), + } + } + } + } +} diff --git a/moksha-wallet/src/lib.rs b/moksha-wallet/src/lib.rs index 59a40462..24618146 100644 --- a/moksha-wallet/src/lib.rs +++ b/moksha-wallet/src/lib.rs @@ -1,9 +1,7 @@ +pub mod btcprice; pub mod client; +pub mod config_path; pub mod error; - +pub mod http; pub mod localstore; pub mod wallet; - -pub mod btcprice; - -pub mod config_path; diff --git a/moksha-wallet/src/wallet.rs b/moksha-wallet/src/wallet.rs index f7efb67a..96b5913b 100644 --- a/moksha-wallet/src/wallet.rs +++ b/moksha-wallet/src/wallet.rs @@ -16,7 +16,7 @@ use secp256k1::SecretKey; use url::Url; use crate::{ - client::Client, + client::CashuClient, error::MokshaWalletError, localstore::{LocalStore, WalletKeyset}, }; @@ -24,7 +24,7 @@ use lightning_invoice::Bolt11Invoice as LNInvoice; use std::str::FromStr; #[derive(Clone)] -pub struct Wallet { +pub struct Wallet { client: C, keyset_id: V1Keyset, keyset: KeyResponse, @@ -33,13 +33,13 @@ pub struct Wallet { mint_url: Url, } -pub struct WalletBuilder { +pub struct WalletBuilder { client: Option, localstore: Option, mint_url: Option, } -impl WalletBuilder { +impl WalletBuilder { const fn new() -> Self { Self { client: None, @@ -115,13 +115,13 @@ impl WalletBuilder { } } -impl Default for WalletBuilder { +impl Default for WalletBuilder { fn default() -> Self { Self::new() } } -impl Wallet { +impl Wallet { fn new( client: C, mint_keys: V1Keyset, @@ -637,7 +637,7 @@ fn get_blinded_msg(blinded_messages: Vec<(BlindedMessage, SecretKey)>) -> Vec MockClient { + fn create_mock() -> MockCashuClient { let keys = MintKeyset::new("mykey", ""); let key_response = KeyResponse { keys: keys.public_keys.clone(), @@ -716,7 +716,7 @@ mod tests { let keys_response = KeysResponse::new(key_response); let keysets = V1Keysets::new(keys.keyset_id, CurrencyUnit::Sat, true); - let mut client = MockClient::default(); + let mut client = MockCashuClient::default(); client .expect_get_keys() .returning(move |_| Ok(keys_response.clone()));