txs: async tasks for wallet
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run

This commit is contained in:
ardocrat 2025-06-19 09:18:20 +03:00
parent b540fcbf19
commit cd0e3485c5
19 changed files with 986 additions and 983 deletions

View file

@ -86,6 +86,7 @@ wallets:
tx_canceled: Abgebrochen
tx_cancelling: Abbrechen
tx_finalizing: Finalisierung
tx_posting: Buchungsvorgang
tx_confirmed: Bestätigt
txs: Transaktionen
tx: Transaktion
@ -141,7 +142,7 @@ transport:
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
tor_sending: 'Sende %{amount} ツ über Tor'
tor_sending: Sende über Tor
tor_settings: Tor Einstellungen
bridges: Brücken
bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert.

View file

@ -86,6 +86,7 @@ wallets:
tx_canceled: Canceled
tx_cancelling: Cancelling
tx_finalizing: Finalizing
tx_posting: Posting
tx_confirmed: Confirmed
txs: Transactions
tx: Transaction
@ -141,7 +142,7 @@ transport:
incorrect_addr_err: 'Entered address is incorrect:'
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
tor_sending: 'Sending %{amount} ツ over Tor'
tor_sending: Sending over Tor
tor_settings: Tor Settings
bridges: Bridges
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.

View file

@ -86,6 +86,7 @@ wallets:
tx_canceled: Annulé
tx_cancelling: Annulation
tx_finalizing: Finalisation
tx_posting: Publication
tx_confirmed: Confirmé
txs: Transactions
tx: Transaction
@ -141,7 +142,7 @@ transport:
incorrect_addr_err: 'Adresse entrée incorrecte:'
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
tor_sending: 'Envoi de %{amount} ツ via Tor'
tor_sending: Envoi via Tor
tor_settings: Paramètres Tor
bridges: Passerelles
bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas.

View file

@ -86,6 +86,7 @@ wallets:
tx_canceled: Отменено
tx_cancelling: Отмена
tx_finalizing: Завершение
tx_posting: Публикация
tx_confirmed: Подтверждено
txs: Транзакции
tx: Транзакция
@ -141,7 +142,7 @@ transport:
incorrect_addr_err: 'Введённый адрес неверен:'
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
tor_sending: 'Отправка %{amount} ツ через Tor'
tor_sending: Отправка через Tor
tor_settings: Настройки Tor
bridges: Мосты
bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает.

View file

@ -86,6 +86,7 @@ wallets:
tx_canceled: Iptal edildi
tx_cancelling: Iptal ediliyor
tx_finalizing: Islem tamamlaniyor
tx_posting: Islem kaydetme
tx_confirmed: Onaylandi
txs: Islemler
tx: Islem
@ -141,7 +142,7 @@ transport:
incorrect_addr_err: 'Girilen adres hatali:'
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
tor_sending: 'Tor adrese %{amount} ツ gonderiliyor.'
tor_sending: Tor adrese gonderiliyor
tor_settings: Tor Ayarlar
bridges: Bridges
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.

View file

@ -81,11 +81,13 @@ wallets:
tx_sent: 已发送
tx_received: 已接收
tx_sending: 发送中
tx_sending_tor: 通过 Tor 发送
tx_receiving: 接收中
tx_confirming: 等待确认
tx_canceled: 已取消
tx_cancelling: 取消
tx_finalizing: 完成
tx_posting: 过账交易
tx_confirmed: 已确认
txs: 所有交易
tx: 交易
@ -141,7 +143,6 @@ transport:
incorrect_addr_err: '输入的地址不正确:'
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
tor_sending: '通过 Tor 发送%{amount} ツ'
tor_settings: Tor 设置
bridges: 桥梁
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.

View file

@ -468,9 +468,12 @@ impl View {
Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui);
}
/// Size of big loading spinner.
pub const SMALL_SPINNER_SIZE: f32 = 32.0;
/// Draw small gold loading spinner.
pub fn small_loading_spinner(ui: &mut egui::Ui) {
Spinner::new().size(38.0).color(Colors::gold()).ui(ui);
Spinner::new().size(30.0).color(Colors::gold()).ui(ui);
}
/// Draw the button that looks like checkbox with callback on check.

View file

