Skip to content

Implement price-feed feature#49

Merged
KyrylR merged 1 commit intomainfrom
feature/price-feed
Feb 4, 2026
Merged

Implement price-feed feature#49
KyrylR merged 1 commit intomainfrom
feature/price-feed

Conversation

@zahorodnyi
Copy link
Collaborator

@zahorodnyi zahorodnyi commented Jan 15, 2026

Usage: cargo run -p cli-client price-feed

Fix: #40

@zahorodnyi zahorodnyi self-assigned this Jan 15, 2026
@zahorodnyi zahorodnyi requested a review from KyrylR as a code owner January 15, 2026 18:25
@zahorodnyi zahorodnyi force-pushed the feature/price-feed branch 2 times, most recently from 6f0ec87 to cccac84 Compare January 15, 2026 18:47
@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

@greptile-apps
Copy link

greptile-apps bot commented Jan 15, 2026

Greptile Overview

Greptile Summary

This PR adds BTC/USD price fetching functionality from CoinGecko API, displaying the current Bitcoin price in the positions view.

Key changes:

  • New price_fetcher module with trait-based design for fetching cryptocurrency prices
  • CoingeckoPriceFetcher implementation with proper timeout (5s) and error handling
  • Integration into positions display using tokio::task::spawn_blocking to avoid blocking the async runtime
  • Comprehensive error handling for rate limits, network errors, and parse failures

Addressed from previous feedback:

  • Uses spawn_blocking for synchronous HTTP calls ✓
  • Improved error handling with specific error types ✓
  • Fixed currency symbol placement ($ prefix) ✓
  • Derives Default for zero-field struct ✓

The implementation is clean, follows Rust best practices, and properly handles the blocking I/O in an async context.

Confidence Score: 4.5/5

  • This PR is safe to merge with minimal risk
  • The implementation is clean and addresses all major concerns from previous reviews. The code properly handles blocking I/O, has comprehensive error handling, and follows established patterns in the codebase. Score is not 5 due to lack of unit tests, but this is acceptable per developer feedback.
  • No files require special attention

Important Files Changed

Filename Overview
crates/cli-client/src/price_fetcher.rs New module implementing BTC/USD price fetching from CoinGecko with proper error handling and timeout
crates/cli-client/src/cli/positions.rs Integrates price fetcher into positions display with spawn_blocking and proper error handling

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant Positions
    participant PriceFetcher
    participant CoinGecko

    User->>CLI: cargo run positions
    CLI->>Positions: run_positions()
    Positions->>PriceFetcher: spawn_blocking(fetch_btc_usd_price)
    PriceFetcher->>CoinGecko: GET /api/v3/simple/price
    alt Success (200)
        CoinGecko-->>PriceFetcher: {"bitcoin":{"usd":price}}
        PriceFetcher-->>Positions: Ok(price)
        Positions->>User: Display "$price"
    else Rate Limited (429)
        CoinGecko-->>PriceFetcher: 429 status
        PriceFetcher-->>Positions: Err(RateLimit)
        Positions->>User: Display "Rate limit exceeded"
    else Other Error
        CoinGecko-->>PriceFetcher: Error/timeout
        PriceFetcher-->>Positions: Err(error)
        Positions->>User: Display "Price fetcher service unavailable"
    end
    Positions->>User: Display positions with price info
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@zahorodnyi zahorodnyi marked this pull request as draft January 16, 2026 12:00
@zahorodnyi zahorodnyi added the greptile Mark this PR to be reviewed by Greptile label Jan 16, 2026
@zahorodnyi zahorodnyi marked this pull request as ready for review January 16, 2026 15:45
@greptile-apps
Copy link

greptile-apps bot commented Jan 16, 2026

Greptile's behavior is changing!

From now on, if a review finishes with no comments, we will not post an additional "statistics" comment to confirm that our review found nothing to comment on. However, you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

@zahorodnyi zahorodnyi marked this pull request as draft January 28, 2026 13:33
@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@zahorodnyi zahorodnyi marked this pull request as ready for review January 29, 2026 11:57
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@KyrylR
Copy link
Collaborator

KyrylR commented Jan 29, 2026

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

nostr-sdk = { version = "0.44.1" }

