Merge pull request #29 from Electric-Coin-Company/exchange-rate-usd

balance: Add currency conversion over Tor
This commit is contained in:
Kris Nuttycombe 2024-07-29 11:59:19 -06:00 committed by GitHub
commit 3a60108fe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 2769 additions and 81 deletions

2755
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ tonic = { version = "0.12", features = ["gzip", "tls-webpki-roots"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
zcash_address = "0.3.2" zcash_address = "0.3.2"
zcash_client_backend = { version = "0.12.1", features = ["lightwalletd-tonic", "orchard"] } zcash_client_backend = { version = "0.12.1", features = ["lightwalletd-tonic", "orchard", "tor"] }
zcash_client_sqlite = { version = "0.10.2", features = ["unstable", "orchard"] } zcash_client_sqlite = { version = "0.10.2", features = ["unstable", "orchard"] }
zcash_keys = { version = "0.2", features = ["unstable", "orchard"] } zcash_keys = { version = "0.2", features = ["unstable", "orchard"] }
zcash_primitives = "0.15" zcash_primitives = "0.15"
@ -34,6 +34,10 @@ zcash_proofs = "0.15"
zcash_protocol = "0.1" zcash_protocol = "0.1"
zip321 = "0.0.0" zip321 = "0.0.0"
# Currency conversion
iso_currency = { version = "0.4", features = ["with-serde"] }
rust_decimal = "1"
# TUI # TUI
crossterm = { version = "0.27", optional = true, features = ["event-stream"] } crossterm = { version = "0.27", optional = true, features = ["event-stream"] }
ratatui = { version = "0.26", optional = true } ratatui = { version = "0.26", optional = true }

View File

@ -1,11 +1,17 @@
use std::path::Path;
use anyhow::anyhow; use anyhow::anyhow;
use gumdrop::Options; use gumdrop::Options;
use zcash_client_backend::data_api::WalletRead; use iso_currency::Currency;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use tracing::{info, warn};
use zcash_client_backend::{data_api::WalletRead, tor};
use zcash_client_sqlite::WalletDb; use zcash_client_sqlite::WalletDb;
use zcash_protocol::value::{Zatoshis, COIN};
use crate::{ use crate::{
data::{get_db_paths, get_wallet_network}, data::{get_db_paths, get_tor_dir, get_wallet_network},
error, error,
ui::format_zec, ui::format_zec,
MIN_CONFIRMATIONS, MIN_CONFIRMATIONS,
@ -13,13 +19,16 @@ use crate::{
// Options accepted for the `balance` command // Options accepted for the `balance` command
#[derive(Debug, Options)] #[derive(Debug, Options)]
pub(crate) struct Command {} pub(crate) struct Command {
#[options(help = "Convert ZEC values into the given currency")]
convert: Option<Currency>,
}
impl Command { impl Command {
pub(crate) fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> { pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let params = get_wallet_network(wallet_dir.as_ref())?; let params = get_wallet_network(wallet_dir.as_ref())?;
let (_, db_data) = get_db_paths(wallet_dir); let (_, db_data) = get_db_paths(wallet_dir.as_ref());
let db_data = WalletDb::for_path(db_data, params)?; let db_data = WalletDb::for_path(db_data, params)?;
let account_id = *db_data let account_id = *db_data
.get_account_ids()? .get_account_ids()?
@ -30,6 +39,12 @@ impl Command {
.get_current_address(account_id)? .get_current_address(account_id)?
.ok_or(error::Error::InvalidRecipient)?; .ok_or(error::Error::InvalidRecipient)?;
let printer = if let Some(currency) = self.convert {
ValuePrinter::with_exchange_rate(&get_tor_dir(wallet_dir), currency).await?
} else {
ValuePrinter::ZecOnly
};
if let Some(wallet_summary) = db_data.get_wallet_summary(MIN_CONFIRMATIONS.into())? { if let Some(wallet_summary) = db_data.get_wallet_summary(MIN_CONFIRMATIONS.into())? {
let balance = wallet_summary let balance = wallet_summary
.account_balances() .account_balances()
@ -45,17 +60,20 @@ impl Command {
(*progress.numerator() as f64) * 100f64 / (*progress.denominator() as f64) (*progress.numerator() as f64) * 100f64 / (*progress.denominator() as f64)
); );
} }
println!(" Balance: {}", format_zec(balance.total())); println!(" Balance: {}", printer.format(balance.total()));
println!( println!(
" Sapling Spendable: {}", " Sapling Spendable: {}",
format_zec(balance.sapling_balance().spendable_value()) printer.format(balance.sapling_balance().spendable_value()),
); );
println!( println!(
" Orchard Spendable: {}", " Orchard Spendable: {}",
format_zec(balance.orchard_balance().spendable_value()) printer.format(balance.orchard_balance().spendable_value()),
); );
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
println!(" Unshielded: {}", format_zec(balance.unshielded())); println!(
" Unshielded: {}",
printer.format(balance.unshielded()),
);
} else { } else {
println!("Insufficient information to build a wallet summary."); println!("Insufficient information to build a wallet summary.");
} }
@ -63,3 +81,45 @@ impl Command {
Ok(()) Ok(())
} }
} }
enum ValuePrinter {
WithConversion { currency: Currency, rate: Decimal },
ZecOnly,
}
impl ValuePrinter {
async fn with_exchange_rate(tor_dir: &Path, currency: Currency) -> anyhow::Result<Self> {
// Ensure Tor directory exists.
tokio::fs::create_dir_all(tor_dir).await?;
let tor = tor::Client::create(tor_dir).await?;
info!("Fetching {:?}/ZEC exchange rate", currency);
let exchanges = tor::http::cryptex::Exchanges::unauthenticated_known_with_gemini_trusted();
let usd_zec = tor.get_latest_zec_to_usd_rate(&exchanges).await?;
if currency == Currency::USD {
let rate = usd_zec;
info!("Current {:?}/ZEC exchange rate: {}", currency, rate);
Ok(Self::WithConversion { currency, rate })
} else {
warn!("{:?}/ZEC exchange rate is unsupported", currency);
Ok(Self::ZecOnly)
}
}
fn format(&self, value: Zatoshis) -> String {
match self {
ValuePrinter::WithConversion { currency, rate } => {
format!(
"{} ({}{:.2})",
format_zec(value),
currency.symbol(),
rate * Decimal::from_u64(value.into_u64()).unwrap()
/ Decimal::from_u64(COIN).unwrap(),
)
}
ValuePrinter::ZecOnly => format_zec(value),
}
}
}

View File

@ -18,6 +18,7 @@ const DEFAULT_WALLET_DIR: &str = "./zec_sqlite_wallet";
const KEYS_FILE: &str = "keys.toml"; const KEYS_FILE: &str = "keys.toml";
const BLOCKS_FOLDER: &str = "blocks"; const BLOCKS_FOLDER: &str = "blocks";
const DATA_DB: &str = "data.sqlite"; const DATA_DB: &str = "data.sqlite";
const TOR_DIR: &str = "tor";
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub(crate) enum Network { pub(crate) enum Network {
@ -195,6 +196,14 @@ pub(crate) fn get_block_path(fsblockdb_root: &Path, meta: &BlockMeta) -> PathBuf
meta.block_file_path(&fsblockdb_root.join(BLOCKS_FOLDER)) meta.block_file_path(&fsblockdb_root.join(BLOCKS_FOLDER))
} }
pub(crate) fn get_tor_dir<P: AsRef<Path>>(wallet_dir: Option<P>) -> PathBuf {
wallet_dir
.as_ref()
.map(|p| p.as_ref())
.unwrap_or(DEFAULT_WALLET_DIR.as_ref())
.join(TOR_DIR)
}
pub(crate) async fn erase_wallet_state<P: AsRef<Path>>(wallet_dir: Option<P>) { pub(crate) async fn erase_wallet_state<P: AsRef<Path>>(wallet_dir: Option<P>) {
let (fsblockdb_root, db_data) = get_db_paths(wallet_dir); let (fsblockdb_root, db_data) = get_db_paths(wallet_dir);
let blocks_meta = fsblockdb_root.join("blockmeta.sqlite"); let blocks_meta = fsblockdb_root.join("blockmeta.sqlite");

View File

@ -129,7 +129,7 @@ fn main() -> Result<(), anyhow::Error> {
.await .await
} }
Some(Command::Enhance(command)) => command.run(opts.wallet_dir).await, Some(Command::Enhance(command)) => command.run(opts.wallet_dir).await,
Some(Command::Balance(command)) => command.run(opts.wallet_dir), Some(Command::Balance(command)) => command.run(opts.wallet_dir).await,
Some(Command::ListTx(command)) => command.run(opts.wallet_dir), Some(Command::ListTx(command)) => command.run(opts.wallet_dir),
Some(Command::ListUnspent(command)) => command.run(opts.wallet_dir), Some(Command::ListUnspent(command)) => command.run(opts.wallet_dir),
Some(Command::Propose(command)) => command.run(opts.wallet_dir).await, Some(Command::Propose(command)) => command.run(opts.wallet_dir).await,