@ -26,7 +26,7 @@ use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletConten
use crate::gui::views::wallets::WalletContent;
use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::types::{ConnectionMethod, WalletTask};
use crate::wallet::{Wallet, WalletList};
use crate::AppConfig;
@ -619,7 +619,7 @@ impl WalletsContent {
fn select_wallet(&mut self, wallet: &Wallet, data: Option<String>, cb: &dyn PlatformCallbacks) {
self.wallet_content.account_content.close_qr_scan(cb);
if let Some(data) = data {
wallet.open_message(data);
wallet.task(WalletTask::OpenMessage(data));
}
self.wallets.select(Some(wallet.get_config().id));
}

View file

@ -24,6 +24,7 @@ use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
use crate::gui::Colors;
use crate::wallet::{Wallet, WalletConfig};
use crate::wallet::types::WalletTask;
/// Wallet account panel content.
pub struct AccountContent {
@ -288,7 +289,7 @@ impl AccountContent {
//TODO: send with address
}
QrScanResult::Slatepack(m) => {
wallet.open_message(m.to_string());
wallet.task(WalletTask::OpenMessage(m));
}
_ => {
self.qr_scan_result = Some(result);

View file

@ -16,7 +16,7 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use grin_chain::SyncStatus;
use std::time::Duration;
use grin_wallet_libwallet::Error;
use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, ModalPosition};
@ -30,7 +30,7 @@ use crate::gui::views::wallets::WalletTransactions;
use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::types::{ConnectionMethod, WalletTransaction};
use crate::wallet::types::{ConnectionMethod, WalletTask};
use crate::wallet::{ExternalConnection, Wallet};
use crate::AppConfig;
@ -105,11 +105,6 @@ impl WalletContentContainer for WalletContent {
show_account = false;
}
// Consume inserted message.
if let Some(res) = wallet.consume_message_result() {
self.on_transaction(res);
}
// Show wallet tabs.
egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame {
@ -314,13 +309,6 @@ impl WalletContent {
}
}
/// Callback on incoming transaction for user to take action.
fn on_transaction(&mut self, tx_result: Result<WalletTransaction, Error>) {
if let Ok(tx) = tx_result {
self.current_tab = Box::new(WalletTransactions::new(Some(tx)));
}
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.account_content.can_back() || self.transport_content.can_back()
@ -370,41 +358,49 @@ impl WalletContent {
});
});
columns[1].vertical_centered_justified(|ui| {
let active = if has_wallet_data {
Some(false)
if wallet.invoice_creating() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
None
};
View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| {
self.invoice_request_content = Some(InvoiceRequestContent::default());
Modal::new(INVOICE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.receive"))
.show();
});
let active = if has_wallet_data {
Some(false)
} else {
None
};
View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| {
self.invoice_request_content = Some(InvoiceRequestContent::default());
Modal::new(INVOICE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.receive"))
.show();
});
}
});
columns[2].vertical_centered_justified(|ui| {
if wallet.message_opening() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
let mut message = "".to_string();
self.file_pick_tab_button.ui(ui, cb, |m| {
message = m;
wallet.task(WalletTask::OpenMessage(m));
});
if !message.is_empty() {
wallet.open_message(message);
}
}
});
if can_send {
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FILE_ARROW_UP, Some(Colors::red()), Some(false), |_| {
self.send_request_content = Some(SendRequestContent::new(None));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
});
if wallet.send_creating() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
let (icon, color) = (FILE_ARROW_UP, Some(Colors::red()));
View::tab_button(ui, icon, color, Some(false), |_| {
self.send_request_content = Some(SendRequestContent::new(None));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
});
}
});
}
let settings_index = if tabs_amount == 5 { 4 } else { 3 };

View file

