Skip to content

Commit

Permalink
feat(asset screen): Add placeholders and pretty-print addresses (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole committed Aug 21, 2024
1 parent 751d69d commit 4a8c9b4
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 67 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ bs58.workspace = true
chrono = { workspace = true, features = ["serde"] }
copypasta.workspace = true
futures.workspace = true
itertools.workspace = true
ledger_bitcoin_client.workspace = true
ledger-lib.workspace = true
ledger-proto.workspace = true
Expand All @@ -25,5 +26,5 @@ rust_decimal_macros.workspace = true
serde.workspace = true
serde_json.workspace = true
strum = { workspace = true, features = ["derive"] }
tokio.workspace = true
tokio = { workspace = true, features = ["time"] }
tui-widget-list.workspace = true
2 changes: 1 addition & 1 deletion app/src/api/blockchain_monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use api_proc_macro::implement_cache;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;

use super::common::{Account, Network};
use super::common_types::{Account, Network};

// TODO: This API will be fallible (return `Result<...>`) in future.
implement_cache! {
Expand Down
21 changes: 21 additions & 0 deletions app/src/api/cache_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ use std::{
pin::Pin,
time::{Duration, Instant},
};
use tokio::time::sleep;

#[derive(Default, Clone, Copy)]
pub enum ModePlan {
#[default]
Transparent,
TimedOut(Duration),
Slow(Duration),
}

impl ModePlan {
pub fn into_mode<In: Hash + PartialEq + Eq>(self) -> Mode<In> {
match self {
Self::Transparent => Mode::new_transparent(),
Self::TimedOut(timeout) => Mode::new_timed_out(timeout),
Self::Slow(delay) => Mode::new_slow(delay),
}
}
}
Expand All @@ -30,6 +33,8 @@ pub enum Mode<In: Hash + PartialEq + Eq> {
/// This type of cache will call API only if some specified time have passed after previous call.
/// It will return value from cache elsewhere.
TimedOut(TimedOutMode<In>),
/// This type of cache will delay calls to API to simulate network or i/o delays.
Slow(Duration),
}

#[derive(Clone)]
Expand All @@ -49,6 +54,10 @@ impl<In: Hash + PartialEq + Eq> Mode<In> {
previous_request: Default::default(),
})
}

pub fn new_slow(delay: Duration) -> Self {
Self::Slow(delay)
}
}

pub(super) async fn use_cache<In, Out>(
Expand All @@ -64,6 +73,7 @@ where
match mode {
Mode::Transparent => transparent_mode(request, cache, api_result).await,
Mode::TimedOut(state) => timed_out_mode(request, cache, api_result, state).await,
Mode::Slow(delay) => slow_mode(request, cache, api_result, *delay).await,
}
}

Expand Down Expand Up @@ -101,3 +111,14 @@ where

result
}

async fn slow_mode<In, Out>(
_request: In,
_cache: Entry<'_, In, Out>,
api_result: Pin<Box<impl Future<Output = Out>>>,
delay: Duration,
) -> Out {
sleep(delay).await;

api_result.await
}
File renamed without changes.
2 changes: 1 addition & 1 deletion app/src/api/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ledger_lib::{
DEFAULT_TIMEOUT,
};

use super::common::{Account, Network};
use super::common_types::{Account, Network};

