wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter

This commit is contained in:
ardocrat 2024-09-07 00:11:17 +03:00
parent c8bca08bdc
commit 21ecf200b8
17 changed files with 2051 additions and 2438 deletions

View file

@ -30,8 +30,8 @@ use crate::gui::views::View;
/// QR code image from text.
pub struct QrCodeContent {
/// Text to create QR code.
pub(crate) text: String,
/// QR code text.
text: String,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
@ -62,18 +62,18 @@ impl QrCodeContent {
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated {
// Show animated QR code.
self.animated_ui(ui, text, cb);
self.animated_ui(ui, cb);
} else {
// Show static QR code.
self.static_ui(ui, text, cb);
self.static_ui(ui, cb);
}
}
/// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
@ -84,7 +84,7 @@ impl QrCodeContent {
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list(text);
self.create_svg_list();
}
} else {
let svg_list = {
@ -111,7 +111,7 @@ impl QrCodeContent {
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
@ -131,7 +131,7 @@ impl QrCodeContent {
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize);
self.create_qr_gif();
});
} else {
ui.vertical_centered(|ui| {
@ -171,7 +171,7 @@ impl QrCodeContent {
}
/// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
@ -182,7 +182,7 @@ impl QrCodeContent {
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg(text);
self.create_svg();
}
} else {
// Create image from SVG data.
@ -194,7 +194,7 @@ impl QrCodeContent {
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
// Show button to share QR.
@ -204,21 +204,22 @@ impl QrCodeContent {
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
}
});
});
ui.add_space(8.0);
@ -267,8 +268,9 @@ impl QrCodeContent {
}
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) {
fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
@ -294,8 +296,9 @@ impl QrCodeContent {
}
/// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) {
fn create_svg(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
@ -332,13 +335,14 @@ impl QrCodeContent {
}
/// Create GIF image at separate thread.
fn create_qr_gif(&self, text: String, size: usize) {
fn create_qr_gif(&self) {
{
let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true;
}
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
@ -354,7 +358,7 @@ impl QrCodeContent {
) {
// Create an image from QR data.
let image = qr.render()
.max_dimensions(size as u32, size as u32)
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255]))
.build();

View file

@ -148,7 +148,7 @@ impl WalletContent {
ui.vertical_centered(|ui| {
// Draw wallet tabs.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, wallet);
self.tabs_ui(ui);
});
});
});
@ -468,7 +468,7 @@ impl WalletContent {
QrScanResult::Slatepack(message) => {
// Redirect to messages to handle parsed message.
let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string()));
WalletMessages::new(Some(message.to_string()));
messages.parse_message(wallet);
modal.close();
self.current_tab = Box::new(messages);
@ -477,8 +477,7 @@ impl WalletContent {
QrScanResult::Address(receiver) => {
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
// Redirect to send amount with Tor.
let addr = wallet.slatepack_address().unwrap();
let mut transport = WalletTransport::new(addr.clone());
let mut transport = WalletTransport::default();
modal.close();
transport.show_send_tor_modal(cb, Some(receiver.to_string()));
self.current_tab = Box::new(transport);
@ -528,7 +527,7 @@ impl WalletContent {
}
/// Draw tab buttons in the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.scope(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
@ -547,14 +546,13 @@ impl WalletContent {
let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
self.current_tab = Box::new(
WalletMessages::new(wallet.can_use_dandelion(), None)
WalletMessages::new(None)
);
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
let addr = wallet.slatepack_address().unwrap();
self.current_tab = Box::new(WalletTransport::new(addr));
self.current_tab = Box::new(WalletTransport::default());
});
});
columns[3].vertical_centered_justified(|ui| {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,541 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickButton, Modal, Content, View, CameraContent};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal};
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Slatepack messages interaction tab content.
pub struct WalletMessages {
/// Slatepacks message input text.
message_edit: String,
/// Flag to check if message request is loading.
message_loading: bool,
/// Error on finalization, parse or response creation.
message_error: String,
/// Parsed message result with finalization flag and transaction.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// Wallet transaction [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
/// Invoice or sending request creation [`Modal`] content.
request_modal_content: Option<MessageRequestModal>,
/// Camera content for Slatepack message QR code scanning [`Modal`].
message_camera_content: CameraContent,
/// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`].
message_scan_error: bool,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
}
/// Identifier for amount input [`Modal`] to create invoice or sending request.
const REQUEST_MODAL: &'static str = "messages_request";
/// Identifier for [`Modal`] modal to show transaction information.
const TX_INFO_MODAL: &'static str = "messages_tx_info";
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MESSAGE_MODAL: &'static str = "qr_slatepack_message_scan_modal";
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.id_source(Id::from("wallet_messages").with(wallet.get_config().id))
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
}
}
impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self {
Self {
message_edit: message.unwrap_or("".to_string()),
message_loading: false,
message_error: "".to_string(),
message_result: Arc::new(Default::default()),
tx_info_content: None,
request_modal_content: None,
message_camera_content: Default::default(),
message_scan_error: false,
file_pick_button: FilePickButton::default(),
}
}
/// Draw manual wallet transaction interaction content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
// Show creation of request to send or receive funds.
self.request_ui(ui, wallet, cb);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Slatepack message input field.
self.input_slatepack_ui(ui, wallet, cb);
ui.add_space(6.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 {
REQUEST_MODAL => {
if let Some(content) = self.request_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
SCAN_QR_MESSAGE_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_message_scan_modal_ui(ui, modal, wallet, cb);
});
}
_ => {}
}
}
}
}
/// Draw creation of request to send or receive funds.
fn request_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("wallets.create_request_desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Show send button only if balance is not empty.
let data = wallet.get_data().unwrap();
if data.info.amount_currently_spendable > 0 {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || {
self.show_request_modal(false, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
self.receive_button_ui(ui, cb);
});
});
} else {
self.receive_button_ui(ui, cb);
}
}
/// Draw invoice request creation button.
fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive"));
View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || {
self.show_request_modal(true, cb);
});
}
/// Show [`Modal`] to create invoice or sending request.
fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) {
self.request_modal_content = Some(MessageRequestModal::new(invoice));
let title = if invoice {
t!("wallets.receive")
} else {
t!("wallets.send")
};
Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show();
cb.show_keyboard();
}
/// Draw Slatepack message input content.
fn input_slatepack_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
// Setup description text.
if !self.message_error.is_empty() {
ui.label(RichText::new(&self.message_error).size(16.0).color(Colors::red()));
} else {
ui.label(RichText::new(t!("wallets.input_slatepack_desc"))
.size(16.0)
.color(Colors::inactive_text()));
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
// Save message to check for changes.
let message_before = self.message_edit.clone();
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
ScrollArea::vertical()
.id_source(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!self.message_loading)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
// Show soft keyboard on click.
if resp.clicked() {
resp.request_focus();
cb.show_keyboard();
}
if resp.has_focus() {
// Apply text from input on Android as temporary fix for egui.
View::on_soft_input(ui, input_id, &mut self.message_edit);
}
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Parse message if input field was changed.
if message_before != self.message_edit {
self.parse_message(wallet);
}
if self.message_loading {
View::small_loading_spinner(ui);
// Check loading result.
let has_tx = {
let r_res = self.message_result.read();
r_res.is_some()
};
if has_tx {
let mut w_res = self.message_result.write();
let tx_res = w_res.as_ref().unwrap();
let slate = &tx_res.0;
match &tx_res.1 {
Ok(tx) => {
self.message_edit.clear();
// Show transaction modal on success.
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
*w_res = None;
}
Err(err) => {
match err {
// Set already canceled transaction error message.
Error::TransactionWasCancelled {..} => {
self.message_error = t!("wallets.resp_canceled_err");
}
// Set an error when there is not enough funds to pay.
Error::NotEnoughFunds {..} => {
let m = t!(
"wallets.pay_balance_error",
"amount" => amount_to_hr_string(slate.amount, true)
);
self.message_error = m;
}
// Set default error message.
_ => {
let finalize = slate.state == SlateState::Standard2 ||
slate.state == SlateState::Invoice2;
self.message_error = if finalize {
t!("wallets.finalize_slatepack_err")
} else {
t!("wallets.resp_slatepack_err")
};
}
}
}
}
self.message_loading = false;
}
return;
}
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to scan Slatepack message QR code.
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::button(), || {
self.message_edit.clear();
self.message_error.clear();
self.show_qr_message_scan_modal(cb);
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to paste text from clipboard.
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste, Colors::button(), || {
let buf = cb.get_string_from_buffer();
let previous = self.message_edit.clone();
self.message_edit = buf.clone().trim().to_string();
// Parse Slatepack message resetting message error.
if buf != previous {
self.parse_message(wallet);
self.parse_message(wallet);
}
});
});
});
ui.add_space(10.0);
});
if self.message_edit.is_empty() {
// Draw button to choose file.
let mut parsed_text = "".to_string();
self.file_pick_button.ui(ui, cb, |text| {
parsed_text = text;
});
self.message_edit = parsed_text;
self.parse_message(wallet);
} else {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::button(), || {
self.message_edit.clear();
self.message_error.clear();
});
}
}
/// Parse message input making operation based on incoming status.
pub fn parse_message(&mut self, wallet: &Wallet) {
self.message_error.clear();
self.message_edit = self.message_edit.trim().to_string();
if self.message_edit.is_empty() {
return;
}
if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) {
// Try to setup empty amount from transaction by id.
if slate.amount == 0 {
let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| {
if tx.data.tx_slate_id == Some(slate.id) {
if slate.amount == 0 {
slate.amount = tx.amount;
}
}
tx
}).collect::<Vec<&WalletTransaction>>();
}
// Check if message with same id and state already exists to show tx modal.
let exists = wallet.read_slatepack(&slate).is_some();
if exists {
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
} else {
self.message_error = t!("wallets.parse_slatepack_err");
}
return;
}
// Create response or finalize at separate thread.
let sl = slate.clone();
let message = self.message_edit.clone();
let message_result = self.message_result.clone();
let wallet = wallet.clone();
self.message_loading = true;
thread::spawn(move || {
let result = match slate.state {
SlateState::Standard1 | SlateState::Invoice1 => {
if sl.state != SlateState::Standard1 {
wallet.pay(&message)
} else {
wallet.receive(&message)
}
}
SlateState::Standard2 | SlateState::Invoice2 => {
wallet.finalize(&message)
}
_ => {
if let Some(tx) = wallet.tx_by_slate(&slate) {
Ok(tx)
} else {
Err(Error::GenericError(t!("wallets.parse_slatepack_err")))
}
}
};
let mut w_res = message_result.write();
*w_res = Some((slate, result));
});
} else {
self.message_error = t!("wallets.parse_slatepack_err");
}
}
/// Show QR code Slatepack message scanner [`Modal`].
pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.message_scan_error = false;
// Show QR code scan modal.
Modal::new(SCAN_QR_MESSAGE_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw QR code scanner [`Modal`] content.
fn qr_message_scan_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
if self.message_scan_error {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", ".");
ui.label(RichText::new(err_text)
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.message_scan_error = false;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.message_scan_error = false;
cb.start_camera();
});
});
});
ui.add_space(6.0);
return;
} else if let Some(result) = self.message_camera_content.qr_scan_result() {
cb.stop_camera();
self.message_camera_content.clear_state();
match &result {
QrScanResult::Slatepack(text) => {
self.message_edit = text.to_string();
self.parse_message(wallet);
modal.close();
}
_ => {
self.message_scan_error = true;
}
}
} else {
ui.add_space(6.0);
self.message_camera_content.ui(ui, cb);
ui.add_space(8.0);
}
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
ui.add_space(6.0);
}
}

