From 95be986e0ff1c4fad6dcc4adbec6089998c58149 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 20 Apr 2024 16:59:54 +0300 Subject: [PATCH] wallet + ui: separate transaction struct, fix tx cancel, fix tx cancel at finalizing state, post tx from slatepack --- locales/en.yml | 4 +- locales/ru.yml | 4 +- src/gui/views/network/node.rs | 8 +- src/gui/views/views.rs | 2 +- src/gui/views/wallets/wallet/content.rs | 10 +- src/gui/views/wallets/wallet/messages.rs | 237 +++++++++++-------- src/gui/views/wallets/wallet/txs.rs | 118 +++++----- src/wallet/config.rs | 17 +- src/wallet/types.rs | 13 +- src/wallet/wallet.rs | 278 +++++++++++++---------- 10 files changed, 395 insertions(+), 296 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index e28cb13..b525f7b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -61,8 +61,8 @@ wallets: tx_sending: Sending tx_receiving: Receiving tx_confirming: Awaiting confirmation - tx_cancelling: Cancelling tx_canceled: Canceled + tx_finalizing: Finalizing tx_confirmed: Confirmed txs: Transactions messages: Messages @@ -73,8 +73,10 @@ wallets: pay_balance_error: 'Account balance is insufficient to pay %{amount} ツ and network fee.' parse_i1_slatepack_desc: 'To pay %{amount} ツ send this message to the receiver:' parse_i2_slatepack_desc: 'Finalize transaction to receive %{amount} ツ' + parse_i3_slatepack_desc: 'Post transaction to finalize receiving of %{amount} ツ' parse_s1_slatepack_desc: 'To receive %{amount} ツ send this message to the sender:' parse_s2_slatepack_desc: 'Finalize transaction to send %{amount} ツ' + parse_s3_slatepack_desc: 'Post transaction to finalize sending of %{amount} ツ' response_slatepack_err: 'An error occurred during creation of the response, check input data:' response_exists_err: 'Such transaction already exists:' create_request_desc: 'Create request to send or receive the funds:' diff --git a/locales/ru.yml b/locales/ru.yml index b06939d..69fb776 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -61,8 +61,8 @@ wallets: tx_sending: Отправка tx_receiving: Получение tx_confirming: Ожидает подтверждения - tx_cancelling: Отмена tx_canceled: Отменено + tx_finalizing: Завершение tx_confirmed: Подтверждено txs: Транзакции messages: Сообщения @@ -73,8 +73,10 @@ wallets: pay_balance_error: 'Средств на аккаунте недостаточно для оплаты %{amount} ツ и комиссии сети.' parse_i1_slatepack_desc: 'Для оплаты %{amount} ツ отправьте это сообщение получателю:' parse_i2_slatepack_desc: 'Завершите транзакцию для получения %{amount} ツ' + parse_i3_slatepack_desc: 'Опубликуйте транзакцию для завершения получения %{amount} ツ' parse_s1_slatepack_desc: 'Для получения %{amount} ツ отправьте это сообщение отправителю:' parse_s2_slatepack_desc: 'Завершите транзакцию для отправки %{amount} ツ' + parse_s3_slatepack_desc: 'Опубликуйте транзакцию для завершения отправки %{amount} ツ' response_slatepack_err: 'Во время создания ответа произошла ошибка, проверьте входные данные:' response_exists_err: 'Такая транзакция уже существует:' create_request_desc: 'Cоздать запрос на получение или отправку средств:' diff --git a/src/gui/views/network/node.rs b/src/gui/views/network/node.rs index edd0d0c..e99d929 100644 --- a/src/gui/views/network/node.rs +++ b/src/gui/views/network/node.rs @@ -51,7 +51,7 @@ impl NetworkTab for NetworkNode { .id_source("integrated_node") .auto_shrink([false; 2]) .show(ui, |ui| { - ui.add_space(1.0); + ui.add_space(2.0); // Show header info. View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); @@ -85,7 +85,7 @@ impl NetworkTab for NetworkNode { [false, false, false, true]); }); }); - ui.add_space(4.0); + ui.add_space(5.0); // Show block info. View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block"))); @@ -119,7 +119,7 @@ impl NetworkTab for NetworkNode { [false, false, false, true]); }); }); - ui.add_space(4.0); + ui.add_space(5.0); // Show data info. View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data"))); @@ -161,7 +161,7 @@ impl NetworkTab for NetworkNode { [false, false, false, true]); }); }); - ui.add_space(4.0); + ui.add_space(5.0); // Show peer stats when available. if stats.peer_count > 0 { diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index a1f0f6f..98c0bcc 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -544,7 +544,7 @@ impl View { pub fn format_time(ts: i64) -> String { let utc_offset = chrono::Local::now().offset().local_minus_utc(); let utc_time = ts + utc_offset as i64; - let tx_time = chrono::NaiveDateTime::from_timestamp_opt(utc_time, 0).unwrap(); + let tx_time = chrono::DateTime::from_timestamp(utc_time, 0).unwrap(); tx_time.format("%d/%m/%Y %H:%M:%S").to_string() } diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index b4110eb..7fd7ae6 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -118,7 +118,7 @@ impl WalletContent { ui.vertical_centered(|ui| { // Draw wallet tabs. View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(ui); + self.tabs_ui(ui, wallet); }); }); }); @@ -340,7 +340,7 @@ impl WalletContent { } /// Draw tab buttons in the bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui) { + fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(4.0, 0.0); @@ -356,8 +356,10 @@ impl WalletContent { }); }); columns[1].vertical_centered_justified(|ui| { - View::tab_button(ui, CHAT_CIRCLE_TEXT, current_type == WalletTabType::Messages, || { - self.current_tab = Box::new(WalletMessages::default()); + let is_messages = current_type == WalletTabType::Messages; + View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { + let dandelion = wallet.get_config().use_dandelion.unwrap_or(true); + self.current_tab = Box::new(WalletMessages::new(dandelion)); }); }); columns[2].vertical_centered_justified(|ui| { diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs index b1c065a..98302d0 100644 --- a/src/gui/views/wallets/wallet/messages.rs +++ b/src/gui/views/wallets/wallet/messages.rs @@ -15,16 +15,17 @@ use egui::{Id, Margin, RichText, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::{Slate, SlateState, TxLogEntry}; +use grin_wallet_libwallet::{Slate, SlateState}; use log::error; use crate::gui::Colors; -use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD, UPLOAD}; +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD, PROHIBIT, UPLOAD}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Root, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; +use crate::wallet::types::WalletTransaction; use crate::wallet::Wallet; #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] @@ -64,7 +65,7 @@ pub struct WalletMessages { /// Generated Slatepack response message. response_edit: String, /// Flag to check if Dandelion is needed to finalize transaction. - use_dandelion: Option, + dandelion: bool, /// Amount to send or receive. amount_edit: String, @@ -77,15 +78,15 @@ pub struct WalletMessages { /// Identifier for invoice amount [`Modal`]. const AMOUNT_MODAL: &'static str = "amount_modal"; -impl Default for WalletMessages { - fn default() -> Self { +impl WalletMessages { + pub fn new(dandelion: bool) -> Self { Self { send_request: false, message_edit: "".to_string(), message_slate: None, message_error: None, response_edit: "".to_string(), - use_dandelion: None, + dandelion, amount_edit: "".to_string(), request_edit: "".to_string(), request_error: None, @@ -202,12 +203,18 @@ impl WalletMessages { SlateState::Standard2 => { t!("wallets.parse_s2_slatepack_desc","amount" => amount) } + SlateState::Standard3 => { + t!("wallets.parse_s3_slatepack_desc","amount" => amount) + } SlateState::Invoice1 => { t!("wallets.parse_i1_slatepack_desc","amount" => amount) } SlateState::Invoice2 => { t!("wallets.parse_i2_slatepack_desc","amount" => amount) } + SlateState::Invoice3 => { + t!("wallets.parse_i3_slatepack_desc","amount" => amount) + } _ => { t!("wallets.input_slatepack_desc") } @@ -215,10 +222,10 @@ impl WalletMessages { }; ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT)); } - ui.add_space(7.0); + ui.add_space(6.0); // Setup Slatepack message text input. - let mut message = if response_empty { + let message = if response_empty { &mut self.message_edit } else { &mut self.response_edit @@ -253,22 +260,52 @@ impl WalletMessages { View::horizontal_line(ui, Colors::ITEM_STROKE); ui.add_space(10.0); + // Parse Slatepack message if input field was changed, resetting message error. + if &message_before != message { + self.parse_message(wallet); + } + // Draw buttons to clear/copy/paste. let fields_empty = self.message_edit.is_empty() && self.response_edit.is_empty(); let columns_num = if fields_empty { 1 } else { 2 }; let mut show_dandelion = false; ui.scope(|ui| { // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); ui.columns(columns_num, |columns| { let first_column_content = |ui: &mut egui::Ui| { - if self.message_slate.is_some() && self.message_error.is_none() { - self.clear_message_button_ui(ui); + if self.message_slate.is_some() { + if self.response_edit.is_empty() { + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::BUTTON, || { + self.message_edit.clear(); + self.response_edit.clear(); + self.message_error = None; + self.message_slate = None; + }); + } else { + let clear_text = format!("{} {}", PROHIBIT, t!("modal.cancel")); + View::button(ui, clear_text, Colors::BUTTON, || { + let slate = self.message_slate.clone().unwrap(); + if let Some(tx) = wallet.tx_by_slate(&slate) { + wallet.cancel(tx.data.id); + self.message_edit.clear(); + self.response_edit.clear(); + self.message_slate = None; + } + }); + } } else { let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); View::button(ui, paste_text, Colors::BUTTON, || { - self.message_edit = cb.get_string_from_buffer(); + let buf = cb.get_string_from_buffer(); + let previous = self.message_edit.clone(); + self.message_edit = buf.clone(); + // Parse Slatepack message resetting message error. + if buf != previous { + self.parse_message(wallet); + } }); } }; @@ -277,27 +314,53 @@ impl WalletMessages { } else { columns[0].vertical_centered_justified(first_column_content); columns[1].vertical_centered_justified(|ui| { - if self.message_error.is_some() { - self.clear_message_button_ui(ui); - } else if !self.response_edit.is_empty() { - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::BUTTON, || { - cb.copy_string_to_buffer(self.response_edit.clone()); - }); - } else { - show_dandelion = true; - View::button(ui, t!("wallets.finalize"), Colors::GOLD, || { - let message = self.message_edit.clone(); - let use_dandelion = self.use_dandelion.unwrap(); - if let Ok(_) = wallet.finalize(message, use_dandelion) { + if self.message_slate.is_some() { + if !self.response_edit.is_empty() { + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::BUTTON, || { + cb.copy_string_to_buffer(self.response_edit.clone()); self.message_edit.clear(); + self.response_edit.clear(); self.message_slate = None; - - } else { - self.message_error = Some( - MessageError::Finalize(t!("wallets.finalize_slatepack_err")) - ); - } + }); + } else { + show_dandelion = true; + View::button(ui, t!("wallets.finalize"), Colors::GOLD, || { + let message = self.message_edit.clone(); + let slate = self.message_slate.clone().unwrap(); + if slate.state == SlateState::Invoice3 || + slate.state == SlateState::Standard3 { + if let Ok(_) = wallet.post(&slate, self.dandelion) { + self.message_edit.clear(); + self.message_slate = None; + } else { + self.message_error = Some( + MessageError::Finalize( + t!("wallets.finalize_slatepack_err") + ) + ); + } + } else { + if let Ok(_) = wallet.finalize(message, self.dandelion) { + self.message_edit.clear(); + self.message_slate = None; + } else { + self.message_error = Some( + MessageError::Finalize( + t!("wallets.finalize_slatepack_err") + ) + ); + } + } + }); + } + } else { + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::BUTTON, || { + self.message_error = None; + self.message_edit.clear(); + self.response_edit.clear(); + self.message_slate = None; }); } }); @@ -307,35 +370,17 @@ impl WalletMessages { // Draw setup of ability to post transaction with Dandelion. if show_dandelion { - if self.use_dandelion.is_none() { - self.use_dandelion = if let Some(u) = wallet.get_config().use_dandelion { - Some(u) - } else { - Some(true) - }; - } - let use_dandelion = self.use_dandelion.unwrap(); - View::checkbox(ui, use_dandelion, t!("wallets.use_dandelion"), || { - self.use_dandelion = Some(!use_dandelion); - wallet.update_use_dandelion(use_dandelion); + let dandelion_before = self.dandelion; + View::checkbox(ui, dandelion_before, t!("wallets.use_dandelion"), || { + self.dandelion = !dandelion_before; + wallet.update_use_dandelion(self.dandelion); }); } - - message = if response_empty { - &mut self.message_edit - } else { - &mut self.response_edit - }; - - // Parse Slatepack message if input field was changed, resetting message error. - if &message_before != message { - self.message_error = None; - self.parse_message(wallet); - } } - /// Parse message input into [`Slate`], making operations like receive or pay to confirm. + /// Parse message input into [`Slate`] updating slate and response input. fn parse_message(&mut self, wallet: &mut Wallet) { + self.message_error = None; if self.message_edit.is_empty() { return; } @@ -348,16 +393,7 @@ impl WalletMessages { self.response_edit = resp; } else { // Check if tx with same slate id already exists. - let mut exists_tx = false; - let _ = wallet.get_data().unwrap().txs.clone().iter().map(|tx| { - if tx.tx_slate_id == Some(slate.id) { - exists_tx= true; - self.message_error = Some( - MessageError::Response(t!("wallets.response_exists_err")) - ); - } - tx - }).collect::>(); + let exists_tx = wallet.tx_by_slate(&slate).is_some(); if exists_tx { return; } @@ -389,24 +425,21 @@ impl WalletMessages { } } } - _ => {} + _ => { + self.response_edit = "".to_string(); + } } // Try to get amount from transaction by id. if slate.amount == 0 { let _ = wallet.get_data().unwrap().txs.clone().iter().map(|tx| { - if tx.tx_slate_id == Some(slate.id) { + if tx.data.tx_slate_id == Some(slate.id) { if slate.amount == 0 { - let amount = if tx.amount_debited > tx.amount_credited { - tx.amount_debited - tx.amount_credited - } else { - tx.amount_credited - tx.amount_debited - }; - slate.amount = amount; + slate.amount = tx.amount; } } tx - }).collect::>(); + }).collect::>(); } self.message_slate = Some(slate.clone()); } else { @@ -415,17 +448,6 @@ impl WalletMessages { } } - /// Draw button to clear entered message, slate and errors. - fn clear_message_button_ui(&mut self, ui: &mut egui::Ui) { - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::BUTTON, || { - self.message_error = None; - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - }); - } - /// Draw creation of request to send or receive funds. fn request_ui(&mut self, ui: &mut egui::Ui, @@ -436,7 +458,7 @@ impl WalletMessages { ui.add_space(7.0); // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { @@ -499,27 +521,42 @@ impl WalletMessages { // Draw invoice amount text edit. let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id); let amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center(); - let mut amount_edit_before = self.amount_edit.clone(); - View::text_edit(ui, cb, &mut amount_edit_before, amount_edit_opts); + let amount_edit_before = self.amount_edit.clone(); + View::text_edit(ui, cb, &mut self.amount_edit, amount_edit_opts); // Check value if input was changed. if amount_edit_before != self.amount_edit { self.request_error = None; - match amount_from_hr_string(amount_edit_before.as_str()) { - Ok(a) => { - if a <= 0 { - return; - } - // Do not input amount more than balance in sending. - if self.send_request { - let b = wallet.get_data().unwrap().info.amount_currently_spendable; - if b < a { - return; + if !self.amount_edit.is_empty() { + match amount_from_hr_string(self.amount_edit.as_str()) { + Ok(a) => { + if !self.amount_edit.contains(".") { + // To avoid input of several "0". + if a == 0 { + self.amount_edit = "0".to_string(); + return; + } + } else { + // Check input after ".". + let parts = self.amount_edit.split(".").collect::>(); + if parts.len() == 2 && parts[1].len() > 9 { + self.amount_edit = amount_edit_before; + return; + } + } + + // Do not input amount more than balance in sending. + if self.send_request { + let b = wallet.get_data().unwrap().info.amount_currently_spendable; + if b < a { + self.amount_edit = amount_edit_before; + } } } - self.amount_edit = amount_edit_before; + Err(_) => { + self.amount_edit = amount_edit_before; + } } - Err(_) => {} } } diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs index 03f6fc5..93128cf 100644 --- a/src/gui/views/wallets/wallet/txs.rs +++ b/src/gui/views/wallets/wallet/txs.rs @@ -15,17 +15,19 @@ use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; use grin_core::core::amount_to_hr_string; -use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType}; +use grin_wallet_libwallet::{TxLogEntryType}; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Root, View}; use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::WalletTabType; +use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; +use crate::wallet::types::{WalletData, WalletTransaction}; use crate::wallet::Wallet; + /// Wallet info tab content. #[derive(Default)] pub struct WalletInfo; @@ -69,6 +71,7 @@ impl WalletInfo { /// Draw transactions content. fn txs_ui(&self, ui: &mut egui::Ui, wallet: &mut Wallet) { let data = wallet.get_data().unwrap(); + let config = wallet.get_config(); let txs_size = data.txs.len(); // Show transactions info. @@ -133,7 +136,7 @@ impl WalletInfo { ui.add_space(3.0); ScrollArea::vertical() .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) - .id_source(Id::from("txs_content").with(wallet.get_config().id)) + .id_source(Id::from("txs_content").with(config.id)) .auto_shrink([false; 2]) .show_rows(ui, TX_ITEM_HEIGHT, txs_size, |ui, row_range| { ui.add_space(4.0); @@ -143,17 +146,11 @@ impl WalletInfo { // Setup item rounding. let item_rounding = View::item_rounding(index, txs_size, false); // Show transaction item. - tx_item_ui(ui, tx, item_rounding, data.info.last_confirmed_height, wallet); + tx_item_ui(ui, tx, item_rounding, config.min_confirmations, &data, wallet); } }); ui.add_space(2.0); }); - - // for tx in &data.txs { - // if tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxSentCancelled { - // println!("tx: {}", serde_json::to_string::(tx).unwrap()); - // } - // } } } @@ -162,9 +159,10 @@ const TX_ITEM_HEIGHT: f32 = 76.0; /// Draw transaction item. fn tx_item_ui(ui: &mut egui::Ui, - tx: &TxLogEntry, + tx: &WalletTransaction, mut rounding: Rounding, - last_height: u64, + min_conf: u64, + data: &WalletData, wallet: &mut Wallet) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); @@ -175,11 +173,6 @@ fn tx_item_ui(ui: &mut egui::Ui, let bg_rect = rect.clone(); ui.painter().rect(bg_rect, rounding, Colors::BUTTON, View::ITEM_STROKE); - // Setup transaction flags. - let is_canceled = tx.tx_type == TxLogEntryType::TxSentCancelled - || tx.tx_type == TxLogEntryType::TxReceivedCancelled; - let is_cancelling = wallet.is_cancelling(&tx.id); - ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.add_space(-6.0); @@ -190,10 +183,11 @@ fn tx_item_ui(ui: &mut egui::Ui, //TODO: Show tx info }); - if !is_cancelling && !tx.confirmed && tx.tx_type != TxLogEntryType::TxReceivedCancelled - && tx.tx_type != TxLogEntryType::TxSentCancelled { + if !tx.posting && !tx.data.confirmed && + tx.data.tx_type != TxLogEntryType::TxReceivedCancelled + && tx.data.tx_type != TxLogEntryType::TxSentCancelled { View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::RED), || { - wallet.cancel(tx.id); + wallet.cancel(tx.data.id); }); } @@ -201,18 +195,25 @@ fn tx_item_ui(ui: &mut egui::Ui, ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.add_space(12.0); ui.vertical(|ui| { - // Setup transaction amount. ui.add_space(3.0); - let amount_text = if tx.amount_credited > tx.amount_debited { - format!("+{}", - amount_to_hr_string(tx.amount_credited - tx.amount_debited, true)) + + // Setup transaction amount. + let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxSentCancelled { + "-" + } else if tx.data.tx_type == TxLogEntryType::TxReceived || + tx.data.tx_type == TxLogEntryType::TxReceivedCancelled { + "+" } else { - format!("-{}", - amount_to_hr_string(tx.amount_debited - tx.amount_credited, true)) - }; + "" + }.to_string(); + amount_text = format!("{}{} {}", + amount_text, + amount_to_hr_string(tx.amount, true), + GRIN); // Setup amount color. - let amount_color = match tx.tx_type { + let amount_color = match tx.data.tx_type { TxLogEntryType::ConfirmedCoinbase => Colors::BLACK, TxLogEntryType::TxReceived => Colors::BLACK, TxLogEntryType::TxSent => Colors::BLACK, @@ -224,13 +225,16 @@ fn tx_item_ui(ui: &mut egui::Ui, ui.add_space(-2.0); // Setup transaction status text. - let status_text = if !tx.confirmed { - if wallet.is_cancelling(&tx.id) { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling")) - } else if is_canceled { + let status_text = if !tx.data.confirmed { + let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled + || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; + if is_canceled { format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) + } else if tx.data.kernel_excess.is_some() && + tx.data.tx_type == TxLogEntryType::TxReceived { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) } else { - match tx.tx_type { + match tx.data.tx_type { TxLogEntryType::TxReceived => { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_receiving")) }, @@ -238,31 +242,37 @@ fn tx_item_ui(ui: &mut egui::Ui, format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_sending")) }, _ => { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirmed")) } } } } else { - let tx_height = tx.kernel_lookup_min_height.unwrap_or(0); - let min_confirmations = wallet.get_config().min_confirmations; - match tx.tx_type { + let tx_height = tx.data.kernel_lookup_min_height.unwrap_or(0); + match tx.data.tx_type { TxLogEntryType::ConfirmedCoinbase => { format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) }, - TxLogEntryType::TxReceived => { - if last_height - tx_height > min_confirmations { - format!("{} {}", ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) + TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { + if data.info.last_confirmed_height - tx_height > min_conf { + let text = if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.tx_sent") + } else { + t!("wallets.tx_received") + }; + format!("{} {}", ARROW_CIRCLE_DOWN, text) } else { - format!("{} {}", + let h = data.info.last_confirmed_height; + let left_conf = h - tx_height; + let conf_info = if h >= tx_height && left_conf <= min_conf { + format!("{}/{}", left_conf, min_conf) + } else { + "".to_string() + }; + format!("{} {} {}", DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } - }, - TxLogEntryType::TxSent => { - if last_height - tx_height > min_confirmations { - format!("{} {}", ARROW_CIRCLE_UP, t!("wallets.tx_sent")) - } else { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) + t!("wallets.tx_confirming"), + conf_info + ) } }, _ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")) @@ -270,14 +280,14 @@ fn tx_item_ui(ui: &mut egui::Ui, }; // Setup status text color. - let status_color = match tx.tx_type { + let status_color = match tx.data.tx_type { TxLogEntryType::ConfirmedCoinbase => Colors::TEXT, - TxLogEntryType::TxReceived => if tx.confirmed { + TxLogEntryType::TxReceived => if tx.data.confirmed { Colors::GREEN } else { Colors::TEXT }, - TxLogEntryType::TxSent => if tx.confirmed { + TxLogEntryType::TxSent => if tx.data.confirmed { Colors::RED } else { Colors::TEXT @@ -289,7 +299,7 @@ fn tx_item_ui(ui: &mut egui::Ui, ui.label(RichText::new(status_text).size(15.0).color(status_color)); // Setup transaction time. - let tx_time = View::format_time(tx.creation_ts.timestamp()); + let tx_time = View::format_time(tx.data.creation_ts.timestamp()); let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::GRAY)); }); diff --git a/src/wallet/config.rs b/src/wallet/config.rs index a1c9b05..ce1d80f 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; use std::string::ToString; use grin_core::global::ChainTypes; +use grin_wallet_libwallet::{SlateState}; use serde_derive::{Deserialize, Serialize}; use crate::{AppConfig, Settings}; @@ -120,14 +121,16 @@ impl WalletConfig { config_path.to_str().unwrap().to_string() } - /// Get slatepacks data path for current wallet. - pub fn get_slatepacks_path(&self) -> PathBuf { - let mut slatepacks_dir = PathBuf::from(self.get_data_path()); - slatepacks_dir.push(SLATEPACKS_DIR_NAME); - if !slatepacks_dir.exists() { - let _ = fs::create_dir_all(slatepacks_dir.clone()); + /// Get Slatepacks data path for current wallet. + pub fn get_slatepack_path(&self, id: String, state: &SlateState) -> PathBuf { + let mut slatepack_dir = PathBuf::from(self.get_data_path()); + slatepack_dir.push(SLATEPACKS_DIR_NAME); + if !slatepack_dir.exists() { + let _ = fs::create_dir_all(slatepack_dir.clone()); } - slatepacks_dir + let slatepack_file_name = format!("{}.{}.slatepack", id, state); + slatepack_dir.push(slatepack_file_name); + slatepack_dir } /// Save wallet config. diff --git a/src/wallet/types.rs b/src/wallet/types.rs index c263494..bca8408 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -136,5 +136,16 @@ pub struct WalletData { /// Balance data for current account. pub info: WalletInfo, /// Transactions data. - pub txs: Vec + pub txs: Vec +} + +/// Wallet transaction data. +#[derive(Clone)] +pub struct WalletTransaction { + /// Transaction information. + pub data: TxLogEntry, + /// Calculated total transaction amount. + pub amount: u64, + /// Flag to check if transaction is posting after finalizing. + pub posting: bool } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index a39f40c..25d5aec 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -31,17 +31,16 @@ use grin_keychain::{ExtKeychain, Identifier, Keychain}; use grin_util::Mutex; use grin_util::types::ZeroingString; use grin_wallet_api::Owner; -use grin_wallet_controller::command::parse_slatepack; use grin_wallet_controller::controller; use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; -use grin_wallet_libwallet::{Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, Slate, SlatepackAddress, StatusMessage, TxLogEntry, TxLogEntryType, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::{Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, StatusMessage, TxLogEntry, TxLogEntryType, WalletInst, WalletLCProvider}; use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, retrieve_txs}; use crate::AppConfig; use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig}; -use crate::wallet::types::{ConnectionMethod, WalletAccount, WalletData, WalletInstance}; +use crate::wallet::types::{ConnectionMethod, WalletAccount, WalletData, WalletInstance, WalletTransaction}; /// Contains wallet instance, configuration and state, handles wallet commands. #[derive(Clone)] @@ -86,10 +85,7 @@ pub struct Wallet { /// Flag to check if wallet repairing and restoring missing outputs is needed. repair_needed: Arc, /// Wallet repair progress in percents. - repair_progress: Arc, - - /// Identifiers for transactions to cancel. - cancel_txs: Arc>> + repair_progress: Arc } /// Default Foreign API server host. @@ -117,8 +113,7 @@ impl Wallet { data: Arc::new(RwLock::new(None)), sync_attempts: Arc::new(AtomicU8::new(0)), repair_needed: Arc::new(AtomicBool::new(false)), - repair_progress: Arc::new(AtomicU8::new(0)), - cancel_txs: Arc::new(RwLock::new(BTreeSet::new())), + repair_progress: Arc::new(AtomicU8::new(0)) } } @@ -470,15 +465,8 @@ impl Wallet { /// Parse Slatepack message into [`Slate`]. pub fn parse_slatepack(&self, message: String) -> Result { - let mut api = Owner::new(self.instance.clone().unwrap(), None); - return match parse_slatepack(&mut api, None, None, Some(message.clone())) { - Ok((slate, _)) => { - Ok(slate) - } - Err(_) => { - Err(Error::SlatepackDeser("Slatepack parse error".to_string())) - } - } + let api = Owner::new(self.instance.clone().unwrap(), None); + api.slate_from_slatepack_message(None, message, vec![]) } /// Create Slatepack message from provided slate. @@ -490,18 +478,29 @@ impl Wallet { Ok(()) })?; - // Create a directory to which slatepack files will be output. - let mut slatepack_dir = self.get_config().get_slatepacks_path(); - let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state); - slatepack_dir.push(slatepack_file_name); - - // Write Slatepack response into the file. + // Save slatepack. + let slatepack_dir = self.get_config().get_slatepack_path(slate.id.to_string(), &slate.state); let mut output = File::create(slatepack_dir)?; output.write_all(message.as_bytes())?; output.sync_all()?; Ok(message) } + /// Get transaction by slate id. + pub fn tx_by_slate(&self, slate: &Slate) -> Option { + if let Some(data) = self.get_data() { + let txs = data.txs.clone().iter().map(|tx| tx.clone()).filter(|tx| { + tx.data.tx_slate_id == Some(slate.id) + }).collect::>(); + return if let Some(tx) = txs.get(0) { + Some(tx.clone()) + } else { + None + } + } + None + } + /// Initialize a transaction to send amount, return request for funds receiver. pub fn send(&self, amount: u64) -> Result { let config = self.get_config(); @@ -513,7 +512,7 @@ impl Wallet { selection_strategy_is_use_all: false, ..Default::default() }; - let mut api = Owner::new(self.instance.clone().unwrap(), None); + let api = Owner::new(self.instance.clone().unwrap(), None); let slate = api.init_send_tx(None, args)?; // Lock outputs to for this transaction. @@ -535,7 +534,7 @@ impl Wallet { amount, target_slate_version: None, }; - let mut api = Owner::new(self.instance.clone().unwrap(), None); + let api = Owner::new(self.instance.clone().unwrap(), None); let slate = api.issue_invoice_tx(None, args)?; // Create Slatepack message response. @@ -558,7 +557,7 @@ impl Wallet { selection_strategy_is_use_all: false, ..Default::default() }; - let mut api = Owner::new(self.instance.clone().unwrap(), None); + let api = Owner::new(self.instance.clone().unwrap(), None); let slate = api.process_invoice_tx(None, &slate, args)?; api.tx_lock_outputs(None, &slate)?; @@ -574,7 +573,7 @@ impl Wallet { /// Handle message to receive funds, return response to sender. pub fn receive(&self, message: String) -> Result { let mut slate = self.parse_slatepack(message)?; - let mut api = Owner::new(self.instance.clone().unwrap(), None); + let api = Owner::new(self.instance.clone().unwrap(), None); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; Ok(()) @@ -591,37 +590,50 @@ impl Wallet { /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. pub fn finalize(&self, message: String, dandelion: bool) -> Result { let mut slate = self.parse_slatepack(message)?; - let mut api = Owner::new(self.instance.clone().unwrap(), None); + let api = Owner::new(self.instance.clone().unwrap(), None); slate = api.finalize_tx(None, &slate)?; + // Create Slatepack message. + let _ = self.create_slatepack_message(slate.clone())?; + // Post transaction to blockchain. api.post_tx(None, &slate, dandelion)?; // Sync wallet info. self.sync(); - Ok(slate) } + /// Post transaction to blockchain. + pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> { + // Post transaction to blockchain. + let api = Owner::new(self.instance.clone().unwrap(), None); + api.post_tx(None, slate, dandelion)?; + // Sync wallet info. + self.sync(); + Ok(()) + } + /// Cancel transaction. pub fn cancel(&mut self, id: u32) { + let instance = self.instance.clone().unwrap(); + let _ = cancel_tx(instance, None, &None, Some(id), None); // Set cancelling status. { - let mut cancelling_w = self.cancel_txs.write().unwrap(); - cancelling_w.insert(id); + let mut w_data = self.data.write().unwrap(); + let mut data = w_data.clone().unwrap(); + let txs = data.txs.iter_mut().map(|tx| { + if tx.data.id == id { + tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived { + TxLogEntryType::TxReceivedCancelled + } else { + TxLogEntryType::TxSentCancelled + }; + } + tx.clone() + }).collect::>(); + data.txs = txs; + *w_data = Some(data); } - - // Launch tx cancelling at separate thread. - let wallet_cancel = self.clone(); - let instance = wallet_cancel.instance.clone().unwrap(); - thread::spawn(move || { - let _ = cancel_tx(instance, None, &None, Some(id), None); - // Refresh wallet info to update statuses. - wallet_cancel.sync(); - }); - } - - /// Check if transaction is cancelling. - pub fn is_cancelling(&self, id: &u32) -> bool { - let cancelling_r = self.cancel_txs.read().unwrap(); - cancelling_r.contains(id) + // Refresh wallet info to update statuses. + self.sync(); } /// Change wallet password. @@ -858,101 +870,121 @@ fn sync_wallet_data(wallet: &Wallet) { } }); + let config = wallet.get_config(); + // Retrieve wallet info. if let Some(instance) = &wallet.instance { - match retrieve_summary_info( + if let Ok(info) = retrieve_summary_info( instance.clone(), None, &Some(info_tx), true, - wallet.get_config().min_confirmations + config.min_confirmations ) { - Ok(info) => { - // Do not retrieve txs if wallet was closed. - if !wallet.is_open() { - return; - } + // Do not retrieve txs if wallet was closed. + if !wallet.is_open() { + return; + } - if wallet.info_sync_progress() == 100 { - // Retrieve accounts data. - let last_height = info.1.last_confirmed_height; - update_accounts(wallet, last_height, info.1.amount_currently_spendable); + if wallet.info_sync_progress() == 100 { + // Retrieve accounts data. + let last_height = info.1.last_confirmed_height; + update_accounts(wallet, last_height, info.1.amount_currently_spendable); - // Update txs sync progress at separate thread. - let wallet_txs = wallet.clone(); - let (txs_tx, txs_rx) = mpsc::channel::(); - thread::spawn(move || { - while let Ok(m) = txs_rx.recv() { - println!("SYNC TXS MESSAGE"); - match m { - StatusMessage::UpdatingOutputs(_) => {} - StatusMessage::UpdatingTransactions(_) => {} - StatusMessage::FullScanWarn(_) => {} - StatusMessage::Scanning(_, progress) => { - wallet_txs.txs_sync_progress.store(progress, Ordering::Relaxed); - } - StatusMessage::ScanningComplete(_) => { - wallet_txs.txs_sync_progress.store(100, Ordering::Relaxed); - } - StatusMessage::UpdateWarning(_) => {} + // Update txs sync progress at separate thread. + let wallet_txs = wallet.clone(); + let (txs_tx, txs_rx) = mpsc::channel::(); + thread::spawn(move || { + while let Ok(m) = txs_rx.recv() { + println!("SYNC TXS MESSAGE"); + match m { + StatusMessage::UpdatingOutputs(_) => {} + StatusMessage::UpdatingTransactions(_) => {} + StatusMessage::FullScanWarn(_) => {} + StatusMessage::Scanning(_, progress) => { + wallet_txs.txs_sync_progress.store(progress, Ordering::Relaxed); } + StatusMessage::ScanningComplete(_) => { + wallet_txs.txs_sync_progress.store(100, Ordering::Relaxed); + } + StatusMessage::UpdateWarning(_) => {} } - }); + } + }); - match retrieve_txs(instance.clone(), - None, - &Some(txs_tx), - true, - None, - None, - None) { - Ok(txs) => { - // Do not sync data if wallet was closed. - if !wallet.is_open() { - return; - } - // Save data if loading was completed. - if wallet.txs_sync_progress() == 100 { - // Reset attempts. - wallet.reset_sync_attempts(); + let txs_args = RetrieveTxQueryArgs { + exclude_cancelled: Some(true), + sort_field: Some(RetrieveTxQuerySortField::CreationTimestamp), + sort_order: Some(RetrieveTxQuerySortOrder::Desc), + ..Default::default() + }; + if let Ok(txs) = retrieve_txs(instance.clone(), + None, + &Some(txs_tx), + true, + None, + None, + Some(txs_args)) { + // Do not sync data if wallet was closed. + if !wallet.is_open() { + return; + } + // Save data if loading was completed. + if wallet.txs_sync_progress() == 100 { + // Reset attempts. + wallet.reset_sync_attempts(); - // Setup transactions. - let mut sort_txs = txs.1; - // Sort txs by creation date. - sort_txs.sort_by_key(|tx| -tx.creation_ts.timestamp()); - // Filter txs by current wallet account. - let mut txs = sort_txs.iter().map(|v| v.clone()).filter(|tx| { - match wallet.get_parent_key_id() { - Ok(key) => { - tx.parent_key_id == key - } - Err(_) => { - true - } - } - }).collect::>(); - // Update txs statuses. - for tx in &txs { - println!("{}", serde_json::to_string(tx).unwrap()); - if tx.tx_type == TxLogEntryType::TxSentCancelled - || tx.tx_type == TxLogEntryType::TxReceivedCancelled { - // Remove cancelling status. - let mut cancel_w = wallet.cancel_txs.write().unwrap(); - cancel_w.remove(&tx.id); - } + // Filter transactions for current account. + let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { + match wallet.get_parent_key_id() { + Ok(key) => { + tx.parent_key_id == key + } + Err(_) => { + true } - - // Update wallet data. - let mut w_data = wallet.data.write().unwrap(); - *w_data = Some(WalletData { info: info.1, txs }); - return; } + }).collect::>(); + + // Create wallet txs. + let mut txs = vec![]; + for tx in &filter_txs { + println!("{}", serde_json::to_string(tx).unwrap()); + let amount = if tx.amount_debited > tx.amount_credited { + tx.amount_debited - tx.amount_credited + } else { + tx.amount_credited - tx.amount_debited + }; + + // Setup transaction broadcasting flag based on slate state. + let posting = if (tx.tx_type == TxLogEntryType::TxSent || + tx.tx_type == TxLogEntryType::TxReceived) && + !tx.confirmed && tx.tx_slate_id.is_some() { + let sl_id = tx.tx_slate_id.unwrap().to_string(); + let state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice3, + _ => SlateState::Standard3 + }; + let slatepack_path = config.get_slatepack_path(sl_id, &state); + fs::read_to_string(slatepack_path).is_ok() + } else { + false + }; + + txs.push(WalletTransaction { + data: tx.clone(), + amount, + posting, + }) } - Err(e) => println!("error on retrieve_txs {}", e), + + // Update wallet data. + let mut w_data = wallet.data.write().unwrap(); + *w_data = Some(WalletData { info: info.1, txs }); + return; } } } - Err(e) => println!("error on retrieve_summary_info {}", e), } }