implement_cache!(
pub trait LedgerApiT {
Expand Down
2 changes: 1 addition & 1 deletion app/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub mod blockchain_monitoring;
pub mod cache_utils;
pub mod coin_price;
pub mod common;
pub mod common_types;
pub mod ledger;
9 changes: 6 additions & 3 deletions app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
coin_price::{
cache::Cache as CoinPriceApiCache, mock::CoinPriceApiMock, CoinPriceApi, CoinPriceApiT,
},
common::{Account, Network},
common_types::{Account, Network},
ledger::{
cache::Cache as LedgerApiCache, mock::LedgerApiMock, Device, DeviceInfo, LedgerApiT,
},
Expand Down Expand Up @@ -64,7 +64,7 @@ impl StateRegistry {
impl App {
pub async fn new() -> Self {
Self {
screens: vec![ScreenName::DeviceSelection],
screens: vec![ScreenName::Portfolio, ScreenName::DeviceSelection],
}
}

Expand All @@ -84,12 +84,15 @@ impl App {
let mut state = Some(StateRegistry::new());

let api_registry = {
let ledger_api = LedgerApiMock::new(10, 3);
let ledger_api = LedgerApiMock::new(2, 3);
let mut ledger_api = block_on(LedgerApiCache::new(ledger_api));
ledger_api.set_all_modes(ModePlan::Transparent);

let _coin_price_api = CoinPriceApiMock::new();
let coin_price_api = CoinPriceApi::new("https://data-api.binance.vision");
let mut coin_price_api = block_on(CoinPriceApiCache::new(coin_price_api));
coin_price_api.set_all_modes(ModePlan::Slow(Duration::from_secs(1)));

let mut coin_price_api = block_on(CoinPriceApiCache::new(coin_price_api));
coin_price_api.set_all_modes(ModePlan::TimedOut(Duration::from_secs(5)));

Expand Down
2 changes: 1 addition & 1 deletion app/src/screen/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
api::{
blockchain_monitoring::{BlockchainMonitoringApiT, TransactionInfo, TransactionUid},
coin_price::{Coin, CoinPriceApiT, TimePeriod as ApiTimePeriod},
common::Network,
common_types::Network,
ledger::LedgerApiT,
},
app::{ApiRegistry, StateRegistry},
Expand Down
157 changes: 104 additions & 53 deletions app/src/screen/asset/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@ use ratatui::{
},
Frame,
};
use rust_decimal::Decimal;
use strum::IntoEnumIterator;

use crate::{
api::{
blockchain_monitoring::{BlockchainMonitoringApiT, TransactionType},
blockchain_monitoring::{
BlockchainMonitoringApiT, TransactionInfo, TransactionType, TransactionUid,
},
coin_price::CoinPriceApiT,
common_types::{Account, Network},
ledger::LedgerApiT,
},
screen::common::network_symbol,
screen::common::{format_address, network_symbol, render_centered_text},
};

use super::{Model, TimePeriod};

const ADDRESSES_MAX_LEN: usize = 12;
const TX_UID_MAX_LEN: usize = 16;

pub(super) fn render<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
frame: &mut Frame<'_>,
Expand All @@ -35,26 +42,56 @@ pub(super) fn render<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApi
let price_chart_block = Block::new().title("Price").borders(Borders::all());
let inner_price_chart_area = price_chart_block.inner(price_chart_area);
frame.render_widget(price_chart_block, price_chart_area);
render_price_chart(model, frame, inner_price_chart_area);

if let Some(prices) = model.coin_price_history.as_ref() {
render_price_chart(
&prices[..],
model.selected_time_period,
frame,
inner_price_chart_area,
);
} else {
render_price_chart_placeholder(model.selected_time_period, frame, inner_price_chart_area);
}

let txs_list_block = Block::new()
.title("Transactions")
.borders(Borders::all())
.padding(Padding::proportional(1));
let inner_txs_list_area = txs_list_block.inner(txs_list_area);
frame.render_widget(txs_list_block, txs_list_area);
render_tx_list(model, frame, inner_txs_list_area);

match model.transactions.as_ref() {
Some(tx_list) if tx_list.is_empty() => {
render_empty_tx_list(frame, inner_txs_list_area);
}
Some(tx_list) => {
let selected_account = model
.state
.selected_account
.as_ref()
.expect("Selected accounmodelt should be present in state"); // TODO: Enforce this rule at `app` level?

render_tx_list(
selected_account.clone(),
&tx_list[..],
frame,
inner_txs_list_area,
);
}
None => {
render_tx_list_placeholder(frame, inner_txs_list_area);
}
}
}

fn render_price_chart<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
fn render_price_chart(
prices: &[Decimal],
selected_time_period: TimePeriod,
frame: &mut Frame<'_>,
area: Rect,
) {
let Some(prices) = model.coin_price_history.as_ref() else {
// TODO: Draw some placeholder.
return;
};
let legend = render_chart_legend(selected_time_period);

let max_price = *prices.iter().max().expect("Empty `prices` vector provided");

Expand All @@ -64,24 +101,6 @@ fn render_price_chart<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringAp
.map(|(idx, &price)| (idx as f64, price.try_into().unwrap()))
.collect();

let legend = TimePeriod::iter().map(|period| {
let label = match period {
TimePeriod::Day => " d[ay]",
TimePeriod::Week => " w[eek]",
TimePeriod::Month => " m[onth]",
TimePeriod::Year => " y[ear]",
TimePeriod::All => " a[ll] ",
};

if period == model.selected_time_period {
Span::raw(label).red()
} else {
Span::raw(label).green()
}
});

let legend = Line::from_iter(legend);

let datasets = vec![Dataset::default()
.marker(symbols::Marker::Bar)
.name(legend)
Expand All @@ -107,46 +126,69 @@ fn render_price_chart<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringAp
frame.render_widget(chart, area);
}

fn render_tx_list<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
fn render_price_chart_placeholder(
selected_time_period: TimePeriod,
frame: &mut Frame<'_>,
area: Rect,
) {
let Some(tx_list) = model.transactions.as_ref() else {
// TODO: Draw placeholder(fetching txs...)
return;
};

if tx_list.is_empty() {
// TODO: Draw placeholder(no txs yet...)
return;
}
let legend = render_chart_legend(selected_time_period);

let (selected_account_network, selected_account) = model
.state
.selected_account
.as_ref()
.expect("Selected account should be present in state"); // TODO: Enforce this rule at `app` level?
let chart = Chart::new(vec![Dataset::default().name(legend)])
.legend_position(Some(ratatui::widgets::LegendPosition::BottomRight))
// Always show a legend(see `hidden_legend_constraints` docs).
.hidden_legend_constraints((Constraint::Min(0), Constraint::Min(0)));

frame.render_widget(chart, area);

let text = Text::raw("Price is loading...");
render_centered_text(frame, area, text);
}

fn render_chart_legend(selected_time_period: TimePeriod) -> Line<'static> {
let legend = TimePeriod::iter().map(|period| {
let label = match period {
TimePeriod::Day => " d[ay]",
TimePeriod::Week => " w[eek]",
TimePeriod::Month => " m[onth]",
TimePeriod::Year => " y[ear]",
TimePeriod::All => " a[ll] ",
};

if period == selected_time_period {
Span::raw(label).red()
} else {
Span::raw(label).green()
}
});

Line::from_iter(legend)
}

fn render_tx_list(
selected_account: (Network, Account),
tx_list: &[(TransactionUid, TransactionInfo)],
frame: &mut Frame<'_>,
area: Rect,
) {
let (selected_account_network, selected_account) = selected_account;

let selected_account_address = selected_account.get_info().pk;

let network_icon = network_symbol(*selected_account_network);
let network_icon = network_symbol(selected_account_network);

let rows = tx_list
.iter()
.map(|(uid, tx)| {
// TODO: Pretty-format.
let uid = &uid.uid;
let uid = format_address(&uid.uid, TX_UID_MAX_LEN);
let uid = Text::raw(uid).alignment(Alignment::Center);

let time = format!("{}", tx.timestamp.format("%Y-%m-%d %H:%M UTC%:z"));
let time = Text::raw(time).alignment(Alignment::Center);

let description = match &tx.ty {
TransactionType::Deposit { from, amount } => {
// TODO: Pretty-format.
let from = [&from.get_info().pk[..8], "..."].concat();
let to = [&selected_account_address[..8], "..."].concat();
let from = format_address(&from.get_info().pk, ADDRESSES_MAX_LEN);
let to = format_address(&selected_account_address, ADDRESSES_MAX_LEN);

vec![
Span::raw(from),
Expand All @@ -156,9 +198,8 @@ fn render_tx_list<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
]
}
TransactionType::Withdraw { to, amount } => {
// TODO: Pretty-format.
let from = [&selected_account_address[..8], "..."].concat();
let to = [&to.get_info().pk[..8], "..."].concat();
let from = format_address(&selected_account_address, ADDRESSES_MAX_LEN);
let to = format_address(&to.get_info().pk, ADDRESSES_MAX_LEN);

vec![
Span::raw(from).green(),
Expand All @@ -182,3 +223,13 @@ fn render_tx_list<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(

frame.render_widget(table, area)
}

fn render_empty_tx_list(frame: &mut Frame<'_>, area: Rect) {
let text = Text::raw("No transactions here yet");
render_centered_text(frame, area, text)
}

fn render_tx_list_placeholder(frame: &mut Frame<'_>, area: Rect) {
let text = Text::raw("Fetching transactions...");
render_centered_text(frame, area, text)
}
Loading

0 comments on commit 4a8c9b4

Please sign in to comment.