Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse reqwest client across codebase #990

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
4 changes: 2 additions & 2 deletions mutiny-core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct CustomClaims {

pub struct MutinyAuthClient {
pub auth: AuthManager,
lnurl_client: Arc<LnUrlClient>,
pub lnurl_client: Arc<LnUrlClient>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nack

not only should you not be reusing an internal client to MutinyAuth for this, but you should be using an internal client to an internal lnurl client library for this either. either have some generic http client that everyone uses at the top level or let things be as is.

url: String,
http_client: Client,
jwt: RwLock<Option<String>>,
Expand All @@ -38,7 +38,7 @@ impl MutinyAuthClient {
logger: Arc<MutinyLogger>,
url: String,
) -> Self {
let http_client = Client::new();
let http_client = lnurl_client.client.clone();
Self {
auth,
lnurl_client,
Expand Down
192 changes: 161 additions & 31 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -730,28 +732,34 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
);
}

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
.get_bitcoin_price_cache()?
.into_iter()
.map(|(k, v)| (k, (v, Duration::from_secs(0))))
.collect();

let mw = MutinyWallet {
xprivkey: self.xprivkey,
Expand All @@ -767,6 +775,7 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
stop,
logger,
network,
bitcoin_price_cache: Arc::new(RwLock::new(price_cache)),
skip_hodl_invoices: self.skip_hodl_invoices,
safe_mode: self.safe_mode,
};
Expand Down Expand Up @@ -822,6 +831,7 @@ pub struct MutinyWallet<S: MutinyStorage> {
pub stop: Arc<AtomicBool>,
pub logger: Arc<MutinyLogger>,
network: Network,
bitcoin_price_cache: Arc<RwLock<HashMap<String, (f32, Duration)>>>,
skip_hodl_invoices: bool,
safe_mode: bool,
}
Expand Down Expand Up @@ -1384,11 +1394,10 @@ impl<S: MutinyStorage> MutinyWallet<S> {
}

/// Makes a request to the primal api
async fn primal_request(
client: &reqwest::Client,
url: &str,
body: Value,
) -> Result<Vec<Value>, MutinyError> {
async fn primal_request(&self, url: &str, body: Value) -> Result<Vec<Value>, 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")
Expand All @@ -1408,10 +1417,9 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.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<Value> = Self::primal_request(&client, url, body).await?;
let data: Vec<Value> = self.primal_request(url, body).await?;
let mut metadata = parse_profile_metadata(data);

let contacts = self.storage.get_contacts()?;
Expand All @@ -1430,7 +1438,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {

if !missing_pks.is_empty() {
let body = json!(["user_infos", {"pubkeys": missing_pks }]);
let data: Vec<Value> = Self::primal_request(&client, url, body).await?;
let data: Vec<Value> = self.primal_request(url, body).await?;
let missing_metadata = parse_profile_metadata(data);
metadata.extend(missing_metadata);
}
Expand Down Expand Up @@ -1488,7 +1496,6 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.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) {
Expand All @@ -1505,7 +1512,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
json!(["get_directmsgs", { "sender": npub.to_hex(), "receiver": self.nostr.public_key.to_hex(), "limit": limit, "since": 0 }])
}
};
let data: Vec<Value> = Self::primal_request(&client, url, body).await?;
let data: Vec<Value> = self.primal_request(url, body).await?;

let mut messages = Vec::with_capacity(data.len());
for d in data {
Expand Down Expand Up @@ -1549,6 +1556,124 @@ impl<S: MutinyStorage> MutinyWallet<S> {
self.node_manager.stop().await
}

/// Gets the current bitcoin price in USD.
pub async fn get_bitcoin_price(&self, fiat: Option<String>) -> Result<f32, MutinyError> {
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();
let client = self.lnurl_client.client.clone();
spawn(async move {
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:?}");
}
});
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(
&self.lnurl_client.client,
fiat,
now,
self.bitcoin_price_cache.clone(),
self.storage.clone(),
self.logger.clone(),
)
.await
}
}
}

async fn fetch_and_cache_price(
client: &reqwest::Client,
fiat: String,
now: Duration,
bitcoin_price_cache: Arc<RwLock<HashMap<String, (f32, Duration)>>>,
storage: S,
logger: Arc<MutinyLogger>,
) -> Result<f32, MutinyError> {
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);
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(client: &reqwest::Client, fiat: &str) -> Result<f32, MutinyError> {
let api_url = format!("https://price.mutinywallet.com/price/{fiat}");

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<String>,
Expand Down Expand Up @@ -2066,6 +2191,11 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
})
}

#[derive(Deserialize, Clone, Copy, Debug)]
struct BitcoinPriceResponse {
pub price: f32,
}

#[cfg(test)]
mod tests {
use crate::{
Expand Down
Loading
Loading