dweb/
token.rs

1/*
2Copyright (c) 2024-2025 Mark Hughes
3
4This program is free software: you can redistribute it and/or modify
5it under the terms of the GNU Affero General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9This program is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12GNU Affero General Public License for more details.
13
14You should have received a copy of the GNU Affero General Public License
15along with this program. If not, see <https://www.gnu.org/licenses/>.
16*/
17
18use color_eyre::{eyre::eyre, Result};
19
20use autonomi::AttoTokens;
21use evmlib::common::{Amount, U256};
22
23use crate::client::DwebClient;
24
25/// Control 'show cost' operations
26#[derive(clap::ValueEnum, Clone, Debug)]
27pub enum ShowCost {
28    Token,
29    Gas,
30    Both,
31    None,
32}
33
34#[derive(Clone)]
35pub struct Spends {
36    client: DwebClient,
37    pub token: Amount,
38    pub gas: Amount,
39
40    show_cost: ShowCost,
41    label: String,
42}
43
44/// Capture gas and token balances for monitoring and reporting spends
45impl Spends {
46    pub async fn new(client: &DwebClient, label: Option<&str>) -> Result<Spends> {
47        let label = label.unwrap_or("Cost total: ").to_string();
48        let client = client.clone();
49        let show_cost = client.api_control.show_dweb_costs.clone();
50        let token = client.wallet.balance_of_tokens().await?;
51        let gas = client.wallet.balance_of_gas_tokens().await?;
52        Ok(Spends {
53            token: token,
54            gas: gas,
55            client,
56            show_cost,
57            label,
58        })
59    }
60
61    pub async fn update(&mut self) -> Result<()> {
62        self.token = self.client.wallet.balance_of_tokens().await?;
63        self.gas = self.client.wallet.balance_of_gas_tokens().await?;
64        Ok(())
65    }
66
67    /// Print the spend since last 'update' with optional label (which defaults to "Cost total: ")
68    pub async fn show_spend(&self) -> Result<()> {
69        let label = &self.label;
70        let spent_gas = AttoTokens::from(self.spent_gas().await?);
71        let spent_gas_string = format_tokens(spent_gas.as_atto());
72
73        let spent_tokens = AttoTokens::from(self.spent_tokens().await?);
74        let spent_tokens_string = format_tokens(spent_tokens.as_atto());
75
76        let spent_gas = if let Some(eth_rate) = &self.client.eth_rate {
77            format!(
78                "{label}{} ({spent_gas_string} Gas)",
79                eth_rate.to_currency(&spent_gas)
80            )
81        } else {
82            format!("{label}{spent_gas_string} Gas")
83        };
84        let spent_tokens = if let Some(ant_rate) = &self.client.ant_rate {
85            format!(
86                "{label}{} ({spent_tokens_string} ANT)",
87                ant_rate.to_currency(&spent_tokens)
88            )
89        } else {
90            format!("{label}{spent_tokens_string} ANT")
91        };
92
93        match self.show_cost {
94            ShowCost::Gas => {
95                println!("{spent_gas}");
96            }
97            ShowCost::Token => {
98                println!("{spent_tokens}");
99            }
100            ShowCost::Both => {
101                println!("{spent_gas}");
102                println!("{spent_tokens}");
103            }
104            _ => {}
105        }
106        Ok(())
107    }
108
109    pub async fn spent_tokens(&self) -> Result<Amount> {
110        let balance = self.client.wallet.balance_of_tokens().await?;
111        match self.token.checked_sub(balance) {
112            Some(spent) => Ok(spent),
113            None => Err(eyre!("Error calculating spent tokens")),
114        }
115    }
116
117    pub async fn spent_gas(&self) -> Result<Amount> {
118        let balance = self.client.wallet.balance_of_gas_tokens().await?;
119        match self.gas.checked_sub(balance) {
120            Some(spent) => Ok(spent),
121            None => {
122                println!("Error calculating spent gas at balance.checked_sub(self.gas)");
123                Err(eyre!("Error calculating spent gas"))
124            }
125        }
126    }
127}
128
129const UNITS_PER_TOKEN_U64: u64 = 1_000_000_000_000_000_000;
130const UNITS_PER_TOKEN_F32: f32 = 1_000_000_000_000_000_000.0;
131
132/// Return a string representation with 18 decimal places
133pub fn format_tokens(amount: Amount) -> String {
134    let unit = amount / Amount::from(UNITS_PER_TOKEN_U64);
135    let remainder = amount % Amount::from(UNITS_PER_TOKEN_U64);
136    format!("{unit}.{remainder:018}").to_string()
137}
138
139/// Helper to simplify handling of Result<_>.
140///
141/// Return show_spend_return_value<T>() with T as the type of the return value you need
142pub async fn show_spend_return_value<T>(spends: &Spends, value: T) -> T {
143    let _ = spends.show_spend().await;
144    value
145}
146
147const RATE_VAR_PREFIX: &str = "DWEB_RATE_";
148
149#[derive(Clone)]
150pub struct Rate {
151    pub ticker: String,   // ANT or ETH
152    pub currency: String, // GBP, USD etc
153    pub rate: f32,
154    // pub date:   Option<time>,
155}
156
157impl Rate {
158    pub fn from_environment(ticker: String) -> Option<Rate> {
159        let env_var = Self::env_var_for(&ticker);
160        let env_value = match std::env::var(&env_var) {
161            Ok(value) => value,
162            Err(_) => return None,
163        };
164
165        let mut iter = env_value.split(',');
166        let rate = match iter.next().unwrap_or("0.0").parse::<f32>() {
167            Ok(rate) => rate,
168            Err(_) => return None,
169        };
170
171        let currency = iter.next().unwrap_or("ERROR").to_string();
172
173        // TODO parse any date string into a date-time type so users can calculate the age of a rate
174        let _date = iter.next().unwrap_or("ERROR");
175
176        Some(Rate {
177            ticker: ticker.clone(),
178            currency,
179            rate,
180        })
181    }
182
183    pub fn env_var_for(ticker: &String) -> String {
184        return format!("{RATE_VAR_PREFIX}{ticker}");
185    }
186
187    pub fn to_currency(&self, tokens: &AttoTokens) -> String {
188        const MIN_FACTOR: f32 = 100_f32; // One power of ten per decimal place
189        let factor = match self.rate {
190            rate if rate < 0.001 => MIN_FACTOR * 1000_f32,
191            rate if rate < 0.01 => MIN_FACTOR * 100_f32,
192            rate if rate < 0.1 => MIN_FACTOR * 10_f32,
193            rate if rate < 1. => MIN_FACTOR,
194            rate if rate >= 1. => MIN_FACTOR,
195            _ => 1.0, // NaN
196        };
197
198        // Scale up the rate by factor -> f32 an
199        let scaled_rate = U256::from(self.rate * factor);
200        let scaled_value = scaled_rate * tokens.as_atto();
201        let scaled_value = scaled_value.to::<u64>();
202        let value = match format!("{scaled_value}").parse::<f32>() {
203            Ok(scaled_value) => (scaled_value / factor) / UNITS_PER_TOKEN_F32,
204            Err(_) => {
205                return "[Invalid value]".to_string();
206            }
207        };
208
209        format!("{}{value:.8}", self.currency).to_string()
210    }
211}