Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit c98f9f1

Browse files
Merge pull request #1140 from MutinyWallet/mv-bitcoin-price
Move bitcoin price fetching to MutinyWallet
2 parents 014bac1 + 3c792a2 commit c98f9f1

File tree

4 files changed

+132
-131
lines changed

4 files changed

+132
-131
lines changed

mutiny-core/src/federation.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,6 @@ fn gateway_preference() {
11691169
use fedimint_core::util::SafeUrl;
11701170
use fedimint_ln_common::bitcoin::secp256k1::PublicKey;
11711171
use fedimint_ln_common::LightningGatewayAnnouncement;
1172-
use std::time::Duration;
11731172

11741173
use super::*;
11751174

mutiny-core/src/lib.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use crate::storage::{
5353
ONCHAIN_PREFIX, PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY,
5454
SUBSCRIPTION_TIMESTAMP,
5555
};
56+
use crate::utils::spawn;
5657
use crate::{auth::MutinyAuthClient, hermes::HermesClient, logging::MutinyLogger};
5758
use crate::{blindauth::BlindAuthClient, cashu::CashuHttpClient};
5859
use crate::{error::MutinyError, nostr::ReservedProfile};
@@ -97,6 +98,7 @@ use bitcoin::{hashes::sha256, Network, Txid};
9798
use fedimint_core::{api::InviteCode, config::FederationId};
9899
use futures::{pin_mut, select, FutureExt};
99100
use futures_util::join;
101+
use futures_util::lock::Mutex;
100102
use hex_conservative::{DisplayHex, FromHex};
101103
use itertools::Itertools;
102104
use lightning::chain::BestBlock;
@@ -116,6 +118,7 @@ use serde::{Deserialize, Serialize};
116118
use serde_json::Value;
117119
use std::collections::HashSet;
118120
use std::sync::Arc;
121+
use std::time::Duration;
119122
#[cfg(not(target_arch = "wasm32"))]
120123
use std::time::Instant;
121124
use std::{collections::HashMap, sync::atomic::AtomicBool};
@@ -129,6 +132,7 @@ use crate::nostr::{NostrKeySource, RELAYS};
129132
#[cfg(test)]
130133
use mockall::{automock, predicate::*};
131134

135+
const BITCOIN_PRICE_CACHE_SEC: u64 = 300;
132136
const DEFAULT_PAYMENT_TIMEOUT: u64 = 30;
133137
const MAX_FEDERATION_INVOICE_AMT: u64 = 200_000;
134138
const SWAP_LABEL: &str = "SWAP";
@@ -1038,6 +1042,13 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
10381042
read.extend(activity_index);
10391043
}
10401044

1045+
let price_cache = self
1046+
.storage
1047+
.get_bitcoin_price_cache()?
1048+
.into_iter()
1049+
.map(|(k, v)| (k, (v, Duration::from_secs(0))))
1050+
.collect();
1051+
10411052
let mw = MutinyWallet {
10421053
xprivkey: self.xprivkey,
10431054
config,
@@ -1057,6 +1068,7 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
10571068
skip_hodl_invoices: self.skip_hodl_invoices,
10581069
safe_mode: self.safe_mode,
10591070
cashu_client: CashuHttpClient::new(),
1071+
bitcoin_price_cache: Arc::new(Mutex::new(price_cache)),
10601072
};
10611073

10621074
// if we are in safe mode, don't create any nodes or
@@ -1127,6 +1139,7 @@ pub struct MutinyWallet<S: MutinyStorage> {
11271139
skip_hodl_invoices: bool,
11281140
safe_mode: bool,
11291141
cashu_client: CashuHttpClient,
1142+
bitcoin_price_cache: Arc<Mutex<HashMap<String, (f32, Duration)>>>,
11301143
}
11311144

