From 01b5b21488ebcdc2f936c34cb236e60dea4dfe7e Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 24 Apr 2024 01:42:56 +0300 Subject: [PATCH] wallet: fix for already canceled invoice, transaction info modal, ability to finalize from list --- Cargo.lock | 12 +- Cargo.toml | 10 +- locales/en.yml | 9 +- locales/ru.yml | 11 +- src/gui/views/wallets/wallet/content.rs | 6 +- src/gui/views/wallets/wallet/messages.rs | 28 +- src/gui/views/wallets/wallet/mod.rs | 2 +- src/gui/views/wallets/wallet/txs.rs | 518 +++++++++++++++++++---- src/wallet/types.rs | 21 +- src/wallet/wallet.rs | 131 ++++-- 10 files changed, 591 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be20ab4..61d95ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "grin_wallet_api" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "base64 0.12.3", "chrono", @@ -2833,7 +2833,7 @@ dependencies = [ [[package]] name = "grin_wallet_config" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "dirs 2.0.2", "grin_core", @@ -2848,7 +2848,7 @@ dependencies = [ [[package]] name = "grin_wallet_controller" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "chrono", "easy-jsonrpc-mw", @@ -2882,7 +2882,7 @@ dependencies = [ [[package]] name = "grin_wallet_impls" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "base64 0.12.3", "blake2-rfc", @@ -2921,7 +2921,7 @@ dependencies = [ [[package]] name = "grin_wallet_libwallet" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "age", "base64 0.9.3", @@ -2958,7 +2958,7 @@ dependencies = [ [[package]] name = "grin_wallet_util" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#75363a9a258bc1fb0cf60bfb4c88a8a653b122f2" +source = "git+https://github.com/yeastplume/grin-wallet?branch=prevent_double_pay#6e6b16a61c53825447f27ad49ba654c922cf9702" dependencies = [ "data-encoding", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index 959819b..73a0d0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,11 @@ grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } ## wallet -grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } -grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } -grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } -grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } -grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_impls = { git = "https://github.com/yeastplume/grin-wallet", branch = "prevent_double_pay" } +grin_wallet_api = { git = "https://github.com/yeastplume/grin-wallet", branch = "prevent_double_pay" } +grin_wallet_libwallet = { git = "https://github.com/yeastplume/grin-wallet", branch = "prevent_double_pay" } +grin_wallet_util = { git = "https://github.com/yeastplume/grin-wallet", branch = "prevent_double_pay" } +grin_wallet_controller = { git = "https://github.com/yeastplume/grin-wallet", branch = "prevent_double_pay" } ## ui egui = { version = "0.27.2", default-features = false } diff --git a/locales/en.yml b/locales/en.yml index 0734a36..7ad1288 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -10,6 +10,8 @@ show: Show delete: Delete clear: Clear create: Create +id: Identifier +kernel: Kernel wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization @@ -65,11 +67,11 @@ wallets: tx_finalizing: Finalizing tx_confirmed: Confirmed txs: Transactions + input_finalize_desc: 'Enter message to finalize the transaction:' messages: Messages transport: Transport input_slatepack_desc: 'Enter message to create response or finalize the transaction:' - send_slatepack_desc: 'Send message to receiver of funds to finalize the transaction:' - parse_slatepack_err: 'An error occurred during handling of the message, check input data:' + parse_slatepack_err: 'An error occurred during reading of the message, check input data:' 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} ツ' @@ -78,7 +80,8 @@ wallets: parse_s2_slatepack_desc: 'Finalize transaction to send %{amount} ツ' parse_s3_slatepack_desc: 'Post transaction to finalize sending of %{amount} ツ' resp_slatepack_err: 'An error occurred during creation of the response, check input data:' - resp_exists_err: 'Such transaction already exists.' + resp_exists_err: Such transaction already exists. + resp_canceled_err: Such transaction was already canceled. create_request_desc: 'Create request to send or receive the funds:' send_request_desc: 'You have created a request to send %{amount} ツ. Send this message to the receiver:' send_slatepack_err: An error occurred during creation of request to send funds, check input data. diff --git a/locales/ru.yml b/locales/ru.yml index 2691186..cc05e2a 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -10,6 +10,8 @@ show: Показать delete: Удалить clear: Очистить create: Создать +id: Идентификатор +kernel: Ядро wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения @@ -65,11 +67,11 @@ wallets: tx_finalizing: Завершение tx_confirmed: Подтверждено txs: Транзакции + input_finalize_desc: 'Введите полученное сообщение для завершения транзакции:' messages: Сообщения transport: Транспорт input_slatepack_desc: 'Введите полученное сообщение для создания ответа или завершения транзакции:' - send_slatepack_desc: 'Отправьте сообщение получателю средств для завершения транзакции:' - parse_slatepack_err: 'Во время обработки сообщения произошла ошибка, проверьте входные данные:' + parse_slatepack_err: 'Во время чтения сообщения произошла ошибка, проверьте входные данные:' pay_balance_error: 'Средств на аккаунте недостаточно для оплаты %{amount} ツ и комиссии сети.' parse_i1_slatepack_desc: 'Для оплаты %{amount} ツ отправьте это сообщение получателю:' parse_i2_slatepack_desc: 'Завершите транзакцию для получения %{amount} ツ' @@ -78,8 +80,9 @@ wallets: parse_s2_slatepack_desc: 'Завершите транзакцию для отправки %{amount} ツ' parse_s3_slatepack_desc: 'Опубликуйте транзакцию для завершения отправки %{amount} ツ' resp_slatepack_err: 'Во время создания ответа произошла ошибка, проверьте входные данные:' - resp_exists_err: 'Такая транзакция уже существует.' - create_request_desc: 'Cоздать запрос на отправку или получение средств:' + resp_exists_err: Такая транзакция уже существует. + resp_canceled_err: Такая транзакция уже была отменена. + create_request_desc: 'Создайте запрос на отправку или получение средств:' send_request_desc: 'Вы создали запрос на отправку %{amount} ツ. Отправьте это сообщение получателю:' send_slatepack_err: Во время создания запроса на отправку средств произошла ошибка, проверьте входные данные. invoice_desc: 'Вы создали запрос на получение %{amount} ツ. Отправьте это сообщение отправителю:' diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 984461d..a875d1e 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -23,7 +23,7 @@ use crate::gui::icons::{BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, FOLDER_USER, use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Root, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; -use crate::gui::views::wallets::{WalletInfo, WalletMessages, WalletTransport, WalletSettings}; +use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport, WalletSettings}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; @@ -52,7 +52,7 @@ impl Default for WalletContent { account_creating: false, account_label_edit: "".to_string(), account_creation_error: false, - current_tab: Box::new(WalletInfo::default()) + current_tab: Box::new(WalletTransactions::default()) } } } @@ -349,7 +349,7 @@ impl WalletContent { ui.columns(4, |columns| { columns[0].vertical_centered_justified(|ui| { View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, || { - self.current_tab = Box::new(WalletInfo::default()); + self.current_tab = Box::new(WalletTransactions::default()); }); }); columns[1].vertical_centered_justified(|ui| { diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs index 3000cd4..f67bf27 100644 --- a/src/gui/views/wallets/wallet/messages.rs +++ b/src/gui/views/wallets/wallet/messages.rs @@ -146,14 +146,14 @@ impl WalletMessages { ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - ui.add_space(4.0); + ui.add_space(3.0); // Show creation of request to send or receive funds. self.request_ui(ui, cb); ui.add_space(12.0); View::horizontal_line(ui, Colors::ITEM_STROKE); - ui.add_space(8.0); + ui.add_space(6.0); // Show Slatepack message input field. self.input_slatepack_ui(ui, wallet, cb); @@ -326,11 +326,10 @@ impl WalletMessages { } 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) { + if wallet.post(&slate, self.dandelion).is_ok() { self.message_edit.clear(); self.message_slate = None; } else { @@ -341,7 +340,8 @@ impl WalletMessages { ); } } else { - if let Ok(_) = wallet.finalize(message, self.dandelion) { + let r = wallet.finalize(&self.message_edit, self.dandelion); + if r.is_ok() { self.message_edit.clear(); self.message_slate = None; } else { @@ -397,7 +397,7 @@ impl WalletMessages { if self.message_edit.is_empty() { return; } - if let Ok(mut slate) = wallet.parse_slatepack(self.message_edit.clone()) { + if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) { println!("parse_message: {}", slate); // Try to setup empty amount from transaction by id. @@ -423,13 +423,23 @@ impl WalletMessages { match slate.state { SlateState::Standard1 | SlateState::Invoice1 => { let resp = if slate.state == SlateState::Standard1 { - wallet.receive(self.message_edit.clone()) + wallet.receive(&self.message_edit) } else { - wallet.pay(self.message_edit.clone()) + wallet.pay(&self.message_edit) }; if resp.is_ok() { self.response_edit = resp.unwrap(); } else { + match resp.err().unwrap() { + grin_wallet_libwallet::Error::TransactionWasCancelled {..} => { + // Set already canceled transaction error message. + self.message_error = Some( + MessageError::Response(t!("wallets.resp_canceled_err")) + ); + return; + } + _ => {} + } // Check if tx with same slate id already exists. let exists_tx = wallet.tx_by_slate(&slate).is_some(); if exists_tx { @@ -726,7 +736,7 @@ impl WalletMessages { // Button to cancel transaction. let cancel = format!("{} {}", PROHIBIT, t!("modal.cancel")); View::colored_text_button(ui, cancel, Colors::RED, Colors::BUTTON, || { - if let Ok(slate) = wallet.parse_slatepack(self.request_edit.clone()) { + if let Ok(slate) = wallet.parse_slatepack(&self.request_edit) { if let Some(tx) = wallet.tx_by_slate(&slate) { wallet.cancel(tx.data.id); } diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 6db373d..0c93bc9 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -15,7 +15,7 @@ pub mod types; mod txs; -pub use txs::WalletInfo; +pub use txs::WalletTransactions; mod messages; pub use messages::WalletMessages; diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs index d5bc8ae..2ff0dd5 100644 --- a/src/gui/views/wallets/wallet/txs.rs +++ b/src/gui/views/wallets/wallet/txs.rs @@ -15,24 +15,50 @@ use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; use grin_core::core::amount_to_hr_string; +use grin_util::ToHex; use grin_wallet_libwallet::{Slate, SlateState, TxLogEntryType}; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, ARROWS_CLOCKWISE, 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, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Root, View}; +use crate::gui::views::{Modal, Root, View}; +use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType}; +use crate::gui::views::wallets::wallet::types::{GRIN, SLATEPACK_MESSAGE_HINT, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; use crate::wallet::types::{WalletData, WalletTransaction}; use crate::wallet::Wallet; +/// Wallet transactions tab content. +pub struct WalletTransactions { + /// Transaction identifier to user at [`Modal`]. + tx_info_id: Option, + /// Transaction [`Slate`] to use at [`Modal`]. + tx_info_slate: Option, + /// Response Slatepack message input value at [`Modal`]. + tx_info_response_edit: String, + /// Finalization Slatepack message input value at [`Modal`]. + tx_info_finalize_edit: String, + /// Flag to check if error happened during transaction finalization at [`Modal`]. + tx_info_finalize_error: bool, + /// Flag to check if tx finalization requested at [`Modal`]. + tx_info_finalize: bool, +} -/// Wallet info tab content. -#[derive(Default)] -pub struct WalletInfo; +impl Default for WalletTransactions { + fn default() -> Self { + Self { + tx_info_id: None, + tx_info_slate: None, + tx_info_response_edit: "".to_string(), + tx_info_finalize_edit: "".to_string(), + tx_info_finalize_error: false, + tx_info_finalize: false, + } + } +} -impl WalletTab for WalletInfo { +impl WalletTab for WalletTransactions { fn get_type(&self) -> WalletTabType { WalletTabType::Txs } @@ -41,11 +67,14 @@ impl WalletTab for WalletInfo { ui: &mut egui::Ui, _: &mut eframe::Frame, wallet: &mut Wallet, - _: &dyn PlatformCallbacks) { + cb: &dyn PlatformCallbacks) { if WalletContent::sync_ui(ui, wallet) { return; } + // Show modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + // Show wallet transactions panel. egui::CentralPanel::default() .frame(egui::Frame { @@ -62,22 +91,31 @@ impl WalletTab for WalletInfo { .show_inside(ui, |ui| { ui.vertical_centered(|ui| { let data = wallet.get_data().unwrap(); - self.txs_ui(ui, wallet, &data); + self.txs_ui(ui, wallet, &data, cb); }); }); } } -impl WalletInfo { +/// Identifier for transaction information [`Modal`]. +const TX_INFO_MODAL: &'static str = "tx_info_modal"; + +/// Height of transaction list item. +const TX_ITEM_HEIGHT: f32 = 76.0; + +impl WalletTransactions { /// Draw transactions content. - fn txs_ui(&self, ui: &mut egui::Ui, wallet: &mut Wallet, data: &WalletData) { - let txs_size = data.txs.len(); + fn txs_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + data: &WalletData, + cb: &dyn PlatformCallbacks) { + let amount_awaiting_conf = data.info.amount_awaiting_confirmation; + let amount_awaiting_fin = data.info.amount_awaiting_finalization; + let amount_locked = data.info.amount_locked; // Show transactions info. View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { - let amount_awaiting_conf = data.info.amount_awaiting_confirmation; - let amount_awaiting_fin = data.info.amount_awaiting_finalization; - let amount_locked = data.info.amount_locked; // Show non-zero awaiting confirmation amount. if amount_awaiting_conf != 0 { @@ -117,7 +155,7 @@ impl WalletInfo { } // Show message when wallet txs are empty. - if txs_size == 0 { + if data.txs.is_empty() { View::center_content(ui, 96.0, |ui| { let empty_text = t!( "wallets.txs_empty", @@ -137,105 +175,139 @@ impl WalletInfo { .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .id_source(Id::from("txs_content").with(wallet.get_config().id)) .auto_shrink([false; 2]) - .show_rows(ui, TX_ITEM_HEIGHT, txs_size, |ui, row_range| { + .show_rows(ui, TX_ITEM_HEIGHT, data.txs.len(), |ui, row_range| { ui.add_space(3.0); View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { - let amount_awaiting_conf = data.info.amount_awaiting_confirmation; - let amount_awaiting_fin = data.info.amount_awaiting_finalization; - let amount_locked = data.info.amount_locked; let extra_padding = amount_awaiting_conf != 0 || amount_awaiting_fin != 0 || amount_locked != 0; for index in row_range { - let tx = data.txs.get(index).unwrap(); - // Setup item rounding. - let item_rounding = View::item_rounding(index, txs_size, false); // Show transaction item. - tx_item_ui(ui, tx, item_rounding, extra_padding, &data, wallet); + let tx = data.txs.get(index).unwrap(); + let rounding = View::item_rounding(index, data.txs.len(), false); + self.tx_item_ui(ui, tx, rounding, extra_padding, true, &data, wallet, cb); } }); }); } -} -/// Height of transaction list item. -const TX_ITEM_HEIGHT: f32 = 76.0; - -/// Draw transaction item. -fn tx_item_ui(ui: &mut egui::Ui, - tx: &WalletTransaction, - mut rounding: Rounding, - extra_padding: bool, - data: &WalletData, - wallet: &mut Wallet) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - if extra_padding { - rect.min += egui::emath::vec2(6.0, 0.0); - rect.max -= egui::emath::vec2(6.0, 0.0); + /// Draw [`Modal`] content for this ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + TX_INFO_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.tx_info_modal_ui(ui, wallet, modal, cb); + }); + } + _ => {} + } + } + } } - rect.set_height(TX_ITEM_HEIGHT); - // Draw round background. - let bg_rect = rect.clone(); - ui.painter().rect(bg_rect, rounding, Colors::BUTTON, View::ITEM_STROKE); + /// Draw transaction item. + fn tx_item_ui(&mut self, + ui: &mut egui::Ui, + tx: &WalletTransaction, + mut rounding: Rounding, + extra_padding: bool, + can_show_info: bool, + data: &WalletData, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + if extra_padding { + rect.min += egui::emath::vec2(6.0, 0.0); + rect.max -= egui::emath::vec2(6.0, 0.0); + } + rect.set_height(TX_ITEM_HEIGHT); + + // Draw round background. + let bg_rect = rect.clone(); + let color = if can_show_info { + Colors::BUTTON + } else { + Colors::FILL + }; + ui.painter().rect(bg_rect, rounding, color, View::ITEM_STROKE); - ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - if extra_padding { - ui.add_space(-6.0); + // Draw button to show transaction info. + if can_show_info { + rounding.nw = 0.0; + rounding.sw = 0.0; + View::item_button(ui, rounding, FILE_TEXT, None, || { + self.tx_info_finalize = false; + self.show_tx_info_modal(wallet, tx); + }); } - // Draw button to show transaction info. - rounding.nw = 0.0; - rounding.sw = 0.0; - View::item_button(ui, rounding, FILE_TEXT, None, || { - //TODO: Show tx info - }); - - // Setup flag to repost unconfirmed posting transaction after min confirmation time. - let last_height = data.info.last_confirmed_height; - let min_conf = data.info.minimum_confirmations; - let can_repost = tx.posting && tx.repost_height.is_some() && - last_height - tx.repost_height.unwrap() > min_conf; - - // Draw cancel button for txs to repost or also non-cancelled, non-posting. - if can_repost || (!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), || { + // Draw cancel button for tx that can be reposted and canceled. + if tx.can_repost(data) || tx.can_cancel() { + let cancel_rounding = if can_show_info { + Rounding::default() + } else { + rounding.nw = 0.0; + rounding.sw = 0.0; + rounding + }; + View::item_button(ui, cancel_rounding, PROHIBIT, Some(Colors::RED), || { wallet.cancel(tx.data.id); }); } - // Draw button to repost transaction. - if can_repost { - View::item_button(ui, - Rounding::default(), - ARROW_CLOCKWISE, - Some(Colors::GREEN), || { - // Create slate to check existing file. - let mut slate = Slate::blank(1, false); - slate.id = tx.data.tx_slate_id.unwrap(); - slate.state = match tx.data.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice3, - _ => SlateState::Standard3 - }; - // Post tx after getting slate from slatepack file. - if let Some(sp) = wallet.read_slatepack(&slate) { - if let Ok(s) = wallet.parse_slatepack(sp) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } + // Draw finalization button for tx that can be finalized. + if tx.can_finalize { + let (icon, color) = if !can_show_info && self.tx_info_finalize { + (FILE_TEXT, None) + } else { + (CHECK, Some(Colors::GREEN)) + }; + View::item_button(ui, Rounding::default(), icon, color, || { + if !can_show_info && self.tx_info_finalize { + self.tx_info_finalize = false; + return; + } + self.tx_info_finalize = true; + // Show transaction information modal. + if can_show_info { + self.show_tx_info_modal(wallet, tx); + cb.show_keyboard(); } }); } + // Draw button to repost transaction. + if tx.can_repost(data) { + View::item_button(ui, + Rounding::default(), + ARROW_CLOCKWISE, + Some(Colors::GREEN), || { + // Create slate to check existing file. + let mut slate = Slate::blank(1, false); + slate.id = tx.data.tx_slate_id.unwrap(); + slate.state = match tx.data.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice3, + _ => SlateState::Standard3 + }; + // Post tx after getting slate from slatepack file. + if let Some(sp) = wallet.read_slatepack(&slate) { + if let Ok(s) = wallet.parse_slatepack(&sp) { + let _ = wallet.post(&s, wallet.can_use_dandelion()); + } + } + }); + } + let layout_size = ui.available_size(); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - if extra_padding { - ui.add_space(12.0); - } else { - ui.add_space(6.0); - } + ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); @@ -294,13 +366,14 @@ fn tx_item_ui(ui: &mut egui::Ui, format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) }, TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { + let min_conf = data.info.minimum_confirmations; if data.info.last_confirmed_height - tx_height > min_conf { - let (icon, text) = if tx.data.tx_type == TxLogEntryType::TxSent { + let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) } else { (ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) }; - format!("{} {}", icon, text) + format!("{} {}", i, t) } else { let h = data.info.last_confirmed_height; let left_conf = h - tx_height; @@ -343,8 +416,269 @@ fn tx_item_ui(ui: &mut egui::Ui, 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)); + ui.add_space(3.0); }); }); }); - }); -} \ No newline at end of file + } + + /// Show transaction information [`Modal`]. + fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction) { + self.tx_info_response_edit = "".to_string(); + self.tx_info_finalize_edit = "".to_string(); + self.tx_info_finalize_error = false; + self.tx_info_id = Some(tx.data.id); + // Setup slate and message from transaction. + if let Some((slate, message)) = wallet.read_slate_by_tx(tx) { + self.tx_info_response_edit = message; + self.tx_info_slate = Some(slate); + } + // Show transaction information modal. + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .show(); + } + + /// Draw transaction info [`Modal`] content. + fn tx_info_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Check values and setup transaction data. + let wallet_data = wallet.get_data(); + if wallet_data.is_none() { + modal.close(); + return; + } + let data = wallet_data.unwrap(); + let tx_id = self.tx_info_id.unwrap(); + let txs = data.txs.iter() + .filter(|tx| tx.data.id == tx_id) + .collect::>(); + if txs.is_empty() { + modal.close(); + return; + } + let tx = txs.get(0).unwrap(); + + ui.add_space(6.0); + + // Show transaction amount status and time. + let rounding = View::item_rounding(0, 2, false); + self.tx_item_ui(ui, tx, rounding, false, false, &data, wallet, cb); + + // Show transaction ID info. + if let Some(id) = tx.data.tx_slate_id { + let label = format!("{} {}", HASH_STRAIGHT, t!("id")); + Self::tx_info_modal_item_ui(ui, id.to_string(), label, true, cb); + } + // Show transaction kernel info. + if let Some(kernel) = tx.data.kernel_excess { + let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); + Self::tx_info_modal_item_ui(ui, kernel.0.to_hex(), label, true, cb); + } + + // Show transaction Slatepack message response or finalization input. + if !tx.posting && !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxReceived) { + self.tx_info_modal_slate_ui(ui, tx, wallet, modal, cb); + } + ui.add_space(8.0); + + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::WHITE, || { + self.tx_info_id = None; + self.tx_info_finalize = false; + cb.hide_keyboard(); + modal.close(); + }); + }); + ui.add_space(6.0); + } + + /// Draw transaction information [`Modal`] item content. + fn tx_info_modal_item_ui(ui: &mut egui::Ui, + value: String, + label: String, + copy: bool, + cb: &dyn PlatformCallbacks) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(50.0); + + // Draw round background. + let bg_rect = rect.clone(); + let mut rounding = View::item_rounding(1, 3, false); + + ui.painter().rect(bg_rect, rounding, Colors::FILL, View::ITEM_STROKE); + + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to copy transaction info value. + if copy { + rounding.nw = 0.0; + rounding.sw = 0.0; + View::item_button(ui, rounding, COPY, None, || { + cb.copy_string_to_buffer(value.clone()); + }); + } + + // Draw value information. + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + View::ellipsize_text(ui, value, 15.0, Colors::TITLE); + ui.label(RichText::new(label).size(15.0).color(Colors::GRAY)); + ui.add_space(3.0); + }); + }); + }); + } + + /// Draw Slate content to show response or generate payment proof. + fn tx_info_modal_slate_ui(&mut self, + ui: &mut egui::Ui, + tx: &WalletTransaction, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + if self.tx_info_slate.is_none() { + return; + } + let slate = self.tx_info_slate.clone().unwrap(); + let amount = amount_to_hr_string(tx.amount, true); + + // Draw Slatepack message input or output description text. + ui.add_space(6.0); + ui.vertical_centered(|ui| { + if self.tx_info_finalize { + let desc_text = if self.tx_info_finalize_error { + t!("wallets.finalize_slatepack_err") + } else { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_s2_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_i2_slatepack_desc", "amount" => amount) + } + }; + let desc_color = if self.tx_info_finalize_error { + Colors::RED + } else { + Colors::INACTIVE_TEXT + }; + ui.label(RichText::new(desc_text).size(16.0).color(desc_color)); + } else { + let desc_text = if tx.can_finalize { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.send_request_desc", "amount" => amount) + } else { + t!("wallets.invoice_desc", "amount" => amount) + } + } else { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_i1_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_s1_slatepack_desc", "amount" => amount) + } + }; + ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT)); + } + }); + ui.add_space(4.0); + + ui.vertical_centered(|ui| { + let message_edit = if self.tx_info_finalize { + &mut self.tx_info_finalize_edit + } else { + &mut self.tx_info_response_edit + }; + let message_before = message_edit.clone(); + + // Draw Slatepack message text input or output. + let input_id = Id::from("tx_info_slatepack_message").with(slate.id).with(tx.data.id); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(3.0); + ScrollArea::vertical() + .max_height(128.0) + .id_source(input_id) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + egui::TextEdit::multiline(message_edit) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(self.tx_info_finalize) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(8.0); + + if self.tx_info_finalize { + // Draw paste button. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::BUTTON, || { + self.tx_info_finalize_edit = cb.get_string_from_buffer(); + }); + + // Callback on finalization message input change. + if message_before != self.tx_info_finalize_edit { + self.on_finalization_input_change(tx, wallet, cb); + } + } else { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::BUTTON, || { + cb.copy_string_to_buffer(self.tx_info_response_edit.clone()); + self.tx_info_finalize_edit = "".to_string(); + if tx.can_finalize { + self.tx_info_finalize = true; + } else { + modal.close(); + } + }); + } + }); + } + + /// Parse Slatepack message on transaction finalization input change. + fn on_finalization_input_change(&mut self, + tx: &WalletTransaction, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + let message = &self.tx_info_finalize_edit; + if message.is_empty() { + self.tx_info_finalize_error = false; + } else { + if let Ok(slate) = wallet.parse_slatepack(message) { + let send = slate.state == SlateState::Standard2 && + tx.data.tx_type == TxLogEntryType::TxSent; + let receive = slate.state == SlateState::Invoice2 && + tx.data.tx_type == TxLogEntryType::TxReceived; + if Some(slate.id) == tx.data.tx_slate_id && (send || receive) { + match wallet.finalize(message, wallet.can_use_dandelion()) { + Ok(_) => { + self.tx_info_finalize = false; + self.tx_info_finalize_edit = "".to_string(); + cb.hide_keyboard(); + } + Err(_) => { + self.tx_info_finalize_error = true; + } + } + } else { + self.tx_info_finalize_error = true; + } + } else { + self.tx_info_finalize_error = true; + } + } + } +} + diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 6bc01bf..e7b1b66 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use grin_keychain::ExtKeychain; use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; -use grin_wallet_libwallet::{TxLogEntry, WalletInfo, WalletInst}; +use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; /// Mnemonic phrase setup mode. #[derive(PartialEq, Clone)] @@ -149,6 +149,25 @@ pub struct WalletTransaction { pub amount: u64, /// Flag to check if transaction is posting after finalization. pub posting: bool, + /// Flag to check if transaction can be finalized based on Slatepack message state. + pub can_finalize: bool, /// Last wallet block height of transaction reposting. pub repost_height: Option +} + +impl WalletTransaction { + /// Check if transaction can be cancelled. + pub fn can_cancel(&self) -> bool { + !self.posting && !self.data.confirmed && + self.data.tx_type != TxLogEntryType::TxReceivedCancelled + && self.data.tx_type != TxLogEntryType::TxSentCancelled + } + + /// Check if transaction can be reposted. + pub fn can_repost(&self, data: &WalletData) -> bool { + let last_height = data.info.last_confirmed_height; + let min_conf = data.info.minimum_confirmations; + self.posting && self.repost_height.is_some() && + last_height - self.repost_height.unwrap() > min_conf + } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index e6ec17f..112df1c 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -479,9 +479,9 @@ impl Wallet { } /// Parse Slatepack message into [`Slate`]. - pub fn parse_slatepack(&self, message: String) -> Result { + pub fn parse_slatepack(&self, message: &String) -> Result { let api = Owner::new(self.instance.clone().unwrap(), None); - api.slate_from_slatepack_message(None, message, vec![]) + api.slate_from_slatepack_message(None, message.clone(), vec![]) } /// Create Slatepack message from provided slate. @@ -493,7 +493,7 @@ impl Wallet { Ok(()) })?; - // Save slatepack. + // Write Slatepack message to file. let slatepack_dir = self.get_config().get_slatepack_path(&slate); let mut output = File::create(slatepack_dir)?; output.write_all(message.as_bytes())?; @@ -510,7 +510,51 @@ impl Wallet { } } - /// Get transaction by slate id. + /// Get last stored [`Slate`] for transaction. + pub fn read_slate_by_tx(&self, tx: &WalletTransaction) -> Option<(Slate, String)> { + let mut slate = None; + if let Some(slate_id) = tx.data.tx_slate_id { + // Get slate state based on tx state and status. + let state = if tx.posting { + if tx.data.tx_type == TxLogEntryType::TxSent { + Some(SlateState::Standard3) + } else { + Some(SlateState::Invoice3) + } + } else if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || + tx.data.tx_type == TxLogEntryType::TxReceived) { + if tx.can_finalize { + if tx.data.tx_type == TxLogEntryType::TxSent { + Some(SlateState::Standard1) + } else { + Some(SlateState::Invoice1) + } + } else { + if tx.data.tx_type == TxLogEntryType::TxReceived { + Some(SlateState::Standard2) + } else { + Some(SlateState::Invoice2) + } + } + } else { + None + }; + // Get slate from state by reading Slatepack message file. + if let Some(st) = state { + let mut s = Slate::blank(0, false); + s.id = slate_id; + s.state = st; + if let Some(m) = self.read_slatepack(&s) { + if let Ok(s) = self.parse_slatepack(&m) { + slate = Some((s, m)); + } + } + } + } + slate + } + + /// Get transaction for [`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| { @@ -571,7 +615,7 @@ impl Wallet { } /// Handle message from the invoice issuer to send founds, return response for funds receiver. - pub fn pay(&self, message: String) -> Result { + pub fn pay(&self, message: &String) -> Result { let slate = self.parse_slatepack(message)?; let config = self.get_config(); let args = InitTxArgs { @@ -595,7 +639,7 @@ impl Wallet { } /// Handle message to receive funds, return response to sender. - pub fn receive(&self, message: String) -> Result { + pub fn receive(&self, message: &String) -> Result { let mut slate = self.parse_slatepack(message)?; let api = Owner::new(self.instance.clone().unwrap(), None); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { @@ -612,7 +656,7 @@ impl Wallet { } /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. - pub fn finalize(&self, message: String, dandelion: bool) -> Result { + pub fn finalize(&self, message: &String, dandelion: bool) -> Result { let mut slate = self.parse_slatepack(message)?; let api = Owner::new(self.instance.clone().unwrap(), None); slate = api.finalize_tx(None, &slate)?; @@ -629,7 +673,7 @@ impl Wallet { // Post transaction to blockchain. let api = Owner::new(self.instance.clone().unwrap(), None); api.post_tx(None, slate, dandelion)?; - // Setup transaction repost height and posting flag. + // Setup transaction repost height, posting flag and ability to finalize. let mut slate = slate.clone(); if slate.state == SlateState::Invoice2 { slate.state = SlateState::Invoice3 @@ -643,6 +687,7 @@ impl Wallet { if t.data.id == tx.data.id { t.repost_height = Some(data.info.last_confirmed_height); t.posting = true; + t.can_finalize = false; } } *w_data = Some(data); @@ -656,23 +701,23 @@ impl Wallet { 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 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); - } + // Setup cancelling status, posting flag, and ability to finalize. + 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.posting = false; + tx.can_finalize = false; + 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); // Refresh wallet info to update statuses. self.sync(); } @@ -995,7 +1040,6 @@ fn sync_wallet_data(wallet: &Wallet) { // Create wallet txs. let mut new_txs: Vec = vec![]; for tx in &filter_txs { - println!("{}", serde_json::to_string(tx).unwrap()); // Setup transaction amount. let amount = if tx.amount_debited > tx.amount_credited { tx.amount_debited - tx.amount_credited @@ -1003,15 +1047,20 @@ fn sync_wallet_data(wallet: &Wallet) { tx.amount_credited - tx.amount_debited }; - // Setup transaction posting 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 unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && + !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || + tx.tx_type == TxLogEntryType::TxReceived); + + // Setup transaction posting status based on slate state. + let posting = if unconfirmed_sent_or_received { + println!("{}", serde_json::to_string(tx).unwrap()); + // Create slate to check existing file. - let mut slate = Slate::blank(1, false); + let is_invoice = tx.tx_type == TxLogEntryType::TxReceived; + let mut slate = Slate::blank(0, is_invoice); slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice3, + slate.state = match is_invoice { + true => SlateState::Invoice3, _ => SlateState::Standard3 }; @@ -1033,6 +1082,20 @@ fn sync_wallet_data(wallet: &Wallet) { false }; + // Setup flag for ability to finalize transaction. + let can_finalize = if !posting && unconfirmed_sent_or_received { + // Create slate to check existing file. + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1 + }; + wallet.read_slatepack(&slate).is_some() + } else { + false + }; + // Setup reposting height. let mut repost_height = None; if posting { @@ -1046,10 +1109,12 @@ fn sync_wallet_data(wallet: &Wallet) { } } + // Add transaction to list. new_txs.push(WalletTransaction { data: tx.clone(), amount, posting, + can_finalize, repost_height, }) }