View file

@ -0,0 +1,18 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod request;

View file

@ -0,0 +1,260 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use parking_lot::RwLock;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::Error;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice or sending request creation [`Modal`] content.
pub struct MessageRequestModal {
/// Flag to check if invoice or sending request was opened.
invoice: bool,
/// Amount to send or 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<WalletTransactionModal>,
}
impl MessageRequestModal {
/// Create new content instance.
pub fn new(invoice: bool) -> Self {
Self {
invoice,
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut 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;
}
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, wallet, modal);
return;
}
// Draw amount input content.
self.amount_input_ui(ui, wallet, modal, cb);
// 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.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
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;
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let invoice = self.invoice.clone();
let result = self.request_result.clone();
// Send request at another thread.
self.request_loading = true;
thread::spawn(move || {
let res = if invoice {
wallet.issue_invoice(a)
} else {
wallet.send(a)
};
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
let err = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(err);
}
});
});
});
ui.add_space(6.0);
}
/// Draw amount input content.
fn amount_input_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.vertical_centered(|ui| {
let enter_text = if self.invoice {
t!("wallets.enter_amount_receive")
} else {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
t!("wallets.enter_amount_send","amount" => amount)
};
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw request amount text input.
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
let amount_edit_before = self.amount_edit.clone();
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
// 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()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
if !self.invoice {
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, 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(WalletTransactionModal::new(wallet, tx, false));
}
Err(err) => {
match err {
Error::NotEnoughFunds { .. } => {
let m = t!(
"wallets.pay_balance_error",
"amount" => self.amount_edit
);
self.request_error = Some(m);
}
_ => {
let m = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(m);
}
}
self.request_loading = false;
}
}
}
}
}

