Skip to content

Commit

Permalink
feat: start working on estimate_transaction_fees
Browse files Browse the repository at this point in the history
  • Loading branch information
MishkaRogachev committed Oct 4, 2024
1 parent 956de11 commit 82cff03
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 27 deletions.
5 changes: 5 additions & 0 deletions src/core/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ impl Balance {
Self { currency: currency.to_string(), chain_values }
}

pub fn to_string(&self) -> String {
let summary = self.summary();
format!("{:.6} {} ({:.2} USD)", summary.value, self.currency, summary.usd_value)
}

pub fn summary(&self) -> BalanceValue {
self.chain_values.values().fold(BalanceValue::new(0.0, 0.0), |acc, v| {
BalanceValue::new(acc.value + v.value, acc.usd_value + v.usd_value)
Expand Down
25 changes: 23 additions & 2 deletions src/core/provider_eth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use web3::{
contract::{Contract, Options},
signing::SecretKey,
types::{Address, Transaction, TransactionId, TransactionParameters, TransactionReceipt, H256, U256},
types::{Address, CallRequest, Transaction, TransactionId, TransactionParameters, TransactionReceipt, H256, U256},
};

use super::{balance::{Balance, Balances}, eth_utils, provider::Provider, token::{Token, TokenList}};
Expand Down Expand Up @@ -40,7 +40,7 @@ impl<T: web3::Transport> Provider<T> {
Ok(Token::new(&name, &symbol).with_chain_data(self.chain, contract_address, decimals.as_u64() as u16))
}

async fn get_eth_usd_rate(&self) -> anyhow::Result<f64> {
pub async fn get_eth_usd_rate(&self) -> anyhow::Result<f64> {
let contrcat_address = self.chain.get_chainlink_contract_address();
let contract = Contract::from_json(self.web3.eth(), contrcat_address, CHAINLINK_ABI)?;

Expand Down Expand Up @@ -99,6 +99,27 @@ impl<T: web3::Transport> Provider<T> {
Ok(balances)
}

pub async fn estimate_transaction_fees(&self, transaction: TransactionParameters, from: Address) -> anyhow::Result<Balance> {
let gas_limit: U256 = self.web3.eth()
.estimate_gas(
CallRequest {
from: Some(from),
to: transaction.to,
gas: None,
gas_price: None,
value: Some(transaction.value),
data: Some(transaction.data),
..Default::default()
},
None
)
.await?;
let gas_price: U256 = self.web3.eth().gas_price().await?;
let wei = gas_limit * gas_price;
let usd_value = eth_utils::wei_to_eth(wei) * self.get_eth_usd_rate().await?;
Ok(Balance::new(ETH, self.chain, eth_utils::wei_to_eth(wei), usd_value))
}

pub async fn send_transaction(&self, transaction: TransactionParameters, secret_key: &SecretKey) -> anyhow::Result<H256> {
let signed = self.web3.accounts()
.sign_transaction(transaction, secret_key)
Expand Down
4 changes: 2 additions & 2 deletions src/core/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use web3::{signing::SecretKey, types::{Address, H256, U64}};
use web3::types::{Address, H256, U64};

use super::eth_chain::EthChain;

pub struct TransactionRequest {
pub from: Address,
pub to: Address,
pub value: f64,
pub amount: f64,
pub currency: String,
pub chain: EthChain,
}
Expand Down
23 changes: 20 additions & 3 deletions src/service/crypto_transactions.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@

use web3::{signing::SecretKey, types::{TransactionParameters, U64}};

use crate::core::{eth_utils, transaction::{TransactionRequest, TransactionResult}};
use crate::core::{balance::Balance, eth_utils, transaction::{TransactionRequest, TransactionResult}};
use super::crypto::Crypto;

const ERR_NO_TRANSACTION_FOUND: &str = "No transaction found";

impl Crypto {
pub async fn estimate_transaction_fees(&self, request: TransactionRequest) -> anyhow::Result<Balance> {
if request.currency != "ETH" { // TODO: token transactions
return Err(anyhow::anyhow!("Non-ETH transactions are not supported yet"));
}

let provider = self.providers.get(&request.chain).ok_or_else(||
anyhow::anyhow!(format!("No provider for chain {}", request.chain)))?;

let transaction = TransactionParameters {
to: Some(request.to),
value: eth_utils::eth_to_wei(request.amount),
..Default::default()
};

provider.estimate_transaction_fees(transaction, request.from).await
}

pub async fn send_transaction(&self, request: TransactionRequest, secret_key: &SecretKey) -> anyhow::Result<TransactionResult> {
if request.currency != "ETH" { // TODO: token transactions
return Err(anyhow::anyhow!("Non-ETH transactions are not supported yet"));
Expand All @@ -17,7 +34,7 @@ impl Crypto {

let transaction = TransactionParameters {
to: Some(request.to),
value: eth_utils::eth_to_wei(request.value),
value: eth_utils::eth_to_wei(request.amount),
..Default::default()
};

Expand All @@ -32,7 +49,7 @@ impl Crypto {
block_number: receipt.block_number,
from: Some(receipt.from),
to: receipt.to,
amount: request.value,
amount: request.amount,
fee: eth_utils::wei_to_eth(receipt.effective_gas_price.unwrap_or_default() * receipt.gas_used.unwrap_or_default()),
chain: request.chain,
successed,
Expand Down
68 changes: 53 additions & 15 deletions src/tui/popups/send.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use web3::types::Address;
use ratatui::{
crossterm::event::Event,
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
style::{Color, Style},
widgets::{Block, Borders, Clear, Paragraph},
Frame
};
use web3::types::Address;

use crate::{core::{eth_chain::EthChain, eth_utils}, service::crypto::Crypto};
use crate::core::{balance::Balance, eth_chain::EthChain, eth_utils, transaction::TransactionRequest};
use crate::service::crypto::Crypto;
use crate::tui::{widgets::controls, app::AppScreen};

const TITLE: &str = "Send Crypto";
Expand All @@ -22,7 +23,7 @@ pub struct Popup {
eth_usd_rate: Option<f64>,
amount_value: f64,
alt_amount_value: Option<f64>,
fees: Option<f64>,
fees: Option<Balance>,

chain_button: controls::MenuButton<EthChain>,
to: controls::Input,
Expand Down Expand Up @@ -77,20 +78,45 @@ impl Popup {
}
}

fn assembly_transaction_request(&self) -> Option<TransactionRequest> {
let chain = self.chain?;
let to = eth_utils::str_to_eth_address(&self.to.value).ok()?;
if self.amount_value <= 0.0 {
return None;
}

Some(TransactionRequest {
currency: "ETH".to_string(),
chain,
from: self.from,
to,
amount: self.amount_value,
})
}

fn invalidate(&mut self) {
self.amount_value = 0.0;
self.alt_amount_value = None;
self.fees = None;
}
}

#[async_trait::async_trait]
impl AppScreen for Popup {
async fn handle_event(&mut self, event: Event) -> anyhow::Result<bool> {

if let Some(_) = controls::handle_scoped_event(&mut [&mut self.to, &mut self.amount], &event) {
self.invalidate();
return Ok(false);
}
if let Some(chain) = self.chain_button.handle_event(&event) {
self.chain = Some(chain);
self.invalidate();
return Ok(false);
}
if let Some(_) = self.swap_button.handle_event(&event) {
if let Some(alt_amount_value) = self.alt_amount_value {
self.amount_value = alt_amount_value;
}
self.alt_amount_value = None;
return Ok(false);
}
Expand All @@ -105,31 +131,43 @@ impl AppScreen for Popup {
}

async fn update(&mut self) {
let mut is_valid = true;
let mut is_ready = true;

// Validate chain
if let Some(chain) = self.chain {
self.chain_button.button.label = chain.get_display_name().to_string();
self.chain_button.button.color = Color::Yellow;
is_valid &= true;
is_ready &= true;
} else {
self.chain_button.button.label = "Select chain".to_string();
self.chain_button.button.color = Color::Red;
is_valid &= false;
is_ready &= false;
}

// Validate receiver address
let to = eth_utils::str_to_eth_address(&self.to.value);
self.to.color = if to.is_ok() || self.to.value.is_empty() { Color::Yellow } else { Color::Red };
is_valid &= to.is_ok();
let address_valid = to.is_ok();
self.to.color = if address_valid || self.to.value.is_empty() { Color::Yellow } else { Color::Red };
is_ready &= address_valid;

// Validate amount
self.amount_value = self.amount.value.parse::<f64>().unwrap_or(0.0);
let amount_valid = self.amount_value > 0.0;
self.amount.color = if amount_valid || self.amount.value.is_empty() { Color::Yellow } else { Color::Red };
is_valid &= amount_valid;
is_ready &= amount_valid;

// Calc fees
if self.fees.is_none() && amount_valid && address_valid {
if let Some(transaction_request) = self.assembly_transaction_request() {
let crypto = self.crypto.lock().await.clone();
self.fees = crypto.estimate_transaction_fees(transaction_request).await.ok();
} else {
self.fees = None;
}
}
is_ready &= self.fees.is_some();

// Cacl alt amount
// Calc alt amount
if amount_valid && self.alt_amount_value.is_none() {
if let Some(eth_usd_rate) = self.eth_usd_rate {
self.alt_amount_value = Some(if self.swap_button.state {
Expand All @@ -144,7 +182,7 @@ impl AppScreen for Popup {
self.alt_amount_value = None;
}

self.send_button.disabled = !is_valid;
self.send_button.disabled = !is_ready;
}

fn render(&mut self, frame: &mut Frame, area: Rect) {
Expand Down Expand Up @@ -282,12 +320,12 @@ impl AppScreen for Popup {
.alignment(Alignment::Left);
frame.render_widget(fees_label, fees_layout[1].inner(label_margin));

let fees_value = if let Some(fees) = self.fees {
Paragraph::new(format!("{:.6} ETH", fees))
let fees_value = if let Some(fees) = &self.fees {
Paragraph::new(fees.to_string())
.style(Style::default().fg(Color::Yellow))
.alignment(Alignment::Left)
} else {
Paragraph::new("-")
Paragraph::new("---")
.style(Style::default().fg(Color::Yellow))
.alignment(Alignment::Left)
};
Expand Down
9 changes: 4 additions & 5 deletions src/tui/widgets/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,13 @@ impl Account {
])
.split(tokens_layout[i]);

let token = &self.balances.as_ref().unwrap()[i + index_offset];
let token_label = Paragraph::new(format!("{}", token.currency))
let balance = &self.balances.as_ref().unwrap()[i + index_offset];
let token_label = Paragraph::new(format!("{}", balance.currency))
.style(Style::default().fg(Color::Yellow))
.alignment(Alignment::Left);

let token_value = token.summary();
let token_value_color = if token.from_test_network() { Color::Red } else { Color::Yellow };
let token_value_label = Paragraph::new(format!("{:.6} ({:.2} USD)", token_value.value, token_value.usd_value))
let token_value_color = if balance.from_test_network() { Color::Red } else { Color::Yellow };
let token_value_label = Paragraph::new(balance.to_string())
.style(Style::default().fg(token_value_color))
.alignment(Alignment::Right);

Expand Down

0 comments on commit 82cff03

Please sign in to comment.