minreq = { version = "2.14", features = ["https", "json-using-serde"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use minreq instead


#[derive(Deserialize)]
struct BitcoinPrice {
usd: Decimal,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need rust_decimal::Decimal?

use rust_decimal::Decimal;

pub async fn fetch_btc_usd_price<T: PriceFetcher>(fetcher: &T) -> Result<Decimal, Error> {
Ok(fetcher.fetch_price().await?)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Ok(fetcher.fetch_price().await?)
fetcher.fetch_price().await

Comment on lines 9 to 16
pub async fn run_price_feed() -> Result<(), Error> {
let fetcher = CoingeckoPriceFetcher::new()?;
let price = fetch_btc_usd_price(&fetcher).await?;

println!("1 BTC = ${}", price.round_dp(2));

Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the purpose of this function?

Copy link
Collaborator Author

@zahorodnyi zahorodnyi Feb 2, 2026

Choose a reason for hiding this comment

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

The purpose is to show the price in the terminal using the command cargo run -- price-feed (according to what was written in the original issue: #40). Don’t we need it?

Comment on lines 5 to 8
pub async fn fetch_btc_usd_price<T: PriceFetcher>(fetcher: &T) -> Result<Decimal, Error> {
Ok(fetcher.fetch_price().await?)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's move this to the: crates/cli-client/src/price_fetcher.rs

@zahorodnyi zahorodnyi force-pushed the feature/price-feed branch 2 times, most recently from 7ff0601 to a0b1d27 Compare February 2, 2026 16:55
@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 31 to 34
let fetcher = CoingeckoPriceFetcher::new();
let btc_price = fetch_btc_usd_price(&fetcher)
.map(|x| format!("{x:.2}$"))
.unwrap_or("'Rate limit exceed'".to_string());
Copy link

Choose a reason for hiding this comment

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

[P1] Swallows non-rate-limit errors as "rate limit"

fetch_btc_usd_price(&fetcher) can fail for reasons other than a 429 (e.g., no network, DNS issues, CoinGecko 5xx, JSON shape changes), but this unwrap_or("'Rate limit exceed'") prints the same message for all failures, which makes debugging and user guidance misleading. Consider matching on the error and only mapping PriceFetcherError::RateLimit to that message, while showing (or logging) other errors.

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

Comment:
[P1] Swallows non-rate-limit errors as "rate limit"

`fetch_btc_usd_price(&fetcher)` can fail for reasons other than a 429 (e.g., no network, DNS issues, CoinGecko 5xx, JSON shape changes), but this `unwrap_or("'Rate limit exceed'")` prints the same message for all failures, which makes debugging and user guidance misleading. Consider matching on the error and only mapping `PriceFetcherError::RateLimit` to that message, while showing (or logging) other errors.


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

let fetcher = CoingeckoPriceFetcher::new();
let btc_price = fetch_btc_usd_price(&fetcher)
.map(|x| format!("{x:.2}$"))
.unwrap_or("'Rate limit exceed'".to_string());
Copy link

Choose a reason for hiding this comment

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

[P3] User-facing string has typo/quoting

"'Rate limit exceed'" reads like a debug string (extra quotes) and has a grammar typo; it will show up in normal CLI output. Consider a cleaner message like "Rate limit exceeded".

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

Comment:
[P3] User-facing string has typo/quoting

`"'Rate limit exceed'"` reads like a debug string (extra quotes) and has a grammar typo; it will show up in normal CLI output. Consider a cleaner message like `"Rate limit exceeded"`.


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

Comment on lines 27 to 30
pub trait PriceFetcher {
fn fetch_price(&self) -> Result<f64, Error>;
}
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.

@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +26 to +29
pub trait PriceFetcher {
fn fetch_price(&self) -> Result<f64, PriceFetcherError>;
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 36 to 41
pub fn new() -> Self {
Self {}
}
Copy link

Choose a reason for hiding this comment

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

Missing Default trait implementation. new() creates empty struct - derive or implement Default instead.

Suggested change
pub fn new() -> Self {
Self {}
}
#[must_use]
pub const fn new() -> Self {
Self
}

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: 36:38

Comment:
Missing `Default` trait implementation. `new()` creates empty struct - derive or implement `Default` instead.

```suggestion
    #[must_use]
    pub const fn new() -> Self {
        Self
    }
```

<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.

Comment on lines 31 to 43
let fetcher = CoingeckoPriceFetcher::new();
let btc_price = match fetch_btc_usd_price(&fetcher) {
Ok(price) => format!("{price:.2}$"),
Err(PriceFetcherError::RateLimit) => "Rate limit exceeded".to_string(),
Err(e) => {
eprintln!("Debug error: {e}");
"Price fetcher service unavailable".to_string()
}
};
Copy link

Choose a reason for hiding this comment

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

Blocking HTTP call in async function. fetch_btc_usd_price() uses synchronous minreq and will block the tokio runtime thread. Wrap in tokio::task::spawn_blocking():

let btc_price = match tokio::task::spawn_blocking(move || fetch_btc_usd_price(&fetcher)).await {
    Ok(Ok(price)) => format!("{price:.2}$"),
    Ok(Err(PriceFetcherError::RateLimit)) => "Rate limit exceeded".to_string(),
    Ok(Err(e)) => {
        eprintln!("Debug error: {e}");
        "Price fetcher service unavailable".to_string()
    }
    Err(_) => "Price fetcher service unavailable".to_string(),
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/cli-client/src/cli/positions.rs
Line: 31:39

Comment:
Blocking HTTP call in async function. `fetch_btc_usd_price()` uses synchronous `minreq` and will block the tokio runtime thread. Wrap in `tokio::task::spawn_blocking()`:

```
let btc_price = match tokio::task::spawn_blocking(move || fetch_btc_usd_price(&fetcher)).await {
    Ok(Ok(price)) => format!("{price:.2}$"),
    Ok(Err(PriceFetcherError::RateLimit)) => "Rate limit exceeded".to_string(),
    Ok(Err(e)) => {
        eprintln!("Debug error: {e}");
        "Price fetcher service unavailable".to_string()
    }
    Err(_) => "Price fetcher service unavailable".to_string(),
};
```

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

@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 32 to 41
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;

#[must_use]
pub fn new() -> Self {
Self
}
Copy link

Choose a reason for hiding this comment

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

derive Default instead of implementing new() for zero-field struct

Suggested change
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;
#[must_use]
pub fn new() -> Self {
Self
}
#[derive(Default)]
pub struct CoingeckoPriceFetcher;

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: 32:41

Comment:
derive `Default` instead of implementing `new()` for zero-field struct

```suggestion
#[derive(Default)]
pub struct CoingeckoPriceFetcher;
```

<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.

.unwrap_or_else(|e| Err(PriceFetcherError::Internal(e.to_string())));

let btc_price = match btc_result {
Ok(price) => format!("{price:.2}$"),
Copy link

Choose a reason for hiding this comment

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

inconsistent currency symbol placement - should be ${price:.2} (USD convention)

Suggested change
Ok(price) => format!("{price:.2}$"),
Ok(price) => format!("${price:.2}"),

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/cli/positions.rs
Line: 37:37

Comment:
inconsistent currency symbol placement - should be `${price:.2}` (USD convention)

```suggestion
            Ok(price) => format!("${price:.2}"),
```

<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.

@zahorodnyi
Copy link
Collaborator Author

@greptile review the PR according to the CONTRIBUTION.md

@KyrylR
Copy link
Collaborator

KyrylR commented Feb 4, 2026

Rebase onto main pls, also bump to the last simplicityhl-core version and fix compilation errors

Comment on lines 48 to 57
let price = match resp.status_code {
200 => resp
.json::<BitcoinResponse>()
.map(|data| data.bitcoin.usd)
.map_err(|e| PriceFetcherError::Parse(e.to_string()))?,
429 => return Err(PriceFetcherError::RateLimit),
status => return Err(PriceFetcherError::Status(status)),
};

Ok(price)
Copy link
Collaborator

@KyrylR KyrylR Feb 4, 2026

Choose a reason for hiding this comment

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

Suggested change
let price = match resp.status_code {
200 => resp
.json::<BitcoinResponse>()
.map(|data| data.bitcoin.usd)
.map_err(|e| PriceFetcherError::Parse(e.to_string()))?,
429 => return Err(PriceFetcherError::RateLimit),
status => return Err(PriceFetcherError::Status(status)),
};
Ok(price)
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)),
}

chore:
* bump simplicityhl version from 0.4.0 -> 0.4.1
* remove Cargo.lock from .gitignore
Copy link
Collaborator

@KyrylR KyrylR left a comment

Choose a reason for hiding this comment

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

ACK 236885d

@KyrylR KyrylR merged commit d4a3e12 into main Feb 4, 2026
1 check passed
@KyrylR KyrylR deleted the feature/price-feed branch February 4, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

greptile Mark this PR to be reviewed by Greptile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add price feed

2 participants