add fee estimator before jwp creation

This commit is contained in:
creating2morrow 2023-05-12 04:51:20 -04:00
parent dea7a3a52a
commit dbcddfa6a4
6 changed files with 123 additions and 43 deletions

View file

@ -3,3 +3,4 @@ format_code_in_doc_comments = true
imports_granularity = "Crate" imports_granularity = "Crate"
imports_layout = "Vertical" imports_layout = "Vertical"
wrap_comments = true wrap_comments = true
ignore = ["lib.rs"]

View file

@ -91,8 +91,8 @@ async fn find_tunnels() {
tokio::time::sleep(Duration::new(120, 0)).await; tokio::time::sleep(Duration::new(120, 0)).await;
create_tunnel(); create_tunnel();
} }
// TODO(c2m): why is i2p-zero http proxy always giving "destination not found" error? // TODO(c2m): why is i2p-zero http proxy always giving "destination not
// if !has_http_tunnel { create_http_proxy(); } // found" error? if !has_http_tunnel { create_http_proxy(); }
} }
pub async fn start() { pub async fn start() {
@ -133,7 +133,8 @@ fn create_tunnel() {
// TODO(c2m): use i2p-zero http proxy // TODO(c2m): use i2p-zero http proxy
// fn create_http_proxy() { // fn create_http_proxy() {
// info!("creating 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"]) // .args(["http.create", "4444"])
// .spawn() // .spawn()
// .expect("i2p-zero failed to create a http proxy"); // .expect("i2p-zero failed to create a http proxy");

View file

@ -935,7 +935,7 @@ pub async fn get_info() -> reqres::XmrDaemonGetInfoResponse {
match client.post(host).json(&req).send().await { match client.post(host).json(&req).send().await {
Ok(response) => { Ok(response) => {
let res = response.json::<reqres::XmrDaemonGetInfoResponse>().await; let res = response.json::<reqres::XmrDaemonGetInfoResponse>().await;
debug!("{} response: {:?}", DaemonFields::GetInfo.value(), res); // add debug log here if needed for adding more info to home screen in gui
match res { match res {
Ok(res) => res, Ok(res) => res,
_ => Default::default(), _ => Default::default(),
@ -949,16 +949,13 @@ pub async fn get_info() -> reqres::XmrDaemonGetInfoResponse {
pub async fn get_height() -> reqres::XmrDaemonGetHeightResponse { pub async fn get_height() -> reqres::XmrDaemonGetHeightResponse {
info!("fetching daemon height"); info!("fetching daemon height");
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let host = get_rpc_daemon(); let args = args::Args::parse();
let req = reqres::XmrRpcRequest { let daemon = String::from(args.monero_rpc_daemon);
jsonrpc: DaemonFields::Version.value(), let req = format!("{}/{}", daemon, DaemonFields::GetHeight.value());
id: DaemonFields::Id.value(), match client.post(req).send().await {
method: DaemonFields::GetHeight.value(),
};
match client.post(host).json(&req).send().await {
Ok(response) => { Ok(response) => {
let res = response.json::<reqres::XmrDaemonGetHeightResponse>().await; let res = response.json::<reqres::XmrDaemonGetHeightResponse>().await;
debug!("{} response: {:?}", DaemonFields::GetHeight.value(), res); // don't log this one. The fee estimator blows up logs (T_T)
match res { match res {
Ok(res) => res, Ok(res) => res,
_ => Default::default(), _ => Default::default(),
@ -983,7 +980,7 @@ pub async fn get_block(height: u64) -> reqres::XmrDaemonGetBlockResponse {
match client.post(host).json(&req).send().await { match client.post(host).json(&req).send().await {
Ok(response) => { Ok(response) => {
let res = response.json::<reqres::XmrDaemonGetBlockResponse>().await; let res = response.json::<reqres::XmrDaemonGetBlockResponse>().await;
debug!("{} response: {:?}", DaemonFields::GetBlock.value(), res); // don't log this one. The fee estimator blows up logs (T_T)
match res { match res {
Ok(res) => res, Ok(res) => res,
_ => Default::default(), _ => Default::default(),
@ -997,15 +994,19 @@ pub async fn get_block(height: u64) -> reqres::XmrDaemonGetBlockResponse {
pub async fn get_transactions(txs_hashes: Vec<String>) -> reqres::XmrDaemonGetTransactionsResponse { pub async fn get_transactions(txs_hashes: Vec<String>) -> reqres::XmrDaemonGetTransactionsResponse {
info!("fetching {} transactions", txs_hashes.len()); info!("fetching {} transactions", txs_hashes.len());
let client = reqwest::Client::new(); 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 { let req = reqres::XmrDaemonGetTransactionsRequest {
txs_hashes, txs_hashes,
decode_as_json: true, decode_as_json: true,
}; };
match client.post(host).json(&req).send().await { match client.post(url).json(&req).send().await {
Ok(response) => { Ok(response) => {
let res = response.json::<reqres::XmrDaemonGetTransactionsResponse>().await; let res = response
debug!("{} response: {:?}", DaemonFields::GetTransactions.value(), res); .json::<reqres::XmrDaemonGetTransactionsResponse>()
.await;
// don't log this one. The fee estimator blows up logs (T_T)
match res { match res {
Ok(res) => res, Ok(res) => res,
_ => Default::default(), _ => Default::default(),

View file

@ -165,7 +165,7 @@ pub struct XmrDaemonGetBlockRequest {
pub jsonrpc: String, pub jsonrpc: String,
pub id: String, pub id: String,
pub method: String, pub method: String,
pub params: XmrDaemonGetBlockParams, pub params: XmrDaemonGetBlockParams,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -559,12 +559,15 @@ pub struct XmrDaemonGetBlockResult {
pub blob: String, pub blob: String,
pub block_header: BlockHeader, pub block_header: BlockHeader,
pub credits: u64, pub credits: u64,
pub json: String, pub json: String,
pub miner_tx_hash: String, pub miner_tx_hash: String,
pub status: String, pub status: String,
pub top_hash: String, pub top_hash: String,
pub tx_hashes: Vec<String>, /// For some reason this field just disappears on non-
pub untrusted: bool, ///
/// coinbase transactions instead of being an empty list.
pub tx_hashes: Option<Vec<String>>,
pub untrusted: bool,
} }
// responses // responses
@ -655,17 +658,18 @@ impl Default for XmrDaemonGetBlockResponse {
blob: utils::empty_string(), blob: utils::empty_string(),
block_header: Default::default(), block_header: Default::default(),
credits: 0, credits: 0,
json: utils::empty_string(), json: utils::empty_string(),
miner_tx_hash: utils::empty_string(), miner_tx_hash: utils::empty_string(),
status: utils::empty_string(), status: utils::empty_string(),
top_hash: utils::empty_string(), top_hash: utils::empty_string(),
tx_hashes: Vec::new(), tx_hashes: Some(Vec::new()),
untrusted: false, untrusted: false,
} },
} }
} }
} }
/// Only extract the json string. TODO(c2m): map to a struct
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct XmrDaemonGetTransactionsResponse { pub struct XmrDaemonGetTransactionsResponse {
pub txs_as_json: Vec<String>, pub txs_as_json: Vec<String>,

View file

@ -593,31 +593,63 @@ fn validate_installation_hash(sw: ExternalSoftware, filename: &String) -> bool {
actual_hash == expected_hash actual_hash == expected_hash
} }
/// The highly ineffecient fee estimator. /// ### The highly ineffecient fee estimator.
/// ///
/// Get the current height. Start fetching blocks /// Get the current height. Start fetching blocks
/// ///
/// and checking the number of transactions. If /// and checking the number of transactions. If
/// ///
/// there were non-coinbase transactions in the block /// there were non-coinbase transactions in the block
/// ///
/// extract the `txnFee` from the `as_json` field. /// 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. /// average fee paid from the most recent 30 transactions.
/// ///
/// Note, it may take more than one block to do this, /// Note, it may take more than one block to do this,
/// ///
/// especially on stagenet. /// especially on stagenet.
pub fn estimate_fee() -> u128 { pub async fn estimate_fee() -> u128 {
let mut height: u64 = 0;
0 let mut count: u64 = 1;
let mut v_fee: Vec<u128> = 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<Vec<String>> = 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<String> = 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<String> = post_fee_split.map(|s| String::from(s)).collect();
let fee: u128 = match v2.remove(0).trim().parse::<u128>() {
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 /// Combine the results `estimate_fee()` and `get_balance()` to
/// ///
/// determine whether or not a transfer is possible. /// determine whether or not a transfer for a given invoice is possible.
pub fn can_transfer() -> bool { pub async fn can_transfer(invoice: u128) -> bool {
false 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)
} }

View file

@ -74,6 +74,9 @@ pub struct AddressBookApp {
approve_contact: bool, approve_contact: bool,
approve_payment: bool, approve_payment: bool,
added: bool, added: bool,
can_transfer: bool,
can_transfer_tx: Sender<bool>,
can_transfer_rx: Receiver<bool>,
compose: Compose, compose: Compose,
contact: String, contact: String,
find_contact: String, find_contact: String,
@ -89,6 +92,7 @@ pub struct AddressBookApp {
invoice_rx: Receiver<reqres::Invoice>, invoice_rx: Receiver<reqres::Invoice>,
is_adding: bool, is_adding: bool,
is_composing: bool, is_composing: bool,
is_estimating_fee: bool,
is_pinging: bool, is_pinging: bool,
is_loading: bool, is_loading: bool,
is_message_sent: bool, is_message_sent: bool,
@ -107,6 +111,7 @@ pub struct AddressBookApp {
impl Default for AddressBookApp { impl Default for AddressBookApp {
fn default() -> Self { 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_add_tx, contact_add_rx) = std::sync::mpsc::channel();
let (contact_info_tx, contact_info_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(); let (contact_timeout_tx, contact_timeout_rx) = std::sync::mpsc::channel();
@ -118,6 +123,9 @@ impl Default for AddressBookApp {
approve_contact: false, approve_contact: false,
approve_payment: false, approve_payment: false,
added: false, added: false,
can_transfer: false,
can_transfer_rx,
can_transfer_tx,
compose: Default::default(), compose: Default::default(),
contact: utils::empty_string(), contact: utils::empty_string(),
contacts: Vec::new(), contacts: Vec::new(),
@ -133,6 +141,7 @@ impl Default for AddressBookApp {
invoice_rx, invoice_rx,
is_adding: false, is_adding: false,
is_composing: false, is_composing: false,
is_estimating_fee: false,
is_loading: false, is_loading: false,
is_message_sent: false, is_message_sent: false,
is_pinging: false, is_pinging: false,
@ -164,6 +173,7 @@ impl eframe::App for AddressBookApp {
self.is_pinging = false; self.is_pinging = false;
} }
} }
if let Ok(added_contact) = self.contact_add_rx.try_recv() { if let Ok(added_contact) = self.contact_add_rx.try_recv() {
self.s_added_contact = added_contact; self.s_added_contact = added_contact;
if self.s_added_contact.cid != utils::empty_string() { if self.s_added_contact.cid != utils::empty_string() {
@ -171,6 +181,7 @@ impl eframe::App for AddressBookApp {
self.is_loading = false; self.is_loading = false;
} }
} }
if let Ok(timeout) = self.contact_timeout_rx.try_recv() { if let Ok(timeout) = self.contact_timeout_rx.try_recv() {
self.is_timeout = true; self.is_timeout = true;
if timeout { if timeout {
@ -180,9 +191,18 @@ impl eframe::App for AddressBookApp {
self.contact = utils::empty_string(); self.contact = utils::empty_string();
} }
} }
if let Ok(invoice) = self.invoice_rx.try_recv() { if let Ok(invoice) = self.invoice_rx.try_recv() {
self.s_invoice = invoice; 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() { if let Ok(payment) = self.payment_rx.try_recv() {
self.is_payment_processed = payment; self.is_payment_processed = payment;
if self.is_payment_processed { if self.is_payment_processed {
@ -191,6 +211,7 @@ impl eframe::App for AddressBookApp {
self.showing_status = false; self.showing_status = false;
} }
} }
if let Ok(message) = self.send_message_rx.try_recv() { if let Ok(message) = self.send_message_rx.try_recv() {
self.is_message_sent = message; self.is_message_sent = message;
if self.is_message_sent { if self.is_message_sent {
@ -199,6 +220,11 @@ impl eframe::App for AddressBookApp {
self.compose.message = utils::empty_string(); 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 // initial contact load
if !self.contacts_init { if !self.contacts_init {
@ -258,14 +284,19 @@ impl eframe::App for AddressBookApp {
ui.add(egui::Spinner::new()); ui.add(egui::Spinner::new());
ui.label("creating jwp may take a few minutes..."); 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.heading(self.status.i2p.clone());
ui.label(format!("pay to: {}", address)); ui.label(format!("pay to: {}", address));
ui.label(format!("amount: {} piconero(s)", amount)); ui.label(format!("amount: {} piconero(s)", amount));
ui.label(format!("expiration: {} blocks", expire)); ui.label(format!("expiration: {} blocks", expire));
if !self.is_loading { 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() { if ui.button("Approve").clicked() {
// activate xmr "transfer", check the hash, update db and refresh // 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 }; let d: reqres::Destination = reqres::Destination { address, amount };
send_payment_req( send_payment_req(
self.payment_tx.clone(), 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)); clear_gui_db(String::from("gui-nick"), String::from(&contact));
write_gui_db(String::from("gui-nick"), String::from(&contact), nick); write_gui_db(String::from("gui-nick"), String::from(&contact), nick);
} }
fn send_can_transfer_req(tx: Sender<bool>, 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();
});
}