Skip to content
Merged
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: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ config.toml
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

Expand Down
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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ contracts = { git = "https://github.com/BlockstreamResearch/simplicity-contracts
cli-helper = { git = "https://github.com/BlockstreamResearch/simplicity-contracts.git", rev = "61616fb", package = "cli" }
simplicityhl-core = { version = "0.4.0", features = ["encoding"] }

simplicityhl = { version = "0.4.0" }
simplicityhl = { version = "0.4.1" }

[patch.crates-io]
simplicity-sys = { git = "https://github.com/BlockstreamResearch/rust-simplicity", tag = "simplicity-sys-0.6.1" }
20 changes: 20 additions & 0 deletions crates/cli-client/src/cli/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::config::Config;
use crate::error::Error;
use crate::metadata::ContractMetadata;

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

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

println!("Your Positions:");
println!("===============");
println!();

let fetcher = CoingeckoPriceFetcher;
let btc_result = tokio::task::spawn_blocking(move || fetch_btc_usd_price(&fetcher))
.await
.unwrap_or_else(|e| Err(PriceFetcherError::Internal(e.to_string())));

let btc_price = match btc_result {
Ok(price) => format!("${price:.2}"),
Err(PriceFetcherError::RateLimit) => "Rate limit exceeded".to_string(),
Err(e) => {
eprintln!("Fetcher error: {e}");
"Price fetcher service unavailable".to_string()
}
};

println!("Current btc price: {btc_price}");
println!("-----------------------------");
println!();

let user_script_pubkey = wallet.signer().p2pk_address(config.address_params())?.script_pubkey();

let options_filter = UtxoFilter::new().source(OPTION_SOURCE);
Expand Down
3 changes: 3 additions & 0 deletions crates/cli-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ pub enum Error {

#[error("Taproot pubkey generation error: {0}")]
TaprootPubkeyGen(#[from] contracts::error::TaprootPubkeyGenError),

#[error("Price feed error: {0}")]
PriceFeed(#[from] crate::price_fetcher::PriceFetcherError),
}
1 change: 1 addition & 0 deletions crates/cli-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod explorer;
mod fee;
mod logging;
mod metadata;
mod price_fetcher;
mod signing;
mod sync;
mod wallet;
Expand Down
61 changes: 61 additions & 0 deletions crates/cli-client/src/price_fetcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use serde::Deserialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PriceFetcherError {
#[error("Request error: {0}")]
Request(#[from] minreq::Error),
#[error("Rate limit exceeded (429)")]
RateLimit,
#[error("Response status error: {0}")]
Status(i32),
#[error("Parse error: {0}")]
Parse(String),
#[error("Internal runtime error: {0}")]
Internal(String),
}

#[derive(Deserialize)]
struct BitcoinResponse {
bitcoin: BitcoinPrice,
}

#[derive(Deserialize)]
struct BitcoinPrice {
usd: f64,
}

pub trait PriceFetcher {
fn fetch_price(&self) -> Result<f64, PriceFetcherError>;
Comment on lines +28 to +29
Copy link

Choose a reason for hiding this comment

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

Trait uses synchronous blocking HTTP in async context. fetch_price() calls blocking minreq::get() which blocks the tokio runtime thread. Either mark trait method as async or use tokio::task::spawn_blocking() when calling it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/price_fetcher.rs
Line: 26:27

Comment:
Trait uses synchronous blocking HTTP in async context. `fetch_price()` calls blocking `minreq::get()` which blocks the tokio runtime thread. Either mark trait method as async or use `tokio::task::spawn_blocking()` when calling it.

How can I resolve this? If you propose a fix, please make it concise.

}
Comment on lines 28 to 30
Copy link

Choose a reason for hiding this comment

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

[P1] Trait returns crate-wide Error instead of PriceFetcherError

PriceFetcher::fetch_price() returning Result<f64, Error> makes the trait harder to reuse and mock because it couples the fetcher layer to the entire CLI error enum. Since this module already defines PriceFetcherError, consider making the trait return Result<f64, PriceFetcherError> and converting to crate::error::Error at the CLI boundary.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/price_fetcher.rs
Line: 27:29

Comment:
[P1] Trait returns crate-wide `Error` instead of `PriceFetcherError`

`PriceFetcher::fetch_price()` returning `Result<f64, Error>` makes the trait harder to reuse and mock because it couples the fetcher layer to the entire CLI error enum. Since this module already defines `PriceFetcherError`, consider making the trait return `Result<f64, PriceFetcherError>` and converting to `crate::error::Error` at the CLI boundary.


<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.


#[derive(Default)]
pub struct CoingeckoPriceFetcher;

impl CoingeckoPriceFetcher {
const URL: &'static str = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&precision=8";
const TIMEOUT_SECS: u64 = 5;
}

impl PriceFetcher for CoingeckoPriceFetcher {
fn fetch_price(&self) -> Result<f64, PriceFetcherError> {
let resp = minreq::get(Self::URL)
.with_header("User-Agent", "simplicity-dex/1.0")
.with_timeout(Self::TIMEOUT_SECS)
.send()
.map_err(PriceFetcherError::from)?;

match resp.status_code {
200 => resp
.json::<BitcoinResponse>()
.map(|data| data.bitcoin.usd)
.map_err(|e| PriceFetcherError::Parse(e.to_string())),
429 => Err(PriceFetcherError::RateLimit),
status => Err(PriceFetcherError::Status(status)),
}
}
}

pub fn fetch_btc_usd_price<T: PriceFetcher>(fetcher: &T) -> Result<f64, PriceFetcherError> {
fetcher.fetch_price()
}