From 818751b0dd9c457b6411ee11ae5fefcf8bc77267 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 25 Jan 2024 18:37:40 +0000 Subject: [PATCH 1/2] Move bitcoin price to lib.rs --- mutiny-core/src/lib.rs | 130 ++++++++++++++++++++++++++++++++- mutiny-core/src/nodemanager.rs | 130 +-------------------------------- mutiny-wasm/src/lib.rs | 2 +- 3 files changed, 131 insertions(+), 131 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 690cbf7bc..0167be8b3 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -89,16 +89,18 @@ use nostr_sdk::{Client, RelayPoolNotification}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; +use std::time::Duration; use std::{collections::HashMap, sync::atomic::AtomicBool}; use std::{str::FromStr, sync::atomic::Ordering}; use uuid::Uuid; use crate::labels::LabelItem; use crate::nostr::NostrKeySource; -use crate::utils::parse_profile_metadata; +use crate::utils::{parse_profile_metadata, spawn}; #[cfg(test)] use mockall::{automock, predicate::*}; +const BITCOIN_PRICE_CACHE_SEC: u64 = 300; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; #[cfg_attr(test, automock)] @@ -753,6 +755,13 @@ impl MutinyWalletBuilder { (None, auth_manager) }; + let price_cache = self + .storage + .get_bitcoin_price_cache()? + .into_iter() + .map(|(k, v)| (k, (v, Duration::from_secs(0)))) + .collect(); + let mw = MutinyWallet { xprivkey: self.xprivkey, config, @@ -767,6 +776,7 @@ impl MutinyWalletBuilder { stop, logger, network, + bitcoin_price_cache: Arc::new(RwLock::new(price_cache)), skip_hodl_invoices: self.skip_hodl_invoices, safe_mode: self.safe_mode, }; @@ -822,6 +832,7 @@ pub struct MutinyWallet { pub stop: Arc, pub logger: Arc, network: Network, + bitcoin_price_cache: Arc>>, skip_hodl_invoices: bool, safe_mode: bool, } @@ -1549,6 +1560,118 @@ impl MutinyWallet { self.node_manager.stop().await } + /// Gets the current bitcoin price in USD. + pub async fn get_bitcoin_price(&self, fiat: Option) -> Result { + let now = utils::now(); + let fiat = fiat.unwrap_or("usd".to_string()); + + let cache_result = { + let cache = self.bitcoin_price_cache.read().await; + cache.get(&fiat).copied() + }; + + match cache_result { + Some((price, timestamp)) if timestamp == Duration::from_secs(0) => { + // Cache is from previous run, return it but fetch a new price in the background + let cache = self.bitcoin_price_cache.clone(); + let storage = self.storage.clone(); + let logger = self.logger.clone(); + spawn(async move { + if let Err(e) = + Self::fetch_and_cache_price(fiat, now, cache, storage, logger.clone()).await + { + log_warn!(logger, "failed to fetch bitcoin price: {e:?}"); + } + }); + Ok(price) + } + Some((price, timestamp)) + if timestamp + Duration::from_secs(BITCOIN_PRICE_CACHE_SEC) > now => + { + // Cache is not expired + Ok(price) + } + _ => { + // Cache is either expired, empty, or doesn't have the desired fiat value + Self::fetch_and_cache_price( + fiat, + now, + self.bitcoin_price_cache.clone(), + self.storage.clone(), + self.logger.clone(), + ) + .await + } + } + } + + async fn fetch_and_cache_price( + fiat: String, + now: Duration, + bitcoin_price_cache: Arc>>, + storage: S, + logger: Arc, + ) -> Result { + match Self::fetch_bitcoin_price(&fiat).await { + Ok(new_price) => { + let mut cache = bitcoin_price_cache.write().await; + let cache_entry = (new_price, now); + cache.insert(fiat, cache_entry); + + // save to storage in the background + let cache_clone = cache.clone(); + spawn(async move { + let cache = cache_clone + .into_iter() + .map(|(k, (price, _))| (k, price)) + .collect(); + + if let Err(e) = storage.insert_bitcoin_price_cache(cache) { + log_error!(logger, "failed to save bitcoin price cache: {e:?}"); + } + }); + + Ok(new_price) + } + Err(e) => { + // If fetching price fails, return the cached price (if any) + let cache = bitcoin_price_cache.read().await; + if let Some((price, _)) = cache.get(&fiat) { + log_warn!(logger, "price api failed, returning cached price"); + Ok(*price) + } else { + // If there is no cached price, return the error + log_error!(logger, "no cached price and price api failed for {fiat}"); + Err(e) + } + } + } + } + + async fn fetch_bitcoin_price(fiat: &str) -> Result { + let api_url = format!("https://price.mutinywallet.com/price/{fiat}"); + + let client = reqwest::Client::builder() + .build() + .map_err(|_| MutinyError::BitcoinPriceError)?; + + let request = client + .get(api_url) + .build() + .map_err(|_| MutinyError::BitcoinPriceError)?; + + let resp: reqwest::Response = utils::fetch_with_timeout(&client, request).await?; + + let response: BitcoinPriceResponse = resp + .error_for_status() + .map_err(|_| MutinyError::BitcoinPriceError)? + .json() + .await + .map_err(|_| MutinyError::BitcoinPriceError)?; + + Ok(response.price) + } + pub async fn change_password( &mut self, old: Option, @@ -2066,6 +2189,11 @@ pub(crate) async fn create_new_federation( }) } +#[derive(Deserialize, Clone, Copy, Debug)] +struct BitcoinPriceResponse { + pub price: f32, +} + #[cfg(test)] mod tests { use crate::{ diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index e0a15ccb1..d698d04e2 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -1,7 +1,7 @@ use crate::event::HTLCStatus; use crate::labels::LabelStorage; use crate::logging::LOGGING_KEY; -use crate::utils::{sleep, spawn}; +use crate::utils::sleep; use crate::ActivityItem; use crate::MutinyInvoice; use crate::MutinyWalletConfig; @@ -33,7 +33,6 @@ use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; -use core::time::Duration; use esplora_client::{AsyncClient, Builder}; use futures::{future::join_all, lock::Mutex}; use lightning::chain::Confirm; @@ -58,7 +57,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::{collections::HashMap, ops::Deref, sync::Arc}; use uuid::Uuid; -const BITCOIN_PRICE_CACHE_SEC: u64 = 300; pub const DEVICE_LOCK_INTERVAL_SECS: u64 = 30; // This is the NodeStorage object saved to the DB @@ -456,13 +454,6 @@ impl NodeManagerBuilder { Arc::new(Mutex::new(nodes_map)) }; - let price_cache = self - .storage - .get_bitcoin_price_cache()? - .into_iter() - .map(|(k, v)| (k, (v, Duration::from_secs(0)))) - .collect(); - let nm = NodeManager { stop, xprivkey: self.xprivkey, @@ -481,7 +472,6 @@ impl NodeManagerBuilder { esplora, lsp_config, logger, - bitcoin_price_cache: Arc::new(Mutex::new(price_cache)), do_not_connect_peers: c.do_not_connect_peers, safe_mode: c.safe_mode, }; @@ -515,7 +505,6 @@ pub struct NodeManager { pub(crate) nodes: Arc>>>>, pub(crate) lsp_config: Option, pub(crate) logger: Arc, - bitcoin_price_cache: Arc>>, do_not_connect_peers: bool, pub safe_mode: bool, } @@ -1739,118 +1728,6 @@ impl NodeManager { Ok(storage_peers) } - /// Gets the current bitcoin price in USD. - pub async fn get_bitcoin_price(&self, fiat: Option) -> Result { - let now = crate::utils::now(); - let fiat = fiat.unwrap_or("usd".to_string()); - - let cache_result = { - let cache = self.bitcoin_price_cache.lock().await; - cache.get(&fiat).cloned() - }; - - match cache_result { - Some((price, timestamp)) if timestamp == Duration::from_secs(0) => { - // Cache is from previous run, return it but fetch a new price in the background - let cache = self.bitcoin_price_cache.clone(); - let storage = self.storage.clone(); - let logger = self.logger.clone(); - spawn(async move { - if let Err(e) = - Self::fetch_and_cache_price(fiat, now, cache, storage, logger.clone()).await - { - log_warn!(logger, "failed to fetch bitcoin price: {e:?}"); - } - }); - Ok(price) - } - Some((price, timestamp)) - if timestamp + Duration::from_secs(BITCOIN_PRICE_CACHE_SEC) > now => - { - // Cache is not expired - Ok(price) - } - _ => { - // Cache is either expired, empty, or doesn't have the desired fiat value - Self::fetch_and_cache_price( - fiat, - now, - self.bitcoin_price_cache.clone(), - self.storage.clone(), - self.logger.clone(), - ) - .await - } - } - } - - async fn fetch_and_cache_price( - fiat: String, - now: Duration, - bitcoin_price_cache: Arc>>, - storage: S, - logger: Arc, - ) -> Result { - match Self::fetch_bitcoin_price(&fiat).await { - Ok(new_price) => { - let mut cache = bitcoin_price_cache.lock().await; - let cache_entry = (new_price, now); - cache.insert(fiat.clone(), cache_entry); - - // save to storage in the background - let cache_clone = cache.clone(); - spawn(async move { - let cache = cache_clone - .into_iter() - .map(|(k, (price, _))| (k, price)) - .collect(); - - if let Err(e) = storage.insert_bitcoin_price_cache(cache) { - log_error!(logger, "failed to save bitcoin price cache: {e:?}"); - } - }); - - Ok(new_price) - } - Err(e) => { - // If fetching price fails, return the cached price (if any) - let cache = bitcoin_price_cache.lock().await; - if let Some((price, _)) = cache.get(&fiat) { - log_warn!(logger, "price api failed, returning cached price"); - Ok(*price) - } else { - // If there is no cached price, return the error - log_error!(logger, "no cached price and price api failed for {fiat}"); - Err(e) - } - } - } - } - - async fn fetch_bitcoin_price(fiat: &str) -> Result { - let api_url = format!("https://price.mutinywallet.com/price/{fiat}"); - - let client = Client::builder() - .build() - .map_err(|_| MutinyError::BitcoinPriceError)?; - - let request = client - .get(api_url) - .build() - .map_err(|_| MutinyError::BitcoinPriceError)?; - - let resp: reqwest::Response = utils::fetch_with_timeout(&client, request).await?; - - let response: BitcoinPriceResponse = resp - .error_for_status() - .map_err(|_| MutinyError::BitcoinPriceError)? - .json() - .await - .map_err(|_| MutinyError::BitcoinPriceError)?; - - Ok(response.price) - } - /// Retrieves the logs from storage. pub fn get_logs( storage: S, @@ -1930,11 +1807,6 @@ impl NodeManager { } } -#[derive(Deserialize, Clone, Copy, Debug)] -struct BitcoinPriceResponse { - pub price: f32, -} - // This will create a new node with a node manager and return the PublicKey of the node created. pub(crate) async fn create_new_node_from_node_manager( node_manager: &NodeManager, diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index fd34a152b..9b68ec632 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1288,7 +1288,7 @@ impl MutinyWallet { /// Gets the current bitcoin price in chosen Fiat. #[wasm_bindgen] pub async fn get_bitcoin_price(&self, fiat: Option) -> Result { - Ok(self.inner.node_manager.get_bitcoin_price(fiat).await?) + Ok(self.inner.get_bitcoin_price(fiat).await?) } /// Exports the current state of the node manager to a json object. From 701e167f90a61d837d2fd771d6bc319319e49293 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 25 Jan 2024 18:42:55 +0000 Subject: [PATCH 2/2] Reuse reqwest client across codebase --- Cargo.lock | 4 +-- mutiny-core/Cargo.toml | 2 +- mutiny-core/src/auth.rs | 4 +-- mutiny-core/src/lib.rs | 80 +++++++++++++++++++++-------------------- mutiny-wasm/Cargo.toml | 2 +- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f2b9aa65..1857c655d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,9 +2087,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lnurl-rs" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa42d2e0488393c00b96d0ea170eb6f9acbda7f965690fcd40ce6447517db7" +checksum = "3912d662eeb78043d92de3c36e3839ec831e4b224be194864a5f4057e34854e2" dependencies = [ "aes", "anyhow", diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index ddbb2bb6c..8e7670392 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -12,7 +12,7 @@ homepage = "https://mutinywallet.com" repository = "https://github.com/mutinywallet/mutiny-node" [dependencies] -lnurl-rs = { version = "0.3.1", default-features = false, features = ["async", "async-https"] } +lnurl-rs = { version = "0.3.2", default-features = false, features = ["async", "async-https"] } cfg-if = "1.0.0" bip39 = { version = "2.0.0" } diff --git a/mutiny-core/src/auth.rs b/mutiny-core/src/auth.rs index b3caed607..3690ac2e6 100644 --- a/mutiny-core/src/auth.rs +++ b/mutiny-core/src/auth.rs @@ -24,7 +24,7 @@ struct CustomClaims { pub struct MutinyAuthClient { pub auth: AuthManager, - lnurl_client: Arc, + pub lnurl_client: Arc, url: String, http_client: Client, jwt: RwLock>, @@ -38,7 +38,7 @@ impl MutinyAuthClient { logger: Arc, url: String, ) -> Self { - let http_client = Client::new(); + let http_client = lnurl_client.client.clone(); Self { auth, lnurl_client, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 0167be8b3..56b525d26 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -732,28 +732,27 @@ impl MutinyWalletBuilder { ); } - let lnurl_client = Arc::new( - lnurl::Builder::default() - .build_async() - .expect("failed to make lnurl client"), - ); - - let (subscription_client, auth) = if let Some(auth_client) = self.auth_client.clone() { - if let Some(subscription_url) = self.subscription_url { + let (subscription_client, auth, lnurl_client) = + if let Some(auth_client) = self.auth_client.clone() { let auth = auth_client.auth.clone(); - let s = Arc::new(MutinySubscriptionClient::new( - auth_client, - subscription_url, - logger.clone(), - )); - (Some(s), auth) + let lnurl = auth_client.lnurl_client.clone(); + let sub = self.subscription_url.map(|url| { + Arc::new(MutinySubscriptionClient::new( + auth_client, + url, + logger.clone(), + )) + }); + (sub, auth, lnurl) } else { - (None, auth_client.auth.clone()) - } - } else { - let auth_manager = AuthManager::new(self.xprivkey)?; - (None, auth_manager) - }; + let auth_manager = AuthManager::new(self.xprivkey)?; + let lnurl_client = Arc::new( + lnurl::Builder::default() + .build_async() + .expect("failed to make lnurl client"), + ); + (None, auth_manager, lnurl_client) + }; let price_cache = self .storage @@ -1395,11 +1394,10 @@ impl MutinyWallet { } /// Makes a request to the primal api - async fn primal_request( - client: &reqwest::Client, - url: &str, - body: Value, - ) -> Result, MutinyError> { + async fn primal_request(&self, url: &str, body: Value) -> Result, MutinyError> { + // just use lnurl_client's request client so we don't have to initialize another one + let client = &self.lnurl_client.client; + client .post(url) .header("Content-Type", "application/json") @@ -1419,10 +1417,9 @@ impl MutinyWallet { .primal_url .as_deref() .unwrap_or("https://primal-cache.mutinywallet.com/api"); - let client = reqwest::Client::new(); let body = json!(["contact_list", { "pubkey": npub } ]); - let data: Vec = Self::primal_request(&client, url, body).await?; + let data: Vec = self.primal_request(url, body).await?; let mut metadata = parse_profile_metadata(data); let contacts = self.storage.get_contacts()?; @@ -1441,7 +1438,7 @@ impl MutinyWallet { if !missing_pks.is_empty() { let body = json!(["user_infos", {"pubkeys": missing_pks }]); - let data: Vec = Self::primal_request(&client, url, body).await?; + let data: Vec = self.primal_request(url, body).await?; let missing_metadata = parse_profile_metadata(data); metadata.extend(missing_metadata); } @@ -1499,7 +1496,6 @@ impl MutinyWallet { .primal_url .as_deref() .unwrap_or("https://primal-cache.mutinywallet.com/api"); - let client = reqwest::Client::new(); // api is a little weird, has sender and receiver but still gives full conversation let body = match (until, since) { @@ -1516,7 +1512,7 @@ impl MutinyWallet { json!(["get_directmsgs", { "sender": npub.to_hex(), "receiver": self.nostr.public_key.to_hex(), "limit": limit, "since": 0 }]) } }; - let data: Vec = Self::primal_request(&client, url, body).await?; + let data: Vec = self.primal_request(url, body).await?; let mut messages = Vec::with_capacity(data.len()); for d in data { @@ -1576,9 +1572,17 @@ impl MutinyWallet { let cache = self.bitcoin_price_cache.clone(); let storage = self.storage.clone(); let logger = self.logger.clone(); + let client = self.lnurl_client.client.clone(); spawn(async move { - if let Err(e) = - Self::fetch_and_cache_price(fiat, now, cache, storage, logger.clone()).await + if let Err(e) = Self::fetch_and_cache_price( + &client, + fiat, + now, + cache, + storage, + logger.clone(), + ) + .await { log_warn!(logger, "failed to fetch bitcoin price: {e:?}"); } @@ -1594,6 +1598,7 @@ impl MutinyWallet { _ => { // Cache is either expired, empty, or doesn't have the desired fiat value Self::fetch_and_cache_price( + &self.lnurl_client.client, fiat, now, self.bitcoin_price_cache.clone(), @@ -1606,13 +1611,14 @@ impl MutinyWallet { } async fn fetch_and_cache_price( + client: &reqwest::Client, fiat: String, now: Duration, bitcoin_price_cache: Arc>>, storage: S, logger: Arc, ) -> Result { - match Self::fetch_bitcoin_price(&fiat).await { + match Self::fetch_bitcoin_price(client, &fiat).await { Ok(new_price) => { let mut cache = bitcoin_price_cache.write().await; let cache_entry = (new_price, now); @@ -1648,19 +1654,15 @@ impl MutinyWallet { } } - async fn fetch_bitcoin_price(fiat: &str) -> Result { + async fn fetch_bitcoin_price(client: &reqwest::Client, fiat: &str) -> Result { let api_url = format!("https://price.mutinywallet.com/price/{fiat}"); - let client = reqwest::Client::builder() - .build() - .map_err(|_| MutinyError::BitcoinPriceError)?; - let request = client .get(api_url) .build() .map_err(|_| MutinyError::BitcoinPriceError)?; - let resp: reqwest::Response = utils::fetch_with_timeout(&client, request).await?; + let resp: reqwest::Response = utils::fetch_with_timeout(client, request).await?; let response: BitcoinPriceResponse = resp .error_for_status() diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index 3129cc47d..77ea254ae 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -29,7 +29,7 @@ lightning = { version = "0.0.118", default-features = false, features = ["std"] lightning-invoice = { version = "0.26.0" } thiserror = "1.0" instant = { version = "0.1", features = ["wasm-bindgen"] } -lnurl-rs = { version = "0.3.1", default-features = false } +lnurl-rs = { version = "0.3.2", default-features = false } nostr = { version = "0.27.0", default-features = false, features = ["nip04", "nip05", "nip07", "nip47", "nip57"] } wasm-logger = "0.2.0" log = "0.4.17"