View file

@ -36,7 +36,7 @@ pub struct CommonSettings {
new_pass_edit: String,
/// Minimum confirmations number value.
min_confirmations_edit: String
min_confirmations_edit: String,
}
/// Identifier for wallet name [`Modal`].
@ -54,25 +54,26 @@ impl Default for CommonSettings {
wrong_pass: false,
old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(),
min_confirmations_edit: "".to_string()
min_confirmations_edit: "".to_string(),
}
}
}
impl CommonSettings {
/// Draw common wallet settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
ui.vertical_centered(|ui| {
let wallet_name = wallet.get_config().name;
let config = wallet.get_config();
// Show wallet name.
ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.name"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(2.0);
ui.label(RichText::new(wallet_name.clone())
ui.label(RichText::new(&config.name)
.size(16.0)
.color(Colors::white_or_black(true)));
ui.add_space(8.0);
@ -80,7 +81,7 @@ impl CommonSettings {
// Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::button(), || {
self.name_edit = wallet_name;
self.name_edit = config.name;
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
@ -118,10 +119,9 @@ impl CommonSettings {
ui.add_space(6.0);
// Show minimum amount of confirmations value setup.
let min_confirmations = wallet.get_config().min_confirmations;
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations);
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
View::button(ui, min_conf_text, Colors::button(), || {
self.min_confirmations_edit = min_confirmations.to_string();
self.min_confirmations_edit = config.min_confirmations.to_string();
// Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
@ -131,8 +131,15 @@ impl CommonSettings {
});
ui.add_space(12.0);
// Setup ability to post wallet transactions with Dandelion.
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
wallet.update_use_dandelion(!wallet.can_use_dandelion());
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
ui.add_space(6.0);
});
}

View file

@ -232,7 +232,7 @@ impl RecoverySettings {
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
let mut on_next = || {
match wallet.get_recovery(self.pass_edit.clone()) {
Ok(phrase) => {
self.wrong_pass = false;
@ -243,6 +243,12 @@ impl RecoverySettings {
self.wrong_pass = true;
}
}
};
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next();
});
});
});

View file

