Skip to content

Commit 09f84cd

Browse files
committed
Implement a minimal BTC/USD price feed feature
1 parent 82493f4 commit 09f84cd

File tree

4 files changed

+91
-0
lines changed

4 files changed

+91
-0
lines changed

crates/cli-client/src/cli/positions.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::config::Config;
99
use crate::error::Error;
1010
use crate::metadata::ContractMetadata;
1111

12+
use crate::price_fetcher::{CoingeckoPriceFetcher, PriceFetcherError, fetch_btc_usd_price};
1213
use coin_store::{Store, UtxoEntry, UtxoFilter, UtxoQueryResult, UtxoStore};
1314
use contracts::option_offer::{OPTION_OFFER_SOURCE, OptionOfferArguments, get_option_offer_address};
1415
use contracts::options::{OPTION_SOURCE, OptionsArguments, get_options_address};
@@ -19,13 +20,32 @@ use simplicityhl::elements::Address;
1920
type ContractInfoResult = Result<Option<(Vec<u8>, Vec<u8>, String)>, coin_store::StoreError>;
2021

2122
impl Cli {
23+
#[allow(clippy::too_many_lines)]
2224
pub(crate) async fn run_positions(&self, config: Config) -> Result<(), Error> {
2325
let wallet = self.get_wallet(&config).await?;
2426

2527
println!("Your Positions:");
2628
println!("===============");
2729
println!();
2830

31+
let fetcher = CoingeckoPriceFetcher::new();
32+
let btc_result = tokio::task::spawn_blocking(move || fetch_btc_usd_price(&fetcher))
33+
.await
34+
.unwrap_or_else(|e| Err(PriceFetcherError::Internal(e.to_string())));
35+
36+
let btc_price = match btc_result {
37+
Ok(price) => format!("{price:.2}$"),
38+
Err(PriceFetcherError::RateLimit) => "Rate limit exceeded".to_string(),
39+
Err(e) => {
40+
eprintln!("Fetcher error: {e}");
41+
"Price fetcher service unavailable".to_string()
42+
}
43+
};
44+
45+
println!("Current btc price: {btc_price}");
46+
println!("-----------------------------");
47+
println!();
48+
2949
let user_script_pubkey = wallet.signer().p2pk_address(config.address_params())?.script_pubkey();
3050

3151
let options_filter = UtxoFilter::new().source(OPTION_SOURCE);

crates/cli-client/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,7 @@ pub enum Error {
5555

5656
#[error("Taproot pubkey generation error: {0}")]
5757
TaprootPubkeyGen(#[from] contracts::error::TaprootPubkeyGenError),
58+
59+
#[error("Price feed error: {0}")]
60+
PriceFeed(#[from] crate::price_fetcher::PriceFetcherError),
5861
}

crates/cli-client/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod explorer;
77
mod fee;
88
mod logging;
99
mod metadata;
10+
mod price_fetcher;
1011
mod signing;
1112
mod sync;
1213
mod wallet;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use serde::Deserialize;
2+
use thiserror::Error;
3+
4+
#[derive(Error, Debug)]
5+
pub enum PriceFetcherError {
6+
#[error("Request error: {0}")]
7+
Request(#[from] minreq::Error),
8+
#[error("Rate limit exceeded (429)")]
9+
RateLimit,
10+
#[error("Response status error: {0}")]
11+
Status(i32),
12+
#[error("Parse error: {0}")]
13+
Parse(String),
14+
#[error("Internal runtime error: {0}")]
15+
Internal(String),
16+
}
17+
18+
#[derive(Deserialize)]
19+
struct BitcoinResponse {
20+
bitcoin: BitcoinPrice,
21+
}
22+
23+
#[derive(Deserialize)]
24+
struct BitcoinPrice {
25+
usd: f64,
26+
}
27+
28+
pub trait PriceFetcher {
29+
fn fetch_price(&self) -> Result<f64, PriceFetcherError>;
30+
}
31+
32+
pub struct CoingeckoPriceFetcher;
33+
34+
impl CoingeckoPriceFetcher {
35+
const URL: &'static str = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&precision=8";
36+
const TIMEOUT_SECS: u64 = 5;
37+
38+
#[must_use]
39+
pub fn new() -> Self {
40+
Self
41+
}
42+
}
43+
44+
impl PriceFetcher for CoingeckoPriceFetcher {
45+
fn fetch_price(&self) -> Result<f64, PriceFetcherError> {
46+
let resp = minreq::get(Self::URL)
47+
.with_header("User-Agent", "simplicity-dex/1.0")
48+
.with_timeout(Self::TIMEOUT_SECS)
49+
.send()
50+
.map_err(PriceFetcherError::from)?;
51+
52+
let price = match resp.status_code {
53+
200 => resp
54+
.json::<BitcoinResponse>()
55+
.map(|data| data.bitcoin.usd)
56+
.map_err(|e| PriceFetcherError::Parse(e.to_string()))?,
57+
429 => return Err(PriceFetcherError::RateLimit),
58+
status => return Err(PriceFetcherError::Status(status)),
59+
};
60+
61+
Ok(price)
62+
}
63+
}
64+
65+
pub fn fetch_btc_usd_price<T: PriceFetcher>(fetcher: &T) -> Result<f64, PriceFetcherError> {
66+
fetcher.fetch_price()
67+
}

0 commit comments

Comments
 (0)