@ -14,42 +14,23 @@
use egui::{Id, RichText};
use grin_core::core::amount_from_hr_string;
use grin_wallet_libwallet::Error;
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::types::WalletTransaction;
use crate::wallet::types::WalletTask;
use crate::wallet::Wallet;
/// Invoice request creation content.
pub struct InvoiceRequestContent {
/// Amount to receive.
amount_edit: String,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionContent>,
}
impl Default for InvoiceRequestContent {
fn default() -> Self {
Self {
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
}
@ -61,42 +42,20 @@ impl InvoiceRequestContent {
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Setup callback on continue.
let on_continue = |m: &mut InvoiceRequestContent| {
if m.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let result = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
let res = wallet.issue_invoice(a);
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
m.request_error = Some(t!("wallets.invoice_slatepack_err"));
m.amount_edit = "".to_string();
wallet.task(WalletTask::Receive(a));
Modal::close();
}
};
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, modal);
return;
}
// Draw amount input content.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_amount_receive"))
@ -117,7 +76,6 @@ impl InvoiceRequestContent {
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
self.request_error = None;
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
@ -146,16 +104,6 @@ impl InvoiceRequestContent {
}
}
// Show request creation error.
if let Some(err) = &self.request_error {
ui.add_space(12.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
}
ui.add_space(12.0);
// Setup spacing between buttons.
@ -165,7 +113,6 @@ impl InvoiceRequestContent {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.request_error = None;
Modal::close();
});
});
@ -178,36 +125,4 @@ impl InvoiceRequestContent {
});
ui.add_space(6.0);
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(34.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(50.0);
// Check if there is request result error.
if self.request_error.is_some() {
modal.enable_closing();
self.request_loading = false;
return;
}
// Update data on request result.
let r_request = self.request_result.read();
if r_request.is_some() {
modal.enable_closing();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content = Some(WalletTransactionContent::new(tx));
}
Err(_) => {
self.request_error = Some(t!("wallets.invoice_slatepack_err"));
self.request_loading = false;
}
}
}
}
}

View file

@ -12,19 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::{Error, SlatepackAddress};
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::gui::views::types::ModalPosition;
use crate::wallet::types::WalletTransaction;
use crate::gui::Colors;
use crate::wallet::types::WalletTask;
use crate::wallet::Wallet;
/// Content to create a request to send funds.
@ -36,18 +31,8 @@ pub struct SendRequestContent {
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionContent>,
}
impl SendRequestContent {
@ -57,11 +42,7 @@ impl SendRequestContent {
amount_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
address_scan_content: None,
result_tx_content: None,
}
}
@ -71,68 +52,6 @@ impl SendRequestContent {
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Setup callback on continue.
let on_continue = |m: &mut SendRequestContent| {
if m.amount_edit.is_empty() {
return;
}
// Check address to send over Tor if enabled.
let addr_str = m.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
Modal::change_position(ModalPosition::Center);
modal.disable_closing();
let mut wallet = wallet.clone();
let res = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let result = wallet.send_tor(a, &addr).await;
let mut w_res = res.write();
*w_res = Some(result);
});
});
}
} else if !addr_str.is_empty() {
m.address_error = true;
} else if let Ok(amount) = amount_from_hr_string(m.amount_edit.as_str()) {
Modal::change_position(ModalPosition::Center);
modal.disable_closing();
let wallet = wallet.clone();
let result = m.request_result.clone();
// Send request at another thread.
m.request_loading = true;
thread::spawn(move || {
let res = wallet.send(amount, None);
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
m.request_error = Some(t!("wallets.send_slatepack_err"));
}
};
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, modal);
return;
}
ui.add_space(6.0);
// Draw QR code scanner content if requested.
@ -158,6 +77,7 @@ impl SendRequestContent {
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.close();
});
});
@ -267,17 +187,7 @@ impl SendRequestContent {
// Continue on Enter press.
if address_edit.enter_pressed {
on_continue(self);
}
// Show request creation error.
if let Some(err) = &self.request_error {
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
self.on_continue(wallet);
}
// Setup spacing between buttons.
@ -292,83 +202,38 @@ impl SendRequestContent {
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_continue(self);
self.on_continue(wallet);
});
});
});
ui.add_space(6.0);
}
/// Callback when Continue button was pressed.
fn on_continue(&mut self, wallet: &Wallet) {
if self.amount_edit.is_empty() {
return;
}
// Check address to send over Tor if enabled.
let addr_str = self.address_edit.as_str();
if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, Some(r)));
Modal::close();
}
} else if !addr_str.is_empty() {
self.address_error = true;
} else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
wallet.task(WalletTask::Send(a, None));
Modal::close();
}
}
/// Close modal and clear data.
fn close(&mut self) {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
let mut w_res = self.request_result.write();
*w_res = None;
self.result_tx_content = None;
self.address_scan_content = None;
Modal::close();
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(40.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(40.0);
if !self.address_edit.is_empty() {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::inactive_text()));
});
ui.add_space(12.0);
}
// Update data on request result.
let has_res = {
let r_request = self.request_result.read();
r_request.is_some()
};
if has_res {
self.request_loading = false;
modal.enable_closing();
let r_request = self.request_result.read();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content = Some(WalletTransactionContent::new(tx));
}
Err(err) => {
let m = match err {
Error::NotEnoughFunds { .. } => {
t!("wallets.pay_balance_error", "amount" => self.amount_edit)
}
_ => {
if !self.address_edit.is_empty() {
t!("transport.tor_send_error")
} else {
t!("wallets.send_slatepack_err")
}
}
};
self.request_error = Some(m);
}
}
}
// Check if there is request result error.
if self.request_error.is_some() {
Modal::change_position(ModalPosition::CenterTop);
modal.enable_closing();
let mut w_request = self.request_result.write();
*w_request = None;
self.request_loading = false;
}
}
}

View file

@ -130,7 +130,7 @@ impl WalletTransportContent {
if !Tor::is_service_running(service_id) {
let r = CornerRadius::default();
View::item_button(ui, r, POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
if let Ok(key) = wallet.get_secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}

View file

@ -80,7 +80,7 @@ impl WalletTransportSettingsContent {
// Restart running service or rebuild client.
let service_id = &wallet.identifier();
if Tor::is_service_running(service_id) {
if let Ok(key) = wallet.secret_key() {
if let Ok(key) = wallet.get_secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}

View file

@ -21,7 +21,7 @@ use grin_wallet_libwallet::TxLogEntryType;
use std::ops::Range;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::gui::icons::{ARCHIVE_BOX, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::icons::{ARCHIVE_BOX, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::WalletTab;
@ -29,7 +29,7 @@ use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN};
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{Content, Modal, PullToRefresh, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletData, WalletTransaction};
use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTransactionAction};
use crate::wallet::Wallet;
/// Wallet transactions tab content.
@ -50,6 +50,15 @@ impl WalletTab for WalletTransactions {
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if Modal::opened().is_none() {
// Show transaction modal on task result.
if let Some(id) = wallet.consume_tx_task_result() {
let tx = wallet.get_data().unwrap().tx_by_slate_id(id);
if let Some(tx) = tx {
self.show_tx_info_modal(tx.data.id);
}
}
}
self.modal_content_ui(ui, wallet, cb);
self.txs_ui(ui, wallet);
}
@ -72,7 +81,7 @@ impl WalletTransactions {
manual_sync: None,
};
if let Some(tx) = &tx {
content.show_tx_info_modal(tx);
content.show_tx_info_modal(tx.data.id);
}
content
}
@ -104,7 +113,7 @@ impl WalletTransactions {
return;
}
// Draw awaiting amount info if exists.
self.awaiting_info_ui(ui, &data);
self.awaiting_info_ui(ui, &data);
});
ui.add_space(4.0);
@ -161,10 +170,9 @@ impl WalletTransactions {
r.nw = 0.0 as u8;
r.sw = 0.0 as u8;
View::item_button(ui, r, FILE_TEXT, None, || {
self.show_tx_info_modal(tx);
self.show_tx_info_modal(tx.data.id);
});
}
// Draw button to cancel transaction.
if tx.can_cancel() {
let (icon, color) = (PROHIBIT, Some(Colors::red()));
@ -177,6 +185,10 @@ impl WalletTransactions {
.show();
});
}
//TODO: Draw button to repeat transaction task on error.
if tx.action_error.is_some() {
}
});
}
}
@ -303,52 +315,68 @@ impl WalletTransactions {
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if tx.finalizing {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing"))
} else {
if tx.cancelling {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling"))
} else if let Some(a) = &tx.action {
let error = if tx.action_error.is_none() {
"".to_string()
} else {
match tx.data.tx_type {
TxLogEntryType::TxReceived => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_receiving"))
},
TxLogEntryType::TxSent => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_sending"))
},
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
format!("{}: ", t!("error"))
};
let status = match a {
WalletTransactionAction::Cancelling => t!("wallets.tx_cancelling"),
WalletTransactionAction::Finalizing => t!("wallets.tx_finalizing"),
WalletTransactionAction::Posting => t!("wallets.tx_posting"),
WalletTransactionAction::SendingTor => t!("transport.tor_sending")
};
let icon = if error.is_empty() {
DOTS_THREE_CIRCLE
} else {
WARNING
};
format!("{} {}{}", icon, error, status)
} else {
match tx.data.tx_type {
TxLogEntryType::TxReceived => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_receiving")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::TxSent => {
let text = match tx.finalized() {
true => t!("wallets.await_fin_amount"),
false => t!("wallets.tx_sending")
};
format!("{} {}", DOTS_THREE_CIRCLE, text)
},
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => {
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
}
}
@ -439,8 +467,8 @@ impl WalletTransactions {
}
/// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, tx: &WalletTransaction) {
let modal = WalletTransactionContent::new(tx);
fn show_tx_info_modal(&mut self, id: u32) {
let modal = WalletTransactionContent::new(id);
self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::Center)
@ -450,28 +478,29 @@ impl WalletTransactions {
/// Confirmation [`Modal`] to cancel transaction.
fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
Modal::close();
return;
}
let tx = txs.get(0).unwrap();
let amount = amount_to_hr_string(tx.amount, true);
let text = match tx.data.tx_type {
TxLogEntryType::TxReceived => {
t!("wallets.tx_receive_cancel_conf", "amount" => amount)
},
_ => {
t!("wallets.tx_send_cancel_conf", "amount" => amount)
}
};
// Show confirmation text.
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Setup confirmation text.
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
Modal::close();
return;
}
let tx = txs.get(0).unwrap();
let amount = amount_to_hr_string(tx.amount, true);
let text = match tx.data.tx_type {
TxLogEntryType::TxReceived => {
t!("wallets.tx_receive_cancel_conf", "amount" => amount)
},
_ => {
t!("wallets.tx_send_cancel_conf", "amount" => amount)
}
};
ui.label(RichText::new(text)
.size(17.0)
.color(Colors::text(false)));
@ -492,7 +521,7 @@ impl WalletTransactions {
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
wallet.cancel(self.confirm_cancel_tx_id.unwrap());
wallet.task(WalletTask::Cancel(tx.clone()));
self.confirm_cancel_tx_id = None;
Modal::close();
});

