Open .slatepack file with the app #13

Merged
ardocrat merged 28 commits from slatepack_ext_file into master 2024-09-16 19:08:27 +03:00
17 changed files with 2051 additions and 2438 deletions
Showing only changes of commit 21ecf200b8 - Show all commits

View file

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

View file

@ -148,7 +148,7 @@ impl WalletContent {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// Draw wallet tabs. // Draw wallet tabs.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { 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) => { QrScanResult::Slatepack(message) => {
// Redirect to messages to handle parsed message. // Redirect to messages to handle parsed message.
let mut messages = let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string())); WalletMessages::new(Some(message.to_string()));
messages.parse_message(wallet); messages.parse_message(wallet);
modal.close(); modal.close();
self.current_tab = Box::new(messages); self.current_tab = Box::new(messages);
@ -477,8 +477,7 @@ impl WalletContent {
QrScanResult::Address(receiver) => { QrScanResult::Address(receiver) => {
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 { if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
// Redirect to send amount with Tor. // Redirect to send amount with Tor.
let addr = wallet.slatepack_address().unwrap(); let mut transport = WalletTransport::default();
let mut transport = WalletTransport::new(addr.clone());
modal.close(); modal.close();
transport.show_send_tor_modal(cb, Some(receiver.to_string())); transport.show_send_tor_modal(cb, Some(receiver.to_string()));
self.current_tab = Box::new(transport); self.current_tab = Box::new(transport);
@ -528,7 +527,7 @@ impl WalletContent {
} }
/// Draw tab buttons in the bottom of the screen. /// 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| { ui.scope(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); 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; let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
self.current_tab = Box::new( self.current_tab = Box::new(
WalletMessages::new(wallet.can_use_dandelion(), None) WalletMessages::new(None)
); );
}); });
}); });
columns[2].vertical_centered_justified(|ui| { columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
let addr = wallet.slatepack_address().unwrap(); self.current_tab = Box::new(WalletTransport::default());
self.current_tab = Box::new(WalletTransport::new(addr));
}); });
}); });
columns[3].vertical_centered_justified(|ui| { 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, new_pass_edit: String,
/// Minimum confirmations number value. /// Minimum confirmations number value.
min_confirmations_edit: String min_confirmations_edit: String,
} }
/// Identifier for wallet name [`Modal`]. /// Identifier for wallet name [`Modal`].
@ -54,25 +54,26 @@ impl Default for CommonSettings {
wrong_pass: false, wrong_pass: false,
old_pass_edit: "".to_string(), old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(), new_pass_edit: "".to_string(),
min_confirmations_edit: "".to_string() min_confirmations_edit: "".to_string(),
} }
} }
} }
impl CommonSettings { impl CommonSettings {
/// Draw common wallet settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container. // Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let wallet_name = wallet.get_config().name; let config = wallet.get_config();
// Show wallet name. // Show wallet name.
ui.add_space(2.0); ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.name")) ui.label(RichText::new(t!("wallets.name"))
.size(16.0) .size(16.0)
.color(Colors::gray())); .color(Colors::gray()));
ui.add_space(2.0); ui.add_space(2.0);
ui.label(RichText::new(wallet_name.clone()) ui.label(RichText::new(&config.name)
.size(16.0) .size(16.0)
.color(Colors::white_or_black(true))); .color(Colors::white_or_black(true)));
ui.add_space(8.0); ui.add_space(8.0);
@ -80,7 +81,7 @@ impl CommonSettings {
// Show wallet name setup. // Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change")); let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::button(), || { View::button(ui, name_text, Colors::button(), || {
self.name_edit = wallet_name; self.name_edit = config.name;
// Show wallet name modal. // Show wallet name modal.
Modal::new(NAME_EDIT_MODAL) Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -118,10 +119,9 @@ impl CommonSettings {
ui.add_space(6.0); ui.add_space(6.0);
// Show minimum amount of confirmations value setup. // Show minimum amount of confirmations value setup.
let min_confirmations = wallet.get_config().min_confirmations; let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations);
View::button(ui, min_conf_text, Colors::button(), || { 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. // Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -131,8 +131,15 @@ impl CommonSettings {
}); });
ui.add_space(12.0); 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()); 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| { 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()) { match wallet.get_recovery(self.pass_edit.clone()) {
Ok(phrase) => { Ok(phrase) => {
self.wrong_pass = false; self.wrong_pass = false;
@ -243,6 +243,12 @@ impl RecoverySettings {
self.wrong_pass = true; 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 grin_wallet_libwallet::TxLogEntryType;
use crate::gui::Colors; 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::platform::PlatformCallbacks;
use crate::gui::views::{Modal, PullToRefresh, Content, View}; use crate::gui::views::{Modal, PullToRefresh, Content, View};
use crate::gui::views::types::ModalPosition; use crate::gui::views::types::ModalPosition;
@ -226,19 +226,6 @@ impl WalletTransactions {
.show(); .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() { if refresh_resp.should_refresh() {
self.manual_sync = Some(now); self.manual_sync = Some(now);
if !wallet.syncing() { if !wallet.syncing() {
wallet.sync(true); wallet.sync();
} }
} }
} }
@ -339,7 +326,7 @@ impl WalletTransactions {
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled { if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if tx.posting { } else if tx.finalizing {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing"))
} else { } else {
if tx.cancelling { if tx.cancelling {
@ -431,8 +418,7 @@ impl WalletTransactions {
/// Show transaction information [`Modal`]. /// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) { fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) {
let mut modal = WalletTransactionModal::new(wallet, tx); let modal = WalletTransactionModal::new(wallet, tx, finalize);
modal.show_finalization = finalize;
self.tx_info_content = Some(modal); self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL) Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)

View file

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

View file

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