@ -1,944 +0,0 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
use egui::os::OperatingSystem;
use egui::scroll_area::ScrollBarVisibility;
use parking_lot::RwLock;
use tor_rtcompat::BlockOn;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Flag to check if transaction is sending over Tor to show progress at [`Modal`].
tor_sending: Arc<RwLock<bool>>,
/// Flag to check if error occurred during sending of transaction over Tor at [`Modal`].
tor_send_error: Arc<RwLock<bool>>,
/// Flag to check if transaction sent successfully over Tor [`Modal`].
tor_success: Arc<RwLock<bool>>,
/// Entered amount value for [`Modal`].
amount_edit: String,
/// Entered address value for [`Modal`].
address_edit: String,
/// Flag to check if entered address is incorrect at [`Modal`].
address_error: bool,
/// Flag to check if QR code scanner is opened at address [`Modal`].
show_address_scan: bool,
/// Address QR code scanner [`Modal`] content.
address_scan_content: CameraContent,
/// Flag to check if [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
/// QR code address image [`Modal`] content.
qr_address_content: QrCodeContent,
/// Flag to check if Tor settings were changed.
tor_settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Flag to check if QR code scanner is opened at bridge [`Modal`].
show_bridge_scan: bool,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: CameraContent,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
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 transport content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
}
}
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl WalletTransport {
/// Create new content instance from provided Slatepack address text.
pub fn new(addr: String) -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
tor_sending: Arc::new(RwLock::new(false)),
tor_send_error: Arc::new(RwLock::new(false)),
tor_success: Arc::new(RwLock::new(false)),
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
show_address_scan: false,
address_scan_content: CameraContent::default(),
modal_just_opened: false,
qr_address_content: QrCodeContent::new(addr, false),
tor_settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
show_bridge_scan: false,
bridge_qr_scan_content: CameraContent::default(),
}
}
/// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor content.
self.tor_ui(ui, wallet, cb);
}
/// 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 {
SEND_TOR_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.send_tor_modal_ui(ui, wallet, modal, cb);
});
}
TOR_SETTINGS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.tor_settings_modal_ui(ui, wallet, modal, cb);
});
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.show_tor_settings_modal();
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
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);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
};
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Show Tor transport settings [`Modal`].
fn show_tor_settings_modal(&mut self) {
self.tor_settings_changed = false;
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
}
/// Draw Tor transport settings [`Modal`] content.
fn tor_settings_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if self.show_bridge_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_bridge_scan = false;
};
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop(&mut self.bridge_qr_scan_content);
cb.show_keyboard();
} else {
self.bridge_qr_scan_content.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.tor_settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.tor_settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
.paste()
.no_focus();
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
.paste()
.no_focus()
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
// Check if scan button was pressed.
if conn_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
conn_edit_opts.scan_pressed = false;
self.show_bridge_scan = true;
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.tor_settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.tor_settings_changed {
self.tor_settings_changed = false;
// 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() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}
} else {
Tor::rebuild_client();
}
}
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let slatepack_addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content.clear_state();
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, Rounding::default(), COPY, None, || {
cb.copy_string_to_buffer(slatepack_addr.clone());
});
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);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, slatepack_addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
let text = self.qr_address_content.text.clone();
self.qr_address_content.ui(ui, text.clone(), cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content.clear_state();
m.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
{
let mut w_send_err = self.tor_send_error.write();
*w_send_err = false;
let mut w_sending = self.tor_sending.write();
*w_sending = false;
let mut w_success = self.tor_success.write();
*w_success = false;
}
self.modal_just_opened = true;
self.amount_edit = "".to_string();
self.address_edit = address.unwrap_or("".to_string());
self.address_error = false;
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
cb.show_keyboard();
}
/// Check if error occurred during sending over Tor at [`Modal`].
fn has_tor_send_error(&self) -> bool {
let r_send_err = self.tor_send_error.read();
r_send_err.clone()
}
/// Check if transaction is sending over Tor to show progress at [`Modal`].
fn tor_sending(&self) -> bool {
let r_sending = self.tor_sending.read();
r_sending.clone()
}
/// Check if transaction sent over Tor with success at [`Modal`].
fn tor_success(&self) -> bool {
let r_success = self.tor_success.read();
r_success.clone()
}
/// Draw amount input [`Modal`] content to send over Tor.
fn send_tor_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
let has_send_err = self.has_tor_send_error();
let sending = self.tor_sending();
if !has_send_err && !sending {
// Draw QR code scanner content if requested.
if self.show_address_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_address_scan = false;
};
if let Some(result) = self.address_scan_content.qr_scan_result() {
self.address_edit = result.text();
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
} else {
self.address_scan_content.ui(ui, cb);
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.address_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
return;
}
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
let amount_edit_before = self.amount_edit.clone();
if self.modal_just_opened {
self.modal_just_opened = false;
amount_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Draw address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id);
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
.paste()
.no_focus()
.scan_qr();
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
// Check if scan button was pressed.
if address_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
address_edit_opts.scan_pressed = false;
self.show_address_scan = true;
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
// Check entered address.
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
} else {
self.address_error = true;
}
});
});
});
ui.add_space(6.0);
} else if has_send_err {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
let mut w_send_error = self.tor_send_error.write();
*w_send_error = false;
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let addr_text = self.address_edit.clone();
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
let addr_str = addr_text.as_str();
let addr = &SlatepackAddress::try_from(addr_str)
.unwrap();
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
});
});
});
ui.add_space(6.0);
} else {
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(10.0);
// Close modal on success sending.
if self.tor_success() {
modal.close();
}
}
}
}

View file

@ -0,0 +1,397 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, QrCodeContent, Content, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::transport::send::TransportSendModal;
use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
use crate::tor::{Tor, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Sending [`Modal`] content.
send_modal_content: Option<TransportSendModal>,
/// QR code address image [`Modal`] content.
qr_address_content: Option<QrCodeContent>,
/// Tor settings [`Modal`] content.
settings_modal_content: Option<TransportSettingsModal>,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
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 transport content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
}
}
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl Default for WalletTransport {
fn default() -> Self {
Self {
send_modal_content: None,
qr_address_content: None,
settings_modal_content: None,
}
}
}
impl WalletTransport {
/// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor transport content.
self.tor_ui(ui, wallet, cb);
}
/// 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 {
SEND_TOR_MODAL => {
if let Some(content) = self.send_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TOR_SETTINGS_MODAL => {
if let Some(content) = self.settings_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
let service_id = &wallet.identifier();
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() &&
!Tor::is_service_starting(service_id) {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.settings_modal_content = Some(TransportSettingsModal::default());
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
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);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
};
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false));
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, Rounding::default(), COPY, None, || {
cb.copy_string_to_buffer(addr.clone());
});
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);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
if let Some(content) = self.qr_address_content.as_mut() {
content.ui(ui, cb);
} else {
modal.close();
return;
}
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
self.send_modal_content = Some(TransportSendModal::new(address));
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
cb.show_keyboard();
}
}

View file

@ -0,0 +1,19 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod send;
mod settings;

View file

