From dbcddfa6a4fa4b919def27d24975403594a1256c Mon Sep 17 00:00:00 2001 From: creating2morrow Date: Fri, 12 May 2023 04:51:20 -0400 Subject: [PATCH] add fee estimator before jwp creation --- nevmes-core/.rustfmt.toml | 1 + nevmes-core/src/i2p.rs | 7 +-- nevmes-core/src/monero.rs | 29 +++++++------ nevmes-core/src/reqres.rs | 20 +++++---- nevmes-core/src/utils.rs | 66 +++++++++++++++++++++-------- nevmes-gui/src/apps/address_book.rs | 43 ++++++++++++++++++- 6 files changed, 123 insertions(+), 43 deletions(-) diff --git a/nevmes-core/.rustfmt.toml b/nevmes-core/.rustfmt.toml index dc407d5..9c537d8 100644 --- a/nevmes-core/.rustfmt.toml +++ b/nevmes-core/.rustfmt.toml @@ -3,3 +3,4 @@ format_code_in_doc_comments = true imports_granularity = "Crate" imports_layout = "Vertical" wrap_comments = true +ignore = ["lib.rs"] diff --git a/nevmes-core/src/i2p.rs b/nevmes-core/src/i2p.rs index 07b52d3..0fb4b26 100644 --- a/nevmes-core/src/i2p.rs +++ b/nevmes-core/src/i2p.rs @@ -91,8 +91,8 @@ async fn find_tunnels() { tokio::time::sleep(Duration::new(120, 0)).await; create_tunnel(); } - // TODO(c2m): why is i2p-zero http proxy always giving "destination not found" error? - // if !has_http_tunnel { create_http_proxy(); } + // TODO(c2m): why is i2p-zero http proxy always giving "destination not + // found" error? if !has_http_tunnel { create_http_proxy(); } } pub async fn start() { @@ -133,7 +133,8 @@ fn create_tunnel() { // TODO(c2m): use i2p-zero http proxy // fn create_http_proxy() { // info!("creating http proxy"); -// let output = Command::new("./i2p-zero-linux.v1.20/router/bin/tunnel-control.sh") +// let output = +// Command::new("./i2p-zero-linux.v1.20/router/bin/tunnel-control.sh") // .args(["http.create", "4444"]) // .spawn() // .expect("i2p-zero failed to create a http proxy"); diff --git a/nevmes-core/src/monero.rs b/nevmes-core/src/monero.rs index d1f9fad..6c910d0 100644 --- a/nevmes-core/src/monero.rs +++ b/nevmes-core/src/monero.rs @@ -935,7 +935,7 @@ pub async fn get_info() -> reqres::XmrDaemonGetInfoResponse { match client.post(host).json(&req).send().await { Ok(response) => { let res = response.json::().await; - debug!("{} response: {:?}", DaemonFields::GetInfo.value(), res); + // add debug log here if needed for adding more info to home screen in gui match res { Ok(res) => res, _ => Default::default(), @@ -949,16 +949,13 @@ pub async fn get_info() -> reqres::XmrDaemonGetInfoResponse { pub async fn get_height() -> reqres::XmrDaemonGetHeightResponse { info!("fetching daemon height"); let client = reqwest::Client::new(); - let host = get_rpc_daemon(); - let req = reqres::XmrRpcRequest { - jsonrpc: DaemonFields::Version.value(), - id: DaemonFields::Id.value(), - method: DaemonFields::GetHeight.value(), - }; - match client.post(host).json(&req).send().await { + let args = args::Args::parse(); + let daemon = String::from(args.monero_rpc_daemon); + let req = format!("{}/{}", daemon, DaemonFields::GetHeight.value()); + match client.post(req).send().await { Ok(response) => { let res = response.json::().await; - debug!("{} response: {:?}", DaemonFields::GetHeight.value(), res); + // don't log this one. The fee estimator blows up logs (T_T) match res { Ok(res) => res, _ => Default::default(), @@ -983,7 +980,7 @@ pub async fn get_block(height: u64) -> reqres::XmrDaemonGetBlockResponse { match client.post(host).json(&req).send().await { Ok(response) => { let res = response.json::().await; - debug!("{} response: {:?}", DaemonFields::GetBlock.value(), res); + // don't log this one. The fee estimator blows up logs (T_T) match res { Ok(res) => res, _ => Default::default(), @@ -997,15 +994,19 @@ pub async fn get_block(height: u64) -> reqres::XmrDaemonGetBlockResponse { pub async fn get_transactions(txs_hashes: Vec) -> reqres::XmrDaemonGetTransactionsResponse { info!("fetching {} transactions", txs_hashes.len()); let client = reqwest::Client::new(); - let host = get_rpc_daemon(); + let args = args::Args::parse(); + let daemon = String::from(args.monero_rpc_daemon); + let url = format!("{}/{}", daemon, DaemonFields::GetTransactions.value()); let req = reqres::XmrDaemonGetTransactionsRequest { txs_hashes, decode_as_json: true, }; - match client.post(host).json(&req).send().await { + match client.post(url).json(&req).send().await { Ok(response) => { - let res = response.json::().await; - debug!("{} response: {:?}", DaemonFields::GetTransactions.value(), res); + let res = response + .json::() + .await; + // don't log this one. The fee estimator blows up logs (T_T) match res { Ok(res) => res, _ => Default::default(), diff --git a/nevmes-core/src/reqres.rs b/nevmes-core/src/reqres.rs index b9b79b3..4285323 100644 --- a/nevmes-core/src/reqres.rs +++ b/nevmes-core/src/reqres.rs @@ -165,7 +165,7 @@ pub struct XmrDaemonGetBlockRequest { pub jsonrpc: String, pub id: String, pub method: String, - pub params: XmrDaemonGetBlockParams, + pub params: XmrDaemonGetBlockParams, } #[derive(Deserialize, Serialize, Debug)] @@ -559,12 +559,15 @@ pub struct XmrDaemonGetBlockResult { pub blob: String, pub block_header: BlockHeader, pub credits: u64, - pub json: String, + pub json: String, pub miner_tx_hash: String, pub status: String, pub top_hash: String, - pub tx_hashes: Vec, - pub untrusted: bool, + /// For some reason this field just disappears on non- + /// + /// coinbase transactions instead of being an empty list. + pub tx_hashes: Option>, + pub untrusted: bool, } // responses @@ -655,17 +658,18 @@ impl Default for XmrDaemonGetBlockResponse { blob: utils::empty_string(), block_header: Default::default(), credits: 0, - json: utils::empty_string(), + json: utils::empty_string(), miner_tx_hash: utils::empty_string(), status: utils::empty_string(), top_hash: utils::empty_string(), - tx_hashes: Vec::new(), - untrusted: false, - } + tx_hashes: Some(Vec::new()), + untrusted: false, + }, } } } +/// Only extract the json string. TODO(c2m): map to a struct #[derive(Serialize, Deserialize, Debug)] pub struct XmrDaemonGetTransactionsResponse { pub txs_as_json: Vec, diff --git a/nevmes-core/src/utils.rs b/nevmes-core/src/utils.rs index 804e0cd..6ad00d3 100644 --- a/nevmes-core/src/utils.rs +++ b/nevmes-core/src/utils.rs @@ -593,31 +593,63 @@ fn validate_installation_hash(sw: ExternalSoftware, filename: &String) -> bool { actual_hash == expected_hash } -/// The highly ineffecient fee estimator. -/// +/// ### The highly ineffecient fee estimator. +/// /// Get the current height. Start fetching blocks -/// +/// /// and checking the number of transactions. If -/// +/// /// there were non-coinbase transactions in the block -/// +/// /// extract the `txnFee` from the `as_json` field. -/// -/// Once we have accumulated n=30 fees paid return the -/// +/// +/// Once we have accumulated n>=30 fees paid return the +/// /// average fee paid from the most recent 30 transactions. -/// +/// /// Note, it may take more than one block to do this, -/// +/// /// especially on stagenet. -pub fn estimate_fee() -> u128 { - - 0 +pub async fn estimate_fee() -> u128 { + let mut height: u64 = 0; + let mut count: u64 = 1; + let mut v_fee: Vec = Vec::new(); + loop { + debug!("current height: {}", height); + if v_fee.len() >= 30 { break; } + let r_height = monero::get_height().await; + height = r_height.height - count; + let block = monero::get_block(height).await; + if block.result.block_header.num_txes > 0 { + debug!("fetching {} txs", block.result.block_header.num_txes); + let tx_hashes: Option> = block.result.tx_hashes; + let transactions = monero::get_transactions(tx_hashes.unwrap()).await; + for tx in transactions.txs_as_json { + let pre_fee_split = tx.split("txnFee\":"); + let mut v1: Vec = pre_fee_split.map(|s| String::from(s)).collect(); + let fee_split = v1.remove(1); + let post_fee_split = fee_split.split(","); + let mut v2: Vec = post_fee_split.map(|s| String::from(s)).collect(); + let fee: u128 = match v2.remove(0).trim().parse::() { + Ok(n) => n, + Err(_e) => 0, + }; + v_fee.push(fee); + } + } + count += 1; + } + &v_fee.iter().sum() / v_fee.len() as u128 } /// Combine the results `estimate_fee()` and `get_balance()` to -/// -/// determine whether or not a transfer is possible. -pub fn can_transfer() -> bool { - false +/// +/// determine whether or not a transfer for a given invoice is possible. +pub async fn can_transfer(invoice: u128) -> bool { + let balance = monero::get_balance().await; + let fee = estimate_fee().await; + debug!("fee estimated to: {}", fee); + debug!("balance: {}", balance.result.unlocked_balance); + debug!("fee + invoice = {}", invoice + fee); + balance.result.unlocked_balance > (fee + invoice) } diff --git a/nevmes-gui/src/apps/address_book.rs b/nevmes-gui/src/apps/address_book.rs index ff7ecfd..bacfab8 100644 --- a/nevmes-gui/src/apps/address_book.rs +++ b/nevmes-gui/src/apps/address_book.rs @@ -74,6 +74,9 @@ pub struct AddressBookApp { approve_contact: bool, approve_payment: bool, added: bool, + can_transfer: bool, + can_transfer_tx: Sender, + can_transfer_rx: Receiver, compose: Compose, contact: String, find_contact: String, @@ -89,6 +92,7 @@ pub struct AddressBookApp { invoice_rx: Receiver, is_adding: bool, is_composing: bool, + is_estimating_fee: bool, is_pinging: bool, is_loading: bool, is_message_sent: bool, @@ -107,6 +111,7 @@ pub struct AddressBookApp { impl Default for AddressBookApp { fn default() -> Self { + let (can_transfer_tx, can_transfer_rx) = std::sync::mpsc::channel(); let (contact_add_tx, contact_add_rx) = std::sync::mpsc::channel(); let (contact_info_tx, contact_info_rx) = std::sync::mpsc::channel(); let (contact_timeout_tx, contact_timeout_rx) = std::sync::mpsc::channel(); @@ -118,6 +123,9 @@ impl Default for AddressBookApp { approve_contact: false, approve_payment: false, added: false, + can_transfer: false, + can_transfer_rx, + can_transfer_tx, compose: Default::default(), contact: utils::empty_string(), contacts: Vec::new(), @@ -133,6 +141,7 @@ impl Default for AddressBookApp { invoice_rx, is_adding: false, is_composing: false, + is_estimating_fee: false, is_loading: false, is_message_sent: false, is_pinging: false, @@ -164,6 +173,7 @@ impl eframe::App for AddressBookApp { self.is_pinging = false; } } + if let Ok(added_contact) = self.contact_add_rx.try_recv() { self.s_added_contact = added_contact; if self.s_added_contact.cid != utils::empty_string() { @@ -171,6 +181,7 @@ impl eframe::App for AddressBookApp { self.is_loading = false; } } + if let Ok(timeout) = self.contact_timeout_rx.try_recv() { self.is_timeout = true; if timeout { @@ -180,9 +191,18 @@ impl eframe::App for AddressBookApp { self.contact = utils::empty_string(); } } + if let Ok(invoice) = self.invoice_rx.try_recv() { self.s_invoice = invoice; + if self.s_invoice.pay_threshold > 0 { + send_can_transfer_req( + self.can_transfer_tx.clone(), + ctx.clone(), self.s_invoice.pay_threshold + ); + self.is_estimating_fee = true; + } } + if let Ok(payment) = self.payment_rx.try_recv() { self.is_payment_processed = payment; if self.is_payment_processed { @@ -191,6 +211,7 @@ impl eframe::App for AddressBookApp { self.showing_status = false; } } + if let Ok(message) = self.send_message_rx.try_recv() { self.is_message_sent = message; if self.is_message_sent { @@ -199,6 +220,11 @@ impl eframe::App for AddressBookApp { self.compose.message = utils::empty_string(); } } + + if let Ok(can_transfer) = self.can_transfer_rx.try_recv() { + self.can_transfer = can_transfer; + self.is_estimating_fee = false; + } // initial contact load if !self.contacts_init { @@ -258,14 +284,19 @@ impl eframe::App for AddressBookApp { ui.add(egui::Spinner::new()); ui.label("creating jwp may take a few minutes..."); } + if self.is_estimating_fee { + ui.add(egui::Spinner::new()); + ui.label("running nevmes jwp fee estimator..."); + } ui.heading(self.status.i2p.clone()); ui.label(format!("pay to: {}", address)); ui.label(format!("amount: {} piconero(s)", amount)); ui.label(format!("expiration: {} blocks", expire)); if !self.is_loading { - if self.s_invoice.address != utils::empty_string() { + if self.s_invoice.address != utils::empty_string() && self.can_transfer { if ui.button("Approve").clicked() { // activate xmr "transfer", check the hash, update db and refresh + // Note it is simply disabled on insufficient funds as calcd by fee estimator let d: reqres::Destination = reqres::Destination { address, amount }; send_payment_req( self.payment_tx.clone(), @@ -764,3 +795,13 @@ fn change_nick_req(contact: String, nick: String) { clear_gui_db(String::from("gui-nick"), String::from(&contact)); write_gui_db(String::from("gui-nick"), String::from(&contact), nick); } + +fn send_can_transfer_req(tx: Sender, ctx: egui::Context, invoice: u128) { + log::debug!("async send_can_transfer_req"); + tokio::spawn(async move { + let can_transfer = utils::can_transfer(invoice).await; + log::debug!("can transfer: {}", can_transfer); + let _ = tx.send(can_transfer); + ctx.request_repaint(); + }); +}