11321145
impl<S: MutinyStorage> MutinyWallet<S> {
@@ -2912,6 +2925,118 @@ impl<S: MutinyStorage> MutinyWallet<S> {
29122925
});
29132926
}
29142927
}
2928+
2929+
/// Gets the current bitcoin price in USD.
2930+
pub async fn get_bitcoin_price(&self, fiat: Option<String>) -> Result<f32, MutinyError> {
2931+
let now = crate::utils::now();
2932+
let fiat = fiat.unwrap_or("usd".to_string());
2933+
2934+
let cache_result = {
2935+
let cache = self.bitcoin_price_cache.lock().await;
2936+
cache.get(&fiat).cloned()
2937+
};
2938+
2939+
match cache_result {
2940+
Some((price, timestamp)) if timestamp == Duration::from_secs(0) => {
2941+
// Cache is from previous run, return it but fetch a new price in the background
2942+
let cache = self.bitcoin_price_cache.clone();
2943+
let storage = self.storage.clone();
2944+
let logger = self.logger.clone();
2945+
spawn(async move {
2946+
if let Err(e) =
2947+
Self::fetch_and_cache_price(fiat, now, cache, storage, logger.clone()).await
2948+
{
2949+
log_warn!(logger, "failed to fetch bitcoin price: {e:?}");
2950+
}
2951+
});
2952+
Ok(price)
2953+
}
2954+
Some((price, timestamp))
2955+
if timestamp + Duration::from_secs(BITCOIN_PRICE_CACHE_SEC) > now =>
2956+
{
2957+
// Cache is not expired
2958+
Ok(price)
2959+
}
2960+
_ => {
2961+
// Cache is either expired, empty, or doesn't have the desired fiat value
2962+
Self::fetch_and_cache_price(
2963+
fiat,
2964+
now,
2965+
self.bitcoin_price_cache.clone(),
2966+
self.storage.clone(),
2967+
self.logger.clone(),
2968+
)
2969+
.await
2970+
}
2971+
}
2972+
}
2973+
2974+
async fn fetch_and_cache_price(
2975+
fiat: String,
2976+
now: Duration,
2977+
bitcoin_price_cache: Arc<Mutex<HashMap<String, (f32, Duration)>>>,
2978+
storage: S,
2979+
logger: Arc<MutinyLogger>,
2980+
) -> Result<f32, MutinyError> {
2981+
match Self::fetch_bitcoin_price(&fiat).await {
2982+
Ok(new_price) => {
2983+
let mut cache = bitcoin_price_cache.lock().await;
2984+
let cache_entry = (new_price, now);
2985+
cache.insert(fiat.clone(), cache_entry);
2986+
2987+
// save to storage in the background
2988+
let cache_clone = cache.clone();
2989+
spawn(async move {
2990+
let cache = cache_clone
2991+
.into_iter()
2992+
.map(|(k, (price, _))| (k, price))
2993+
.collect();
2994+
2995+
if let Err(e) = storage.insert_bitcoin_price_cache(cache) {
2996+
log_error!(logger, "failed to save bitcoin price cache: {e:?}");
2997+
}
2998+
});
2999+
3000+
Ok(new_price)
3001+
}
3002+
Err(e) => {
3003+
// If fetching price fails, return the cached price (if any)
3004+
let cache = bitcoin_price_cache.lock().await;
3005+
if let Some((price, _)) = cache.get(&fiat) {
3006+
log_warn!(logger, "price api failed, returning cached price");
3007+
Ok(*price)
3008+
} else {
3009+
// If there is no cached price, return the error
3010+
log_error!(logger, "no cached price and price api failed for {fiat}");
3011+
Err(e)
3012+
}
3013+
}
3014+
}
3015+
}
3016+
3017+
async fn fetch_bitcoin_price(fiat: &str) -> Result<f32, MutinyError> {
3018+
let api_url = format!("https://price.mutinywallet.com/price/{fiat}");
3019+
3020+
let client = reqwest::Client::builder()
3021+
.build()
3022+
.map_err(|_| MutinyError::BitcoinPriceError)?;
3023+
3024+
let request = client
3025+
.get(api_url)
3026+
.build()
3027+
.map_err(|_| MutinyError::BitcoinPriceError)?;
3028+
3029+
let resp: reqwest::Response = utils::fetch_with_timeout(&client, request).await?;
3030+
3031+
let response: BitcoinPriceResponse = resp
3032+
.error_for_status()
3033+
.map_err(|_| MutinyError::BitcoinPriceError)?
3034+
.json()
3035+
.await
3036+
.map_err(|_| MutinyError::BitcoinPriceError)?;
3037+
3038+
Ok(response.price)
3039+
}
29153040
}
29163041

29173042
impl<S: MutinyStorage> InvoiceHandler for MutinyWallet<S> {
@@ -3070,6 +3195,11 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
30703195
Ok(new_federation_identity)
30713196
}
30723197

3198+
#[derive(Deserialize, Clone, Copy, Debug)]
3199+
struct BitcoinPriceResponse {
3200+
pub price: f32,
3201+
}
3202+
30733203
#[derive(Deserialize)]
30743204
struct NostrBuildResult {
30753205
status: String,

mutiny-core/src/nodemanager.rs

Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ use bitcoin::hashes::sha256;
3535
use bitcoin::psbt::PartiallySignedTransaction;
3636
use bitcoin::secp256k1::PublicKey;
3737
use bitcoin::{Address, Network, OutPoint, Transaction, Txid};
38-
use core::time::Duration;
3938
use esplora_client::{AsyncClient, Builder};
40-
use futures::{future::join_all, lock::Mutex};
39+
use futures::future::join_all;
4140
use hex_conservative::DisplayHex;
4241
use lightning::chain::Confirm;
4342
use lightning::events::ClosureReason;
@@ -64,7 +63,6 @@ use std::{collections::HashMap, ops::Deref, sync::Arc};
6463
#[cfg(target_arch = "wasm32")]
6564
use web_time::Instant;
6665

67-
const BITCOIN_PRICE_CACHE_SEC: u64 = 300;
6866
pub const DEVICE_LOCK_INTERVAL_SECS: u64 = 30;
6967

7068
// This is the NodeStorage object saved to the DB
@@ -503,13 +501,6 @@ impl<S: MutinyStorage> NodeManagerBuilder<S> {
503501
Arc::new(RwLock::new(nodes_map))
504502
};
505503

506-
let price_cache = self
507-
.storage
508-
.get_bitcoin_price_cache()?
509-
.into_iter()
510-
.map(|(k, v)| (k, (v, Duration::from_secs(0))))
511-
.collect();
512-
513504
let nm = NodeManager {
514505
stop,
515506
xprivkey: self.xprivkey,
@@ -530,7 +521,6 @@ impl<S: MutinyStorage> NodeManagerBuilder<S> {
530521
esplora,
531522
lsp_config,
532523
logger,
533-
bitcoin_price_cache: Arc::new(Mutex::new(price_cache)),
534524
do_not_connect_peers: c.do_not_connect_peers,
535525
safe_mode: c.safe_mode,
536526
has_done_initial_ldk_sync: Arc::new(AtomicBool::new(false)),
@@ -567,7 +557,6 @@ pub struct NodeManager<S: MutinyStorage> {
567557
pub(crate) nodes: Arc<RwLock<HashMap<PublicKey, Arc<Node<S>>>>>,
568558
pub(crate) lsp_config: Option<LspConfig>,
569559
pub(crate) logger: Arc<MutinyLogger>,
570-
bitcoin_price_cache: Arc<Mutex<HashMap<String, (f32, Duration)>>>,
571560
do_not_connect_peers: bool,
572561
pub safe_mode: bool,
573562
/// If we've completed an initial sync this instance
@@ -1771,118 +1760,6 @@ impl<S: MutinyStorage> NodeManager<S> {
17711760
Ok(storage_peers)
17721761
}
17731762

1774-
/// Gets the current bitcoin price in USD.
1775-
pub async fn get_bitcoin_price(&self, fiat: Option<String>) -> Result<f32, MutinyError> {
1776-
let now = crate::utils::now();
1777-
let fiat = fiat.unwrap_or("usd".to_string());
1778-
1779-
let cache_result = {
1780-
let cache = self.bitcoin_price_cache.lock().await;
1781-
cache.get(&fiat).cloned()
1782-
};
1783-
1784-
match cache_result {
1785-
Some((price, timestamp)) if timestamp == Duration::from_secs(0) => {
1786-
// Cache is from previous run, return it but fetch a new price in the background
1787-
let cache = self.bitcoin_price_cache.clone();
1788-
let storage = self.storage.clone();
1789-
let logger = self.logger.clone();
1790-
spawn(async move {
1791-
if let Err(e) =
1792-
Self::fetch_and_cache_price(fiat, now, cache, storage, logger.clone()).await
1793-
{
1794-
log_warn!(logger, "failed to fetch bitcoin price: {e:?}");
1795-
}
1796-
});
1797-
Ok(price)
1798-
}
1799-
Some((price, timestamp))
1800-
if timestamp + Duration::from_secs(BITCOIN_PRICE_CACHE_SEC) > now =>
1801-
{
1802-
// Cache is not expired
1803-
Ok(price)
1804-
}
1805-
_ => {
1806-
// Cache is either expired, empty, or doesn't have the desired fiat value
1807-
Self::fetch_and_cache_price(
1808-
fiat,
1809-
now,
1810-
self.bitcoin_price_cache.clone(),
1811-
self.storage.clone(),
1812-
self.logger.clone(),
1813-
)
1814-
.await
1815-
}
1816-
}
1817-
}
1818-
1819-
async fn fetch_and_cache_price(
1820-
fiat: String,
1821-
now: Duration,
1822-
bitcoin_price_cache: Arc<Mutex<HashMap<String, (f32, Duration)>>>,
1823-
storage: S,
1824-
logger: Arc<MutinyLogger>,
1825-
) -> Result<f32, MutinyError> {
1826-
match Self::fetch_bitcoin_price(&fiat).await {
1827-
Ok(new_price) => {
1828-
let mut cache = bitcoin_price_cache.lock().await;
1829-
let cache_entry = (new_price, now);
1830-
cache.insert(fiat.clone(), cache_entry);
1831-
1832-
// save to storage in the background
1833-
let cache_clone = cache.clone();
1834-
spawn(async move {
1835-
let cache = cache_clone
1836-
.into_iter()
1837-
.map(|(k, (price, _))| (k, price))
1838-
.collect();
1839-
1840-
if let Err(e) = storage.insert_bitcoin_price_cache(cache) {
1841-
log_error!(logger, "failed to save bitcoin price cache: {e:?}");
1842-
}
1843-
});
1844-
1845-
Ok(new_price)
1846-
}
1847-
Err(e) => {
1848-
// If fetching price fails, return the cached price (if any)
1849-
let cache = bitcoin_price_cache.lock().await;
1850-
if let Some((price, _)) = cache.get(&fiat) {
1851-
log_warn!(logger, "price api failed, returning cached price");
1852-
Ok(*price)
1853-
} else {
1854-
// If there is no cached price, return the error
1855-
log_error!(logger, "no cached price and price api failed for {fiat}");
1856-
Err(e)
1857-
}
1858-
}
1859-
}
1860-
}
1861-
1862-
async fn fetch_bitcoin_price(fiat: &str) -> Result<f32, MutinyError> {
1863-
let api_url = format!("https://price.mutinywallet.com/price/{fiat}");
1864-
1865-
let client = Client::builder()
1866-
.build()
1867-
.map_err(|_| MutinyError::BitcoinPriceError)?;
1868-
1869-
let request = client
1870-
.get(api_url)
1871-
.build()
1872-
.map_err(|_| MutinyError::BitcoinPriceError)?;
1873-
1874-
let resp: reqwest::Response = utils::fetch_with_timeout(&client, request).await?;
1875-
1876-
let response: BitcoinPriceResponse = resp
1877-
.error_for_status()
1878-
.map_err(|_| MutinyError::BitcoinPriceError)?
1879-
.json()
1880-
.await
1881-
.map_err(|_| MutinyError::BitcoinPriceError)?;
1882-
1883-
Ok(response.price)
1884-
}
1885-
18861763
/// Retrieves the logs from storage.
18871764
pub fn get_logs(
18881765
storage: S,
@@ -1962,11 +1839,6 @@ impl<S: MutinyStorage> NodeManager<S> {
19621839
}
19631840
}
19641841

1965-
#[derive(Deserialize, Clone, Copy, Debug)]
1966-
struct BitcoinPriceResponse {
1967-
pub price: f32,
1968-
}
1969-
19701842
// This will create a new node with a node manager and return the PublicKey of the node created.
19711843
pub(crate) async fn create_new_node_from_node_manager<S: MutinyStorage>(
19721844
node_manager: &NodeManager<S>,

mutiny-wasm/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1525,7 +1525,7 @@ impl MutinyWallet {
15251525
/// Gets the current bitcoin price in chosen Fiat.
15261526
#[wasm_bindgen]
15271527
pub async fn get_bitcoin_price(&self, fiat: Option<String>) -> Result<f32, MutinyJsError> {
1528-
Ok(self.inner.node_manager.get_bitcoin_price(fiat).await?)
1528+
Ok(self.inner.get_bitcoin_price(fiat).await?)
15291529
}
15301530

15311531
/// Exports the current state of the node manager to a json object.

0 commit comments

Comments
 (0)