@ -0,0 +1,357 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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::{Error, SlatepackAddress};
use parking_lot::RwLock;
use tor_rtcompat::BlockOn;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transport sending [`Modal`] content.
pub struct TransportSendModal {
/// Flag to focus on first input field after opening.
first_draw: bool,
/// Flag to check if transaction is sending to show progress.
sending: bool,
/// Flag to check if there is an error to repeat.
error: bool,
/// Transaction result.
send_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Entered amount value.
amount_edit: String,
/// Entered address value.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
/// Transaction information content.
tx_info_content: Option<WalletTransactionModal>,
}
impl TransportSendModal {
/// Create new instance from provided address.
pub fn new(addr: Option<String>) -> Self {
Self {
first_draw: true,
sending: false,
error: false,
send_result: Arc::new(RwLock::new(None)),
amount_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
address_scan_content: None,
tx_info_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.tx_info_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Draw sending content, progress or an error.
if self.sending {
self.progress_ui(ui, wallet);
} else if self.error {
self.error_ui(ui, wallet, modal, cb);
} else {
self.content_ui(ui, wallet, modal, cb);
}
}
/// Draw content to send.
fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw QR code scanner content if requested.
if let Some(scanner) = self.address_scan_content.as_mut() {
let mut on_stop = || {
self.first_draw = true;
cb.stop_camera();
modal.enable_closing();
};
if let Some(result) = scanner.qr_scan_result() {
self.address_edit = result.text();
on_stop();
self.address_scan_content = None;
cb.show_keyboard();
} else {
scanner.ui(ui, cb);
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
return;
}
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
let amount_edit_before = self.amount_edit.clone();
if self.first_draw {
self.first_draw = false;
amount_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Draw address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id);
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
.paste()
.no_focus()
.scan_qr();
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
// Check if scan button was pressed.
if address_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
address_edit_opts.scan_pressed = false;
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close(modal, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
self.send(wallet, modal, cb);
});
});
});
ui.add_space(6.0);
}
/// Draw error content.
fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close(modal, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
self.send(wallet, modal, cb);
});
});
});
ui.add_space(6.0);
}
/// Close modal and clear data.
fn close(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
let mut w_res = self.send_result.write();
*w_res = None;
self.tx_info_content = None;
self.address_scan_content = None;
cb.hide_keyboard();
modal.close();
}
/// Send entered amount to address.
fn send(&mut self, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.amount_edit.is_empty() {
return;
}
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Send amount over Tor.
let mut wallet = wallet.clone();
let res = self.send_result.clone();
self.sending = true;
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
let result = wallet.send_tor(a, &addr).await;
let mut w_res = res.write();
*w_res = Some(result);
});
});
}
} else {
self.address_error = true;
}
}
/// Draw sending progress content.
fn progress_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(10.0);
// Check sending result.
let has_result = {
let r_result = self.send_result.read();
r_result.is_some()
};
if has_result {
{
let res = self.send_result.read().clone().unwrap();
match res {
Ok(tx) => {
self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false));
}
Err(_) => {
self.error = true;
}
}
}
let mut w_res = self.send_result.write();
*w_res = None;
self.sending = false;
}
}
}

View file

@ -0,0 +1,258 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::wallet::Wallet;
/// Transport settings [`Modal`] content.
pub struct TransportSettingsModal {
/// Flag to check if Tor settings were changed.
settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: Option<CameraContent>,
}
impl Default for TransportSettingsModal {
fn default() -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
bridge_qr_scan_content: None,
}
}
}
impl TransportSettingsModal {
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scanner) = self.bridge_qr_scan_content.as_mut() {
let on_stop = || {
cb.stop_camera();
modal.enable_closing();
};
if let Some(result) = scanner.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop();
self.bridge_qr_scan_content = None;
cb.show_keyboard();
} else {
scanner.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
.paste()
.no_focus();
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
.paste()
.no_focus()
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
// Check if scan button was pressed.
if conn_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
conn_edit_opts.scan_pressed = false;
self.bridge_qr_scan_content = Some(CameraContent::default());
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.settings_changed {
self.settings_changed = false;
// 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() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}
} else {
Tor::rebuild_client();
}
}
modal.close();
});
});
ui.add_space(6.0);
}
}

View file

@ -19,7 +19,7 @@ use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::TxLogEntryType;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, PullToRefresh, Content, View};
use crate::gui::views::types::ModalPosition;
@ -226,19 +226,6 @@ impl WalletTransactions {
.show();
});
}
// Draw button to repost transaction.
if tx.can_repost(data) {
let r = Rounding::default();
let (icon, color) = (ARROW_CLOCKWISE, Colors::green());
View::item_button(ui, r, icon, Some(color), || {
cb.hide_keyboard();
// Post tx after getting slate from slatepack file.
if let Some((s, _)) = wallet.read_slate_by_tx(tx) {
let _ = wallet.post(&s, wallet.can_use_dandelion());
}
});
}
});
}
});
@ -249,7 +236,7 @@ impl WalletTransactions {
if refresh_resp.should_refresh() {
self.manual_sync = Some(now);
if !wallet.syncing() {
wallet.sync(true);
wallet.sync();
}
}
}
@ -339,7 +326,7 @@ impl WalletTransactions {
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if tx.posting {
} else if tx.finalizing {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing"))
} else {
if tx.cancelling {
@ -431,8 +418,7 @@ impl WalletTransactions {
/// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) {
let mut modal = WalletTransactionModal::new(wallet, tx);
modal.show_finalization = finalize;
let modal = WalletTransactionModal::new(wallet, tx, finalize);
self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)

View file

@ -21,7 +21,7 @@ use grin_util::ToHex;
use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_CLOCKWISE, BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View};
@ -41,7 +41,7 @@ pub struct WalletTransactionModal {
response_edit: String,
/// Flag to show transaction finalization input.
pub show_finalization: bool,
show_finalization: bool,
/// Finalization Slatepack message input value.
finalize_edit: String,
/// Flag to check if error happened during transaction finalization.
@ -49,17 +49,13 @@ pub struct WalletTransactionModal {
/// Flag to check if transaction is finalizing.
finalizing: bool,
/// Transaction finalization result.
final_result: Arc<RwLock<Option<Result<Slate, Error>>>>,
final_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if QR code is showing.
show_qr: bool,
/// QR code Slatepack message image content.
qr_code_content: QrCodeContent,
qr_code_content: Option<QrCodeContent>,
/// Flag to check if QR code scanner is showing.
show_scanner: bool,
/// QR code scanner content.
scanner_content: CameraContent,
qr_scan_content: Option<CameraContent>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
@ -67,7 +63,7 @@ pub struct WalletTransactionModal {
impl WalletTransactionModal {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
pub fn new(wallet: &Wallet, tx: &WalletTransaction) -> Self {
pub fn new(wallet: &Wallet, tx: &WalletTransaction, show_finalization: bool) -> Self {
Self {
tx_id: tx.data.id,
slate_id: match tx.data.tx_slate_id {
@ -98,13 +94,11 @@ impl WalletTransactionModal {
},
finalize_edit: "".to_string(),
finalize_error: false,
show_finalization: false,
show_finalization,
finalizing: false,
final_result: Arc::new(RwLock::new(None)),
show_qr: false,
qr_code_content: QrCodeContent::new("".to_string(), true),
show_scanner: false,
scanner_content: CameraContent::default(),
qr_code_content: None,
qr_scan_content: None,
file_pick_button: FilePickButton::default(),
}
}
@ -133,7 +127,7 @@ impl WalletTransactionModal {
}
let tx = txs.get(0).unwrap();
if !self.show_qr && !self.show_scanner {
if self.qr_code_content.is_none() && self.qr_scan_content.is_none() {
ui.add_space(6.0);
// Show transaction amount status and time.
@ -174,25 +168,6 @@ impl WalletTransactionModal {
wallet.cancel(tx.data.id);
});
}
// Draw button to repost transaction.
if wallet_loaded && tx.can_repost(&data) {
let r = if self.show_finalization {
Rounding::default()
} else {
let mut r = r.clone();
r.nw = 0.0;
r.sw = 0.0;
r
};
View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || {
cb.hide_keyboard();
// Post tx after getting slate from slatepack file.
if let Some((s, _)) = wallet.read_slate_by_tx(tx) {
let _ = wallet.post(&s, wallet.can_use_dandelion());
}
});
}
});
// Show transaction ID info.
@ -207,54 +182,49 @@ impl WalletTransactionModal {
}
}
// Show Slatepack message or reset flag to show QR if not available.
if !tx.posting && !tx.data.confirmed && !tx.cancelling &&
// Show Slatepack message or reset QR code state if not available.
if !tx.finalizing && !tx.data.confirmed && !tx.cancelling &&
(tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxReceived) {
tx.data.tx_type == TxLogEntryType::TxReceived) && !self.response_edit.is_empty() {
self.message_ui(ui, tx, wallet, modal, cb);
} else if self.show_qr {
self.qr_code_content.clear_state();
self.show_qr = false;
} else if let Some(qr_content) = self.qr_code_content.as_mut() {
qr_content.clear_state();
}
if !self.finalizing {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
if self.show_qr {
if self.qr_code_content.is_some() {
// Show buttons to close modal or come back to text request content.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_code_content.clear_state();
self.show_qr = false;
self.qr_code_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.qr_code_content.clear_state();
self.show_qr = false;
self.qr_code_content = None;
});
});
});
} else if self.show_scanner {
} else if self.qr_scan_content.is_some() {
ui.add_space(8.0);
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scanner_content.clear_state();
self.show_scanner = false;
self.qr_scan_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scanner_content.clear_state();
self.show_scanner = false;
self.qr_scan_content = None;
modal.enable_closing();
});
});
@ -361,20 +331,19 @@ impl WalletTransactionModal {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if self.show_scanner {
if let Some(result) = self.scanner_content.qr_scan_result() {
if let Some(qr_scan_content) = self.qr_scan_content.as_mut() {
if let Some(result) = qr_scan_content.qr_scan_result() {
cb.stop_camera();
self.scanner_content.clear_state();
qr_scan_content.clear_state();
// Setup value to finalization input field.
self.finalize_edit = result.text();
self.on_finalization_input_change(tx, wallet, modal, cb);
modal.enable_closing();
self.scanner_content.clear_state();
self.show_scanner = false;
self.qr_scan_content = None;
} else {
self.scanner_content.ui(ui, cb);
qr_scan_content.ui(ui, cb);
}
return;
}
@ -427,16 +396,9 @@ impl WalletTransactionModal {
let message_before = message_edit.clone();
// Draw QR code content if requested.
if self.show_qr {
let text = message_edit.clone();
if text.is_empty() {
self.qr_code_content.clear_state();
self.show_qr = false;
} else {
// Draw QR code content.
self.qr_code_content.ui(ui, text.clone(), cb);
return;
}
if let Some(qr_content) = self.qr_code_content.as_mut() {
qr_content.ui(ui, cb);
return;
}
// Draw Slatepack message finalization input or request text.
@ -498,7 +460,7 @@ impl WalletTransactionModal {
cb.hide_keyboard();
modal.disable_closing();
cb.start_camera();
self.show_scanner = true;
self.qr_scan_content = Some(CameraContent::default());
});
});
columns[1].vertical_centered_justified(|ui| {
@ -535,9 +497,10 @@ impl WalletTransactionModal {
columns[0].vertical_centered_justified(|ui| {
// Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text, Colors::button(), || {
View::button(ui, qr_text.clone(), Colors::button(), || {
cb.hide_keyboard();
self.show_qr = true;
let text = self.response_edit.clone();
self.qr_code_content = Some(QrCodeContent::new(text, true));
});
});
columns[1].vertical_centered_justified(|ui| {
@ -599,7 +562,7 @@ impl WalletTransactionModal {
self.finalizing = true;
modal.disable_closing();
thread::spawn(move || {
let res = wallet.finalize(&message, wallet.can_use_dandelion());
let res = wallet.finalize(&message);
let mut w_res = final_res.write();
*w_res = Some(res);
});

View file

@ -158,14 +158,12 @@ pub struct WalletTransaction {
pub amount: u64,
/// Flag to check if transaction is cancelling.
pub cancelling: bool,
/// 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,
/// Flag to check if transaction is finalizing.
pub finalizing: bool,
/// Block height when tx was confirmed.
pub conf_height: Option<u64>,
/// Block height when tx was reposted.
pub repost_height: Option<u64>,
/// Flag to check if tx was received after sync from node.
pub from_node: bool,
}
@ -173,16 +171,8 @@ pub struct WalletTransaction {
impl WalletTransaction {
/// Check if transaction can be cancelled.
pub fn can_cancel(&self) -> bool {
self.from_node && !self.cancelling && !self.posting && !self.data.confirmed &&
self.from_node && !self.cancelling && !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.from_node && self.posting && self.repost_height.is_some() &&
last_height - self.repost_height.unwrap() > min_conf
}
}

View file

@ -437,8 +437,8 @@ impl Wallet {
// Mark wallet as not opened.
wallet_close.closing.store(false, Ordering::Relaxed);
wallet_close.is_open.store(false, Ordering::Relaxed);
// Wake up thread to exit.
wallet_close.sync(true);
// Start sync to exit from thread.
wallet_close.sync();
});
}
@ -464,8 +464,8 @@ impl Wallet {
});
}
// Sync wallet data.
self.sync(false);
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(())
})
}
@ -498,7 +498,7 @@ impl Wallet {
self.info_sync_progress.store(0, Ordering::Relaxed);
// Sync wallet data.
self.sync(false);
self.sync();
Ok(())
}
@ -555,18 +555,11 @@ impl Wallet {
r_data.clone()
}
/// Sync wallet data from node or locally.
pub fn sync(&self, from_node: bool) {
if from_node {
let thread_r = self.sync_thread.read();
if let Some(thread) = thread_r.as_ref() {
thread.unpark();
}
} else {
let wallet = self.clone();
thread::spawn(move || {
sync_wallet_data(&wallet, false);
});
/// Sync wallet data from node at sync thread or locally synchronously.
pub fn sync(&self) {
let thread_r = self.sync_thread.read();
if let Some(thread) = thread_r.as_ref() {
thread.unpark();
}
}
@ -625,13 +618,7 @@ impl Wallet {
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 ||
let state = 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 {
@ -681,7 +668,7 @@ impl Wallet {
}
/// Initialize a transaction to send amount, return request for funds receiver.
pub fn send(&self, amount: u64) -> Result<(Slate, String), Error> {
pub fn send(&self, amount: u64) -> Result<WalletTransaction, Error> {
let config = self.get_config();
let args = InitTxArgs {
src_acct_name: Some(config.account),
@ -698,51 +685,35 @@ impl Wallet {
api.tx_lock_outputs(None, &slate)?;
// Create Slatepack message response.
let message_resp = self.create_slatepack_message(&slate)?;
let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info.
self.sync(false);
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok((slate, message_resp))
let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
}
/// Send amount to provided address with Tor transport.
pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option<Slate> {
pub async fn send_tor(&mut self,
amount: u64,
addr: &SlatepackAddress) -> Result<WalletTransaction, Error> {
// Initialize transaction.
let send_res = self.send(amount);
if send_res.is_err() {
return None;
let tx = self.send(amount)?;
let slate_res = self.read_slate_by_tx(&tx);
if slate_res.is_none() {
return Err(Error::GenericError("Slate not found".to_string()));
}
let slate = send_res.unwrap().0;
let (slate, _) = slate_res.unwrap();
// Function to cancel initialized tx in case of error.
let cancel_tx = || {
let instance = self.instance.clone().unwrap();
let id = slate.clone().id;
cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap();
// Setup posting flag, and ability to finalize.
{
let mut w_data = self.data.write();
let mut data = w_data.clone().unwrap();
let txs = data.txs.clone().unwrap().iter_mut().map(|tx| {
if tx.data.tx_slate_id == Some(id) {
tx.cancelling = false;
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::<Vec<WalletTransaction>>();
data.txs = Some(txs);
*w_data = Some(data);
}
// Refresh wallet info to update statuses.
self.sync(false);
sync_wallet_data(&self, false);
};
// Initialize parameters.
@ -764,17 +735,15 @@ impl Wallet {
let req_res = Tor::post(body, url).await;
if req_res.is_none() {
cancel_tx();
return None;
return Err(Error::GenericError("Tor post error".to_string()));
}
// Parse response and finalize transaction.
// Parse response.
let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap();
if res["error"] != json!(null) {
cancel_tx();
return None;
return Err(Error::GenericError("Tx error".to_string()));
}
// Slatepack message json value.
let slate_value = res["result"]["Ok"].clone();
let mut ret_slate = None;
@ -788,7 +757,7 @@ impl Wallet {
// Save Slatepack message to file.
let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string());
// Post transaction to blockchain.
let result = self.post(&slate, self.can_use_dandelion());
let result = self.post(&slate);
match result {
Ok(_) => {
Ok(())
@ -798,21 +767,25 @@ impl Wallet {
}
}
} else {
Err(Error::GenericError("TX finalization error".to_string()))
Err(Error::GenericError("Tx finalization error".to_string()))
};
}).unwrap();
})?;
}
Err(_) => {}
};
// Cancel transaction on error.
if ret_slate.is_none() {
cancel_tx();
return Err(Error::GenericError("Tx error".to_string()));
}
ret_slate
let tx = self.tx_by_slate(ret_slate.as_ref().unwrap())
.ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
}
/// Initialize an invoice transaction to receive amount, return request for funds sender.
pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> {
pub fn issue_invoice(&self, amount: u64) -> Result<WalletTransaction, Error> {
let args = IssueInvoiceTxArgs {
dest_acct_name: None,
amount,
@ -822,16 +795,17 @@ impl Wallet {
let slate = api.issue_invoice_tx(None, args)?;
// Create Slatepack message response.
let response = self.create_slatepack_message(&slate.clone())?;
let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info.
self.sync(false);
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok((slate, response))
let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
}
/// 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<WalletTransaction, Error> {
if let Ok(slate) = self.parse_slatepack(message) {
let config = self.get_config();
let args = InitTxArgs {
@ -846,19 +820,19 @@ impl Wallet {
api.tx_lock_outputs(None, &slate)?;
// Create Slatepack message response.
let response = self.create_slatepack_message(&slate)?;
let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info.
self.sync(false);
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(response)
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
} else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
}
}
/// 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<WalletTransaction, Error> {
if let Ok(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| {
@ -866,61 +840,47 @@ impl Wallet {
Ok(())
})?;
// Create Slatepack message response.
let response = self.create_slatepack_message(&slate)?;
let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info.
self.sync(false);
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(response)
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
} else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
}
}
/// 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) -> Result<WalletTransaction, Error> {
if let Ok(mut slate) = self.parse_slatepack(message) {
let api = Owner::new(self.instance.clone().unwrap(), None);
slate = api.finalize_tx(None, &slate)?;
// Save Slatepack message to file.
let _ = self.create_slatepack_message(&slate)?;
// Post transaction to blockchain.
let _ = self.post(&slate, dandelion);
Ok(slate)
let tx = self.post(&slate)?;
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(tx)
} else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
}
}
/// Post transaction to blockchain.
pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> {
fn post(&self, slate: &Slate) -> Result<WalletTransaction, Error> {
// Post transaction to blockchain.
let api = Owner::new(self.instance.clone().unwrap(), None);
api.post_tx(None, slate, dandelion)?;
// Setup transaction repost height, posting flag and ability to finalize.
let mut slate = slate.clone();
if slate.state == SlateState::Invoice2 {
slate.state = SlateState::Invoice3
} else if slate.state == SlateState::Standard2 {
slate.state = SlateState::Standard3
};
if let Some(tx) = self.tx_by_slate(&slate) {
let mut w_data = self.data.write();
let mut data = w_data.clone().unwrap();
let mut data_txs = data.txs.unwrap();
for t in &mut data_txs {
if t.data.id == tx.data.id {
t.repost_height = Some(data.info.last_confirmed_height);
t.posting = true;
t.can_finalize = false;
}
}
data.txs = Some(data_txs);
*w_data = Some(data);
}
// Sync local wallet info.
self.sync(false);
Ok(())
api.post_tx(None, slate, self.can_use_dandelion())?;
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
}
/// Cancel transaction.
@ -948,27 +908,7 @@ impl Wallet {
}
let instance = wallet.instance.clone().unwrap();
let _ = cancel_tx(instance, None, &None, Some(id), None);
// Setup tx status, cancelling, posting flag, and ability to finalize.
{
let mut w_data = wallet.data.write();
let mut data = w_data.clone().unwrap();
let mut data_txs = data.txs.unwrap();
let txs = data_txs.iter_mut().map(|tx| {
if tx.data.id == id {
tx.cancelling = false;
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::<Vec<WalletTransaction>>();
data.txs = Some(txs);
*w_data = Some(data);
}
// Refresh wallet info to update statuses.
sync_wallet_data(&wallet, false);
});
@ -985,7 +925,7 @@ impl Wallet {
/// Initiate wallet repair by scanning its outputs.
pub fn repair(&self) {
self.repair_needed.store(true, Ordering::Relaxed);
self.sync(true);
self.sync();
}
/// Check if wallet is repairing.
@ -1013,7 +953,7 @@ impl Wallet {
// Remove wallet db files.
let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path());
// Start sync to close thread.
wallet_delete.sync(true);
wallet_delete.sync();
// Mark wallet to reopen.
wallet_delete.set_reopen(reopen);
});
@ -1046,7 +986,7 @@ impl Wallet {
// Mark wallet as deleted.
wallet_delete.deleted.store(true, Ordering::Relaxed);
// Start sync to close thread.
wallet_delete.sync(true);
wallet_delete.sync();
});
}
@ -1268,7 +1208,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
wallet.reset_sync_attempts();
// Filter transactions for current account.
let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| {
let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| {
match wallet.get_parent_key_id() {
Ok(key) => {
tx.parent_key_id == key
@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
// Create wallet txs.
let mut new_txs: Vec<WalletTransaction> = vec![];
for tx in &filter_txs {
for tx in &account_txs {
// Setup transaction amount.
let amount = if tx.amount_debited > tx.amount_credited {
tx.amount_debited - tx.amount_credited
@ -1294,54 +1234,36 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
tx.amount_credited - tx.amount_debited
};
// Setup flag for ability to finalize transaction.
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 {
// Create slate to check existing file.
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 is_invoice {
true => SlateState::Invoice3,
_ => SlateState::Standard3
let mut finalizing = false;
let can_finalize = if unconfirmed_sent_or_received {
let initial_state = {
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()
};
// Setup posting status if we have other tx with same slate id.
let mut same_tx_posting = false;
for t in &mut new_txs {
if t.data.tx_slate_id == tx.tx_slate_id &&
tx.tx_type != t.data.tx_type {
same_tx_posting = t.posting ||
wallet.read_slatepack(&slate).is_some();
if same_tx_posting && !t.posting {
t.posting = true;
}
break;
}
}
same_tx_posting || wallet.read_slatepack(&slate).is_some()
finalizing = {
let mut slate = Slate::blank(1, false);
slate.id = tx.tx_slate_id.unwrap();
slate.state = match tx.tx_type {
TxLogEntryType::TxReceived => SlateState::Invoice3,
_ => SlateState::Standard3
};
wallet.read_slatepack(&slate).is_some()
};
initial_state && !finalizing
} else {
false
};
// Setup flag for ability to finalize transaction.
let can_finalize = if !posting && unconfirmed_sent_or_received {
// 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 confirmation, reposting height and cancelling status.
// Setup confirmation and cancelling status.
let mut conf_height = None;
let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool {
if current_empty && t.kernel_lookup_min_height.is_some() &&
@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
false
};
let mut repost_height = None;
let mut cancelling = false;
if data_txs.is_empty() {
setup_conf_height(tx, true);
@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
t.conf_height.unwrap() == 0) {
conf_height = t.conf_height;
}
repost_height = t.repost_height;
if t.cancelling &&
tx.tx_type != TxLogEntryType::TxReceivedCancelled &&
tx.tx_type != TxLogEntryType::TxSentCancelled {
@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
data: tx.clone(),
amount,
cancelling,
posting,
can_finalize,
finalizing,
conf_height,
repost_height,
from_node: !fresh_sync || from_node
});
}