diff --git a/moksha-wallet/Cargo.toml b/moksha-wallet/Cargo.toml index 926bc8a7..1e60629d 100644 --- a/moksha-wallet/Cargo.toml +++ b/moksha-wallet/Cargo.toml @@ -23,16 +23,18 @@ thiserror = "1.0.56" async-trait = "0.1.77" lightning-invoice = "0.29.0" url = "2.5.0" + +[target.'cfg(not(target_os="espidf"))'.dependencies] dirs = "5.0.1" -[target.'cfg(target_family = "wasm")'.dependencies] +[target.'cfg(any(target_family = "wasm", target_os = "espidf"))'.dependencies] gloo-net = { version = "0.5.0" } serde-wasm-bindgen = "0.6.3" wasm-bindgen = "0.2.90" rexie = "0.5.0" tokio = { version = "1.35.1", features = ["rt", "sync"] } -[target.'cfg(not(target_family="wasm"))'.dependencies] +[target.'cfg(all(not(target_family="wasm"), not(target_os="espidf")))'.dependencies] reqwest = { version = "0.11.23", features = ["serde_json", "json", "rustls-tls"], default-features = false } tokio = { version = "1.35.1", features = ["rt", "rt-multi-thread", "macros"] } sqlx = { version = "0.7.3", default-features = false, features = ["sqlite", "runtime-tokio", "tls-rustls", "migrate", "macros"] } diff --git a/moksha-wallet/src/btcprice.rs b/moksha-wallet/src/btcprice.rs index f72d560c..a7a75d84 100644 --- a/moksha-wallet/src/btcprice.rs +++ b/moksha-wallet/src/btcprice.rs @@ -1,6 +1,7 @@ use crate::error::MokshaWalletError; async fn execute_request(url: &str) -> Result { + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] { let request_client = reqwest::Client::new(); @@ -8,7 +9,7 @@ async fn execute_request(url: &str) -> Result { Ok(response.text().await?) } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", target_os = "espidf"))] { let resp = gloo_net::http::Request::get(url).send().await.unwrap(); Ok(resp.text().await?) diff --git a/moksha-wallet/src/client/mod.rs b/moksha-wallet/src/client/mod.rs index d4c5e4dd..c8900f4a 100644 --- a/moksha-wallet/src/client/mod.rs +++ b/moksha-wallet/src/client/mod.rs @@ -20,6 +20,7 @@ pub mod crossplatform; #[cfg(test)] use mockall::automock; +#[cfg(not(target_os = "espidf"))] #[cfg_attr(test, automock)] #[async_trait(?Send)] pub trait CashuClient { @@ -132,3 +133,115 @@ pub trait CashuClient { txid: String, ) -> Result; } + +#[cfg(target_os = "espidf")] +pub trait CashuClient { + fn get_keys(&self, mint_url: &Url) -> Result; + + fn get_keys_by_id( + &self, + mint_url: &Url, + keyset_id: String, + ) -> Result; + + fn get_keysets(&self, mint_url: &Url) -> Result; + + fn post_swap( + &self, + mint_url: &Url, + proofs: Proofs, + output: Vec, + ) -> Result; + + fn post_melt_bolt11( + &self, + mint_url: &Url, + proofs: Proofs, + quote: String, + outputs: Vec, + ) -> Result; + + fn post_melt_quote_bolt11( + &self, + mint_url: &Url, + payment_request: String, + unit: CurrencyUnit, + ) -> Result; + + fn get_melt_quote_bolt11( + &self, + mint_url: &Url, + quote: String, + ) -> Result; + + fn post_mint_bolt11( + &self, + mint_url: &Url, + quote: String, + blinded_messages: Vec, + ) -> Result; + + fn post_mint_quote_bolt11( + &self, + mint_url: &Url, + amount: u64, + unit: CurrencyUnit, + ) -> Result; + + fn get_mint_quote_bolt11( + &self, + mint_url: &Url, + quote: String, + ) -> Result; + + fn get_info(&self, mint_url: &Url) -> Result; + + fn is_v1_supported(&self, mint_url: &Url) -> Result; + + fn post_mint_onchain( + &self, + mint_url: &Url, + quote: String, + blinded_messages: Vec, + ) -> Result; + + fn post_mint_quote_onchain( + &self, + mint_url: &Url, + amount: u64, + unit: CurrencyUnit, + ) -> Result; + + fn get_mint_quote_onchain( + &self, + mint_url: &Url, + quote: String, + ) -> Result; + + fn post_melt_onchain( + &self, + mint_url: &Url, + proofs: Proofs, + quote: String, + ) -> Result; + + fn post_melt_quote_onchain( + &self, + mint_url: &Url, + address: String, + amount: u64, + unit: CurrencyUnit, + ) -> Result, MokshaWalletError>; + + fn get_melt_quote_onchain( + &self, + mint_url: &Url, + quote: String, + ) -> Result; + + fn get_melt_onchain( + &self, + mint_url: &Url, + txid: String, + ) -> Result; +} diff --git a/moksha-wallet/src/config_path.rs b/moksha-wallet/src/config_path.rs index 14256218..b8a51ac8 100644 --- a/moksha-wallet/src/config_path.rs +++ b/moksha-wallet/src/config_path.rs @@ -1,3 +1,4 @@ +#[cfg(not(target_os = "espidf"))] use dirs::home_dir; use std::{fs::create_dir, path::PathBuf}; @@ -15,6 +16,7 @@ pub const ENV_DB_PATH: &str = "WALLET_DB_PATH"; /// let db_path = moksha_wallet::config_path::db_path(); /// println!("Database path: {}", db_path); /// ``` +#[cfg(not(target_os = "espidf"))] pub fn db_path() -> String { std::env::var(ENV_DB_PATH).map_or_else( |_| { @@ -43,6 +45,7 @@ pub fn db_path() -> String { ) } +#[cfg(not(target_os = "espidf"))] pub fn config_dir() -> PathBuf { let home = home_dir() .expect("home dir not found") diff --git a/moksha-wallet/src/error.rs b/moksha-wallet/src/error.rs index 9075fa04..499b405f 100644 --- a/moksha-wallet/src/error.rs +++ b/moksha-wallet/src/error.rs @@ -5,17 +5,19 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum MokshaWalletError { - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", target_os = "espidf"))] #[error("GlooNetError - {0}")] GlooNet(#[from] gloo_net::Error), #[error("SerdeJsonError - {0}")] Json(#[from] serde_json::Error), + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] #[error("ReqwestError - {0}")] Reqwest(#[from] reqwest::Error), + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] #[error("InvalidHeaderValueError - {0}")] InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), @@ -31,10 +33,12 @@ pub enum MokshaWalletError { #[error("MokshaCoreError - {0}")] MokshaCore(#[from] moksha_core::error::MokshaCoreError), + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] #[error("DB Error {0}")] Db(#[from] sqlx::Error), + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] #[error("Migrate Error {0}")] Migrate(#[from] sqlx::migrate::MigrateError), diff --git a/moksha-wallet/src/http/mod.rs b/moksha-wallet/src/http/mod.rs index 07284311..384eda95 100644 --- a/moksha-wallet/src/http/mod.rs +++ b/moksha-wallet/src/http/mod.rs @@ -1,11 +1,13 @@ +#[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] pub mod reqwest; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", target_os = "espidf"))] pub mod wasm; #[derive(Debug, Clone)] pub struct CrossPlatformHttpClient { + #[cfg(not(target_os = "espidf"))] #[cfg(not(target_arch = "wasm32"))] client: ::reqwest::Client, } diff --git a/moksha-wallet/src/localstore/mod.rs b/moksha-wallet/src/localstore/mod.rs index 3f754ee1..ea8f870b 100644 --- a/moksha-wallet/src/localstore/mod.rs +++ b/moksha-wallet/src/localstore/mod.rs @@ -3,10 +3,10 @@ use moksha_core::proof::Proofs; use crate::error::MokshaWalletError; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(any(target_arch = "wasm32", target_os = "espidf")))] pub mod sqlite; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", target_os = "espidf"))] pub mod rexie; #[derive(Debug, Clone)] @@ -15,7 +15,7 @@ pub struct WalletKeyset { pub mint_url: String, } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(any(target_arch = "wasm32", target_os = "espidf")))] #[async_trait(?Send)] pub trait LocalStore { type DB: sqlx::Database; @@ -39,7 +39,7 @@ pub trait LocalStore { async fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError>; } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", target_os = "espidf"))] pub struct RexieTransaction {} #[cfg(target_arch = "wasm32")] @@ -48,6 +48,12 @@ impl RexieTransaction { Ok(()) } } +#[cfg(target_os = "espidf")] +impl RexieTransaction { + pub fn commit(&self) -> Result<(), MokshaWalletError> { + Ok(()) + } +} #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] @@ -71,3 +77,25 @@ pub trait LocalStore { async fn get_keysets(&self) -> Result, MokshaWalletError>; async fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError>; } + +#[cfg(target_os = "espidf")] +pub trait LocalStore { + fn begin_tx(&self) -> Result { + Ok(RexieTransaction {}) + } + + fn delete_proofs( + &self, + tx: &mut RexieTransaction, + proofs: &Proofs, + ) -> Result<(), MokshaWalletError>; + fn add_proofs( + &self, + tx: &mut RexieTransaction, + proofs: &Proofs, + ) -> Result<(), MokshaWalletError>; + fn get_proofs(&self, tx: &mut RexieTransaction) -> Result; + + fn get_keysets(&self) -> Result, MokshaWalletError>; + fn add_keyset(&self, keyset: &WalletKeyset) -> Result<(), MokshaWalletError>; +} diff --git a/moksha-wallet/src/wallet.rs b/moksha-wallet/src/wallet.rs index 561eba85..2325072c 100644 --- a/moksha-wallet/src/wallet.rs +++ b/moksha-wallet/src/wallet.rs @@ -76,6 +76,59 @@ where self } + #[cfg(target_os = "espidf")] + pub fn build(self) -> Result, MokshaWalletError> { + let client = self.client.unwrap_or_default(); + let localstore = self.localstore.expect("localstore is required"); + let mint_url = self.mint_url.expect("mint_url is required"); + + if !client.is_v1_supported(&mint_url).unwrap() { + return Err(MokshaWalletError::UnsupportedApiVersion); + } + + let load_keysets = localstore.get_keysets().unwrap(); + + let mint_keysets = client.get_keysets(&mint_url).unwrap(); + if load_keysets.is_empty() { + let wallet_keysets = mint_keysets + .keysets + .iter() + .map(|m| WalletKeyset { + id: m.clone().id, + mint_url: mint_url.to_string(), + }) + .collect::>(); + + for wkeyset in wallet_keysets { + localstore.add_keyset(&wkeyset); + } + } + + // FIXME store all keysets + let keys = client.get_keys(&mint_url).unwrap(); + + let key_response = keys + .keysets + .iter() + .find(|k| k.id.starts_with("00")) + .expect("no valid keyset found"); + + let mks = mint_keysets + .keysets + .iter() + .find(|k| k.id.starts_with("00")) + .expect("no valid keyset found"); + + Ok(Wallet::new( + client as C, + mks.clone(), + key_response.clone(), + localstore, + mint_url, + )) + } + + #[cfg(target_arch = "wasm32")] pub async fn build(self) -> Result, MokshaWalletError> { let client = self.client.unwrap_or_default(); let localstore = self.localstore.expect("localstore is required"); @@ -164,6 +217,7 @@ where WalletBuilder::default() } + #[cfg(not(target_os = "espidf"))] pub async fn create_quote_bolt11( &self, amount: u64, @@ -172,7 +226,17 @@ where .post_mint_quote_bolt11(&self.mint_url, amount, CurrencyUnit::Sat) .await } + #[cfg(target_os = "espidf")] + pub fn create_quote_bolt11( + &self, + amount: u64, + ) -> Result { + self.client + .post_mint_quote_bolt11(&self.mint_url, amount, CurrencyUnit::Sat) + } + + #[cfg(not(target_os = "espidf"))] pub async fn create_quote_onchain( &self, amount: u64, @@ -181,7 +245,16 @@ where .post_mint_quote_onchain(&self.mint_url, amount, CurrencyUnit::Sat) .await } + #[cfg(target_os = "espidf")] + pub fn create_quote_onchain( + &self, + amount: u64, + ) -> Result { + self.client + .post_mint_quote_onchain(&self.mint_url, amount, CurrencyUnit::Sat) + } + #[cfg(not(target_os = "espidf"))] pub async fn is_quote_paid( &self, payment_method: &PaymentMethod, @@ -203,7 +276,30 @@ where } }) } + #[cfg(target_os = "espidf")] + pub fn is_quote_paid( + &self, + payment_method: &PaymentMethod, + quote: String, + ) -> Result { + Ok(match payment_method { + PaymentMethod::Bolt11 => { + self.client + .get_mint_quote_bolt11(&self.mint_url, quote) + .unwrap() + .paid + } + + PaymentMethod::BtcOnchain => { + self.client + .get_mint_quote_onchain(&self.mint_url, quote) + .unwrap() + .paid + } + }) + } + #[cfg(not(target_os = "espidf"))] pub async fn is_onchain_paid(&self, quote: String) -> Result { Ok(self .client @@ -211,7 +307,16 @@ where .await? .paid) } + #[cfg(target_os = "espidf")] + pub fn is_onchain_paid(&self, quote: String) -> Result { + Ok(self + .client + .get_melt_quote_onchain(&self.mint_url, quote) + .unwrap() + .paid) + } + #[cfg(not(target_os = "espidf"))] pub async fn is_onchain_tx_paid(&self, txid: String) -> Result { Ok(self .client @@ -219,12 +324,27 @@ where .await? .paid) } + #[cfg(target_os = "espidf")] + pub async fn is_onchain_tx_paid(&self, txid: String) -> Result { + Ok(self + .client + .get_melt_onchain(&self.mint_url, txid) + .unwrap() + .paid) + } + #[cfg(not(target_os = "espidf"))] pub async fn get_balance(&self) -> Result { let mut tx = self.localstore.begin_tx().await?; Ok(self.localstore.get_proofs(&mut tx).await?.total_amount()) } + #[cfg(target_os = "espidf")] + pub fn get_balance(&self) -> Result { + let mut tx = self.localstore.begin_tx().await?; + Ok(self.localstore.get_proofs(&mut tx).unwrap().total_amount()) + } + #[cfg(not(target_os = "espidf"))] pub async fn send_tokens(&self, amount: u64) -> Result { let balance = self.get_balance().await?; if amount > balance { @@ -248,6 +368,30 @@ where Ok(result) } + #[cfg(target_os = "espidf")] + pub fn send_tokens(&self, amount: u64) -> Result { + let balance = self.get_balance().unwrap(); + if amount > balance { + return Err(MokshaWalletError::NotEnoughTokens); + } + + let mut tx = self.localstore.begin_tx().unwrap(); + let all_proofs = self.localstore.get_proofs(&mut tx).unwrap(); + let selected_proofs = all_proofs.proofs_for_amount(amount)?; + let selected_tokens = (self.mint_url.to_owned(), selected_proofs.clone()).into(); + + let (remaining_tokens, result) = self.split_tokens(&selected_tokens, amount.into()).unwrap(); + + self.localstore + .delete_proofs(&mut tx, &selected_proofs) + .unwrap(); + self.localstore + .add_proofs(&mut tx, &remaining_tokens.proofs()) + .unwrap(); + tx.commit().unwrap(); + + Ok(result) + } pub async fn receive_tokens(&self, tokens: &TokenV3) -> Result<(), MokshaWalletError> { let total_amount = tokens.total_amount();