View file

@ -17,12 +17,12 @@ use grin_core::core::amount_to_hr_string;
use grin_util::ToHex;
use grin_wallet_libwallet::TxLogEntryType;
use crate::gui::icons::{COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View};
use crate::gui::Colors;
use crate::wallet::types::WalletTransaction;
use crate::wallet::types::{WalletTask, WalletTransaction};
use crate::wallet::Wallet;
/// Transaction information [`Modal`] content.
@ -42,9 +42,9 @@ pub struct WalletTransactionContent {
impl WalletTransactionContent {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
pub fn new(tx: &WalletTransaction) -> Self {
pub fn new(id: u32) -> Self {
Self {
tx_id: tx.data.id,
tx_id: id,
qr_code_content: None,
scan_qr_content: None,
file_pick_button: FilePickContent::new(
@ -103,7 +103,7 @@ impl WalletTransactionContent {
modal.enable_closing();
self.scan_qr_content = None;
// Provide scan result as Slatepack message.
wallet.open_message(result.text());
wallet.task(WalletTask::OpenMessage(result.text()));
} else {
scan_content.ui(ui, cb);
}
@ -133,8 +133,8 @@ impl WalletTransactionContent {
// Show transaction information.
self.info_ui(ui, modal, tx, wallet, cb);
// Show transaction sharing content.
if tx.can_finalize || tx.is_response {
// Show transaction sharing content when can cancel or finalized.
if tx.can_cancel() && !tx.finalized() {
self.share_ui(ui, wallet, tx, cb);
}
@ -159,7 +159,7 @@ impl WalletTransactionContent {
tx: &WalletTransaction,
cb: &dyn PlatformCallbacks) {
let amount = amount_to_hr_string(tx.amount, true);
let desc_text = if tx.can_finalize {
let desc_text = if tx.can_finalize() {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.send_request_desc", "amount" => amount)
} else {
@ -186,8 +186,8 @@ impl WalletTransactionContent {
// Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text.clone(), Colors::white_or_black(false), || {
if let Some((_, d)) = wallet.read_slatepack_by_tx(tx) {
self.qr_code_content = Some(QrCodeContent::new(d, true));
if let Some(c) = wallet.read_slatepack(tx) {
self.qr_code_content = Some(QrCodeContent::new(c, true));
}
});
});
@ -198,10 +198,12 @@ impl WalletTransactionContent {
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Some((s, d)) = wallet.read_slatepack_by_tx(tx) {
let name = format!("{}.{}.slatepack", s.id, s.state);
let data = d.as_bytes().to_vec();
cb.share_data(name, data).unwrap_or_default();
if let Some(slate_id) = tx.data.tx_slate_id {
let name = format!("{}.{}.slatepack", slate_id, tx.state);
if let Some(c) = wallet.read_slatepack(tx) {
let data = c.as_bytes().to_vec();
cb.share_data(name, data).unwrap_or_default();
}
}
});
});
@ -242,10 +244,10 @@ impl WalletTransactionContent {
}
return;
}
if tx.can_finalize {
if tx.can_finalize() {
// Draw button to pick file.
self.file_pick_button.ui(ui, cb, |data| {
wallet.open_message(data);
wallet.task(WalletTask::OpenMessage(data));
});
// Draw button to scan QR code.
let r = CornerRadius::default();
@ -257,13 +259,14 @@ impl WalletTransactionContent {
}
// Draw button to cancel transaction.
if tx.can_cancel() {
let r = if tx.can_finalize {
let r = if tx.can_finalize() {
CornerRadius::default()
} else {
View::item_rounding(0, 2, true)
};
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
wallet.cancel(tx.data.id);
wallet.task(WalletTask::Cancel(tx.clone()));
Modal::close();
});
}
});
@ -280,7 +283,7 @@ impl WalletTransactionContent {
}
// Show receiver address.
if let Some(rec) = tx.receiver() {
let label = format!("{} {}", CUBE, t!("network_mining.address"));
let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address"));
info_item_ui(ui, rec.to_string(), label, true, cb);
}
}

View file

@ -23,7 +23,7 @@ use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings};
use crate::wallet::ConnectionsConfig;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::types::{ConnectionMethod, WalletTransaction};
/// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)]
@ -192,15 +192,27 @@ impl WalletConfig {
path.to_str().unwrap().to_string()
}
/// Get Slatepacks data path for current wallet.
pub fn get_slatepack_path(&self, slate: &Slate) -> PathBuf {
/// Get Slatepack file path for transaction.
pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf {
let mut path = PathBuf::from(self.get_wallet_path());
path.push(SLATEPACKS_DIR_NAME);
if !path.exists() {
let _ = fs::create_dir_all(path.clone());
}
let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state);
path.push(slatepack_file_name);
let file = format!("{}.{}.slatepack", tx.data.tx_slate_id.unwrap(), tx.state);
path.push(file);
path
}
/// Get Slatepack file path for Slate.
pub fn get_slate_path(&self, slate: &Slate) -> PathBuf {
let mut path = PathBuf::from(self.get_wallet_path());
path.push(SLATEPACKS_DIR_NAME);
if !path.exists() {
let _ = fs::create_dir_all(path.clone());
}
let file = format!("{}.{}.slatepack", slate.id, slate.state);
path.push(file);
path
}

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::{SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst};
use grin_wallet_libwallet::{Error, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst};
use grin_wallet_util::OnionV3Address;
use serde_derive::{Deserialize, Serialize};
@ -151,31 +151,133 @@ pub struct WalletData {
pub txs: Option<Vec<WalletTransaction>>
}
impl WalletData {
/// Update transaction action status.
pub fn on_tx_action(&mut self, id: String, action: Option<WalletTransactionAction>) {
if self.txs.is_none() {
return;
}
for tx in self.txs.as_mut().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
tx.action = action;
tx.action_error = None;
break;
}
}
}
}
/// Update transaction action error status.
pub fn on_tx_error(&mut self, id: String, err: Option<Error>) {
if self.txs.is_none() {
return;
}
for tx in self.txs.as_mut().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
tx.action_error = err;
break;
}
}
}
}
/// Get transaction by slate identifier.
pub fn tx_by_slate_id(&self, id: String) -> Option<WalletTransaction> {
if self.txs.is_none() {
return None;
}
for tx in self.txs.as_ref().unwrap() {
if let Some(slate_id) = tx.data.tx_slate_id {
if slate_id.to_string() == id {
return Some(tx.clone());
}
}
}
None
}
}
/// Wallet transaction action.
#[derive(Clone, PartialEq)]
pub enum WalletTransactionAction {
Cancelling, Finalizing, Posting, SendingTor
}
/// Wallet transaction data.
#[derive(Clone)]
pub struct WalletTransaction {
/// Transaction information.
/// Information from database.
pub data: TxLogEntry,
/// State of transaction Slate.
pub state: SlateState,
/// Calculated transaction amount between debited and credited amount.
pub amount: u64,
/// Flag to check if transaction is cancelling.
pub cancelling: bool,
/// Flag to check if transaction can be sent back to initiator to finalize.
pub is_response: bool,
/// Flag to check if transaction can be finalized based on Slatepack message state.
pub can_finalize: bool,
/// Flag to check if transaction is finalizing.
pub finalizing: bool,
/// Block height where tx was included.
pub height: Option<u64>,
/// Action on transaction.
pub action: Option<WalletTransactionAction>,
/// Action result error.
pub action_error: Option<Error>
}
impl WalletTransaction {
/// Check if transactions can be finalized after receiving response.
pub fn can_finalize(&self) -> bool {
!self.cancelling() && !self.data.confirmed &&
(!self.sending_tor() || self.action_error.is_some()) &&
(self.data.tx_type == TxLogEntryType::TxSent ||
self.data.tx_type == TxLogEntryType::TxReceived) &&
(self.state == SlateState::Invoice1 || self.state == SlateState::Standard1)
}
/// Check if transaction was finalized.
pub fn finalized(&self) -> bool {
(self.data.tx_type == TxLogEntryType::TxSent ||
self.data.tx_type == TxLogEntryType::TxReceived) &&
self.state == SlateState::Invoice3 || self.state == SlateState::Standard3
}
/// Check if transaction is sending over Tor.
pub fn sending_tor(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::SendingTor;
}
false
}
/// Check if transaction is cancelling.
pub fn cancelling(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Cancelling;
}
false
}
/// Check if transaction is posting.
pub fn posting(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Posting;
}
false
}
/// Check if transaction can be cancelled.
pub fn can_cancel(&self) -> bool {
!self.cancelling && !self.data.confirmed &&
self.data.tx_type != TxLogEntryType::TxReceivedCancelled
&& self.data.tx_type != TxLogEntryType::TxSentCancelled
!self.cancelling() && !self.data.confirmed &&
(!self.sending_tor() || self.action_error.is_some()) &&
self.data.tx_type != TxLogEntryType::TxReceivedCancelled &&
self.data.tx_type != TxLogEntryType::TxSentCancelled
}
/// Check if transaction is finalizing.
pub fn finalizing(&self) -> bool {
if let Some(a) = self.action.as_ref() {
return a == &WalletTransactionAction::Finalizing;
}
false
}
/// Get receiver address if payment proof was created.
@ -188,4 +290,33 @@ impl WalletTransaction {
}
None
}
}
/// Task for the wallet.
#[derive(Clone)]
pub enum WalletTask {
/// Open Slatepack message parsing result and making an action.
OpenMessage(String),
/// Create request to send.
/// * amount
/// * receiver
Send(u64, Option<SlatepackAddress>),
/// Resend request over Tor.
/// * local tx id
/// * receiver
SendTor(u32, SlatepackAddress),
/// Invoice creation.
/// * amount
Receive(u64),
/// Transaction finalization.
/// * tx
/// * local tx id
Finalize(Option<Slate>, u32),
/// Post transaction to blockchain.
/// * tx
/// * local tx id
Post(Option<Slate>, u32),
/// Cancel transaction.
/// * tx
Cancel(WalletTransaction),
}

File diff suppressed because it is too large Load diff