wallet: fix for already canceled invoice, transaction info modal, ability to finalize from list

This commit is contained in:
ardocrat 2024-04-24 01:42:56 +03:00
parent 92e1da511d
commit 01b5b21488
10 changed files with 591 additions and 157 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -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 }

View file

@ -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.

View file

@ -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} ツ. Отправьте это сообщение отправителю:'

View file

@ -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| {

View file

@ -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);
}

View file

@ -15,7 +15,7 @@
pub mod types;
mod txs;
pub use txs::WalletInfo;
pub use txs::WalletTransactions;
mod messages;
pub use messages::WalletMessages;

View file

@ -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<u32>,
/// Transaction [`Slate`] to use at [`Modal`].
tx_info_slate: Option<Slate>,
/// 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,23 +91,32 @@ 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 {
/// Draw transactions content.
fn txs_ui(&self, ui: &mut egui::Ui, wallet: &mut Wallet, data: &WalletData) {
let txs_size = data.txs.len();
/// Identifier for transaction information [`Modal`].
const TX_INFO_MODAL: &'static str = "tx_info_modal";
// Show transactions info.
View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| {
/// Height of transaction list item.
const TX_ITEM_HEIGHT: f32 = 76.0;
impl WalletTransactions {
/// Draw transactions content.
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| {
// Show non-zero awaiting confirmation amount.
if amount_awaiting_conf != 0 {
let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, true);
@ -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,36 +175,51 @@ 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 [`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);
});
}
_ => {}
}
}
}
}
/// Draw transaction item.
fn tx_item_ui(ui: &mut egui::Ui,
/// 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) {
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
if extra_padding {
@ -177,38 +230,61 @@ fn tx_item_ui(ui: &mut egui::Ui,
// Draw round background.
let bg_rect = rect.clone();
ui.painter().rect(bg_rect, rounding, Colors::BUTTON, View::ITEM_STROKE);
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, || {
//TODO: Show tx info
self.tx_info_finalize = false;
self.show_tx_info_modal(wallet, tx);
});
}
// 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 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 can_repost {
if tx.can_repost(data) {
View::item_button(ui,
Rounding::default(),
ARROW_CLOCKWISE,
@ -222,7 +298,7 @@ fn tx_item_ui(ui: &mut egui::Ui,
};
// Post tx after getting slate from slatepack file.
if let Some(sp) = wallet.read_slatepack(&slate) {
if let Ok(s) = wallet.parse_slatepack(sp) {
if let Ok(s) = wallet.parse_slatepack(&sp) {
let _ = wallet.post(&s, wallet.can_use_dandelion());
}
}
@ -231,11 +307,7 @@ fn tx_item_ui(ui: &mut egui::Ui,
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.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);
});
});
});
}
/// 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::<Vec<&WalletTransaction>>();
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;
}
}
}
}

View file

@ -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<u64>
}
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
}
}

View file

@ -479,9 +479,9 @@ impl Wallet {
}
/// Parse Slatepack message into [`Slate`].
pub fn parse_slatepack(&self, message: String) -> Result<Slate, Error> {
pub fn parse_slatepack(&self, message: &String) -> Result<Slate, Error> {
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<WalletTransaction> {
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<String, Error> {
pub fn pay(&self, message: &String) -> Result<String, Error> {
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<String, Error> {
pub fn receive(&self, message: &String) -> Result<String, Error> {
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<Slate, Error> {
pub fn finalize(&self, message: &String, dandelion: bool) -> Result<Slate, Error> {
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,12 +701,13 @@ 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.
{
// 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 {
@ -672,7 +718,6 @@ impl Wallet {
}).collect::<Vec<WalletTransaction>>();
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<WalletTransaction> = 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,
})
}