wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter
This commit is contained in:
parent
c8bca08bdc
commit
21ecf200b8
17 changed files with 2051 additions and 2438 deletions
|
@ -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();
|
||||||
|
|
|
@ -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
541
src/gui/views/wallets/wallet/messages/content.rs
Normal file
541
src/gui/views/wallets/wallet/messages/content.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal file
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal 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;
|
260
src/gui/views/wallets/wallet/messages/request.rs
Normal file
260
src/gui/views/wallets/wallet/messages/request.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
397
src/gui/views/wallets/wallet/transport/content.rs
Normal file
397
src/gui/views/wallets/wallet/transport/content.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal file
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal 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;
|
357
src/gui/views/wallets/wallet/transport/send.rs
Normal file
357
src/gui/views/wallets/wallet/transport/send.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal file
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue