diff --git a/locales/en.yml b/locales/en.yml index 0cf8dbb..e7a6261 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -8,6 +8,7 @@ close: Close change: Change show: Show delete: Delete +clear: Clear wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization @@ -52,6 +53,20 @@ wallets: wallet_checking: Checking wallet tx_loading: Loading transactions default_account: Default account + tx_sent: Sent + tx_received: Received + tx_sending: Sending + tx_receiving: Receiving + tx_confirming: Awaiting confirmation + tx_cancelling: Cancelling + tx_canceled: Canceled + tx_confirmed: Confirmed + manually: Manually + receive_paste_slatepack: 'Enter Slatepack message received from the sender to create a response:' + receive_send_slatepack: 'Send response to the sender to finalize the transaction:' + receive_slatepack_err: An error occurred during creation of the response, check input data. + response_copied: Response copied to the clipboard. + create_response: Create response recovery: Recovery repair_wallet: Repair wallet repair_desc: Check a wallet, repairing and restoring missing outputs if required. This operation will take time. @@ -107,7 +122,7 @@ network_node: hash: Hash height: Height difficulty: Difficulty - time_utc: Time (UTC) + time: Time transactions: Transactions main_pool: Main pool stem_pool: Stem pool diff --git a/locales/ru.yml b/locales/ru.yml index 3ad937a..91110e6 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -8,6 +8,7 @@ close: Закрыть change: Изменить show: Показать delete: Удалить +clear: Очистить wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения @@ -52,6 +53,20 @@ wallets: wallet_checking: Проверка кошелька tx_loading: Загрузка транзакций default_account: Стандартный аккаунт + tx_sent: Отправлено + tx_received: Получено + tx_sending: Отправка + tx_receiving: Получение + tx_confirming: Ожидает подтверждения + tx_cancelling: Отмена + tx_canceled: Отменено + tx_confirmed: Подтверждено + manually: Вручную + receive_paste_slatepack: 'Введите Slatepack сообщение, полученное от отправителя для создания ответа:' + receive_send_slatepack: 'Отправьте ответ отправителю для завершения транзакции:' + receive_slatepack_err: Во время создания ответа произошла ошибка, проверьте входные данные. + response_copied: Ответ скопирован в буфер обмена. + create_response: Создать ответ recovery: Восстановление repair_wallet: Починить кошелёк repair_desc: Проверить кошелёк, исправляя и восстанавливая недостающие выходы, если это необходимо. Эта операция займёт время. @@ -107,7 +122,7 @@ network_node: hash: Хэш height: Высота difficulty: Сложность - time_utc: Время (UTC) + time: Время transactions: Транзакции main_pool: Основной пул stem_pool: Stem пул diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 9d7c92d..cdd983c 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -73,8 +73,8 @@ impl WalletContent { .show_animated_inside(ui, show_balance, |ui| { ui.vertical_centered(|ui| { // Draw wallet tabs. - View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { - Self::account_balance_ui(ui, data.as_ref().unwrap(), &wallet.config.account); + View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.35, |ui| { + Self::account_ui(ui, data.as_ref().unwrap(), &wallet.config.account); }); }); }); @@ -120,8 +120,8 @@ impl WalletContent { } } - /// Draw wallet account balance. - fn account_balance_ui(ui: &mut egui::Ui, data: &WalletData, account: &Option) { + /// Draw wallet account content. + fn account_ui(ui: &mut egui::Ui, data: &WalletData, account: &Option) { let mut rect = ui.available_rect_before_wrap(); rect.set_height(75.0); // Draw round background. @@ -131,10 +131,6 @@ impl WalletContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Setup padding for item buttons. ui.style_mut().spacing.button_padding = egui::vec2(14.0, 0.0); - // Setup rounding for item buttons. - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(8.0); - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0); - ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0); // Draw button to add new account. View::item_button(ui, View::item_rounding(0, 2, true), PLUS, None, || { diff --git a/src/gui/views/wallets/wallet/info.rs b/src/gui/views/wallets/wallet/info.rs index 19b08ab..4be08de 100644 --- a/src/gui/views/wallets/wallet/info.rs +++ b/src/gui/views/wallets/wallet/info.rs @@ -12,17 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Margin, RichText}; +use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; use grin_core::core::amount_to_hr_string; +use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType}; use crate::gui::Colors; -use crate::gui::icons::{DOWNLOAD, GEAR_FINE, UPLOAD}; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, DOWNLOAD, FILE_TEXT, GEAR_FINE, PROHIBIT, UPLOAD, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Root, View}; use crate::gui::views::wallets::types::WalletTab; use crate::gui::views::wallets::wallet::types::WalletTabType; use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::WalletData; use crate::wallet::Wallet; /// Wallet info tab content. @@ -38,13 +39,11 @@ impl WalletTab for WalletInfo { ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { + _: &dyn PlatformCallbacks) { if WalletContent::sync_ui(ui, frame, wallet) { return; } - let data = wallet.get_data().unwrap(); - // Show wallet transactions panel. egui::CentralPanel::default() .frame(egui::Frame { @@ -60,47 +59,245 @@ impl WalletTab for WalletInfo { }) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { - View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.txs_ui(ui, &data); - }); + self.txs_ui(ui, wallet); }); - if data.txs.is_empty() { - View::center_content(ui, 96.0, |ui| { - let empty_text = t!( - "wallets.txs_empty", - "receive" => DOWNLOAD, - "send" => UPLOAD, - "settings" => GEAR_FINE - ); - ui.label(RichText::new(empty_text).size(16.0).color(Colors::INACTIVE_TEXT)); - }); - } else { - - } }); } } impl WalletInfo { /// Draw transactions content. - fn txs_ui(&self, ui: &mut egui::Ui, data: &WalletData) { - // Show awaiting confirmation amount. - let awaiting_conf = amount_to_hr_string(data.info.amount_awaiting_confirmation, false); - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.await_conf_amount"), - [false, false, false, false]); - // Show awaiting finalization amount. - let awaiting_conf = amount_to_hr_string(data.info.amount_awaiting_finalization, false); - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.await_fin_amount"), - [false, false, false, false]); - // Show locked amount. - let awaiting_conf = amount_to_hr_string(data.info.amount_locked, false); - View::rounded_box(ui, - format!("{} ツ", awaiting_conf), - t!("wallets.locked_amount"), - [false, false, true, true]); + fn txs_ui(&self, ui: &mut egui::Ui, wallet: &mut Wallet) { + let data = wallet.get_data().unwrap(); + let txs_size = data.txs.len(); + + // Show transactions info. + View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.35, |ui| { + let amount_awaiting_conf = data.info.amount_awaiting_confirmation; + let amount_awaiting_fin = data.info.amount_awaiting_finalization; + let amount_locked = data.info.amount_locked; + + // Show non-zero awaiting confirmation amount. + if amount_awaiting_conf != 0 { + let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, false); + let rounding = if amount_awaiting_fin != 0 || amount_locked != 0 { + [false, false, false, false] + } else { + [false, false, true, true] + }; + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.await_conf_amount"), + rounding); + } + + // Show non-zero awaiting finalization amount. + if amount_awaiting_fin != 0 { + let awaiting_conf = amount_to_hr_string(amount_awaiting_fin, false); + let rounding = if amount_locked != 0 { + [false, false, false, false] + } else { + [false, false, true, true] + }; + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.await_fin_amount"), + rounding); + } + + // Show non-zero locked amount. + if amount_locked != 0 { + let awaiting_conf = amount_to_hr_string(amount_locked, false); + View::rounded_box(ui, + format!("{} ツ", awaiting_conf), + t!("wallets.locked_amount"), + [false, false, true, true]); + } + + // Show message when wallet txs are empty. + if txs_size == 0 { + View::center_content(ui, 96.0, |ui| { + let empty_text = t!( + "wallets.txs_empty", + "receive" => DOWNLOAD, + "send" => UPLOAD, + "settings" => GEAR_FINE + ); + ui.label(RichText::new(empty_text).size(16.0).color(Colors::INACTIVE_TEXT)); + }); + return; + } + }); + + // Show list of transactions. + ui.add_space(3.0); + ScrollArea::vertical() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) + .id_source(Id::from("txs_content").with(wallet.config.id)) + .auto_shrink([false; 2]) + .show_rows(ui, TX_ITEM_HEIGHT, txs_size, |ui, row_range| { + ui.add_space(4.0); + View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { + for index in row_range { + let tx = data.txs.get(index).unwrap(); + // Setup item rounding. + let item_rounding = View::item_rounding(index, txs_size, false); + // Show transaction item. + tx_item_ui(ui, tx, item_rounding, data.info.last_confirmed_height, wallet); + } + }); + ui.add_space(2.0); + }); + + // for tx in &data.txs { + // if tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxSentCancelled { + // println!("tx: {}", serde_json::to_string::(tx).unwrap()); + // } + // } } +} + +/// Height of transaction list item. +const TX_ITEM_HEIGHT: f32 = 75.0; + +/// Draw transaction item. +fn tx_item_ui(ui: &mut egui::Ui, + tx: &TxLogEntry, + mut rounding: Rounding, + last_height: u64, + wallet: &mut Wallet) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.min += egui::vec2(6.0, 0.0); + rect.set_height(TX_ITEM_HEIGHT); + + // Draw round background. + let bg_rect = rect.clone(); + ui.painter().rect(bg_rect, rounding, Colors::BUTTON, View::ITEM_STROKE); + + // Setup transaction flags. + let is_canceled = tx.tx_type == TxLogEntryType::TxSentCancelled + || tx.tx_type == TxLogEntryType::TxReceivedCancelled; + let is_cancelling = wallet.is_cancelling(&tx.id); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + ui.add_space(-6.0); + // Draw button to show transaction info. + rounding.nw = 0.0; + rounding.sw = 0.0; + View::item_button(ui, rounding, FILE_TEXT, None, || { + //TODO: Show tx info + }); + + if !is_cancelling && !tx.confirmed && tx.tx_type != TxLogEntryType::TxReceivedCancelled + && tx.tx_type != TxLogEntryType::TxSentCancelled { + View::item_button(ui, Rounding::none(), PROHIBIT, Some(Colors::RED), || { + wallet.cancel(tx.id); + //TODO: Cancel tx + }); + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(12.0); + ui.vertical(|ui| { + // Setup transaction amount. + ui.add_space(3.0); + let amount = amount_to_hr_string(tx.amount_credited - tx.amount_debited, true); + let amount_text = if tx.amount_credited > tx.amount_debited { + format!("+{}", amount) + } else { + amount + }; + + // Setup amount color. + let amount_color = match tx.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::BLACK, + TxLogEntryType::TxReceived => Colors::BLACK, + TxLogEntryType::TxSent => Colors::BLACK, + TxLogEntryType::TxReceivedCancelled => Colors::TEXT, + TxLogEntryType::TxSentCancelled => Colors::TEXT, + TxLogEntryType::TxReverted => Colors::TEXT + }; + View::ellipsize_text(ui, amount_text, 18.0, amount_color); + ui.add_space(-2.0); + + // Setup transaction status text. + let status_text = if !tx.confirmed { + if wallet.is_cancelling(&tx.id) { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling")) + } else if is_canceled { + format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) + } else { + match tx.tx_type { + TxLogEntryType::TxReceived => { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_receiving")) + }, + TxLogEntryType::TxSent => { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_sending")) + }, + _ => { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) + } + } + } + } else { + let tx_height = tx.kernel_lookup_min_height.unwrap_or(0); + match tx.tx_type { + TxLogEntryType::ConfirmedCoinbase => { + format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) + }, + TxLogEntryType::TxReceived => { + if last_height - tx_height > wallet.config.min_confirmations { + format!("{} {}", ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) + } else { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_awaiting_conf")) + } + }, + TxLogEntryType::TxSent => { + if last_height - tx_height > wallet.config.min_confirmations { + format!("{} {}", ARROW_CIRCLE_DOWN, t!("wallets.tx_sent")) + } else { + format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) + } + }, + _ => format!("{} {}", ARROW_CIRCLE_UP, t!("wallets.canceled")) + } + }; + + // Setup status text color. + let status_color = match tx.tx_type { + TxLogEntryType::ConfirmedCoinbase => Colors::TEXT, + TxLogEntryType::TxReceived => if tx.confirmed { + Colors::GREEN + } else { + Colors::TEXT + }, + TxLogEntryType::TxSent => if tx.confirmed { + Colors::RED + } else { + Colors::TEXT + }, + TxLogEntryType::TxReceivedCancelled => Colors::INACTIVE_TEXT, + TxLogEntryType::TxSentCancelled => Colors::INACTIVE_TEXT, + TxLogEntryType::TxReverted => Colors::INACTIVE_TEXT, + }; + ui.label(RichText::new(status_text).size(15.0).color(status_color)); + + // Setup transaction time. + let tx_ts = if tx.confirmed && tx.confirmation_ts.is_some() { + tx.confirmation_ts.unwrap() + } else { + tx.creation_ts + }.timestamp(); + let tx_time = View::format_time(tx_ts); + let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time); + ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::GRAY)); + }); + }); + }); + }); } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/receive.rs b/src/gui/views/wallets/wallet/receive.rs index dbbbb6c..8d2c820 100644 --- a/src/gui/views/wallets/wallet/receive.rs +++ b/src/gui/views/wallets/wallet/receive.rs @@ -12,18 +12,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::Margin; +use egui::{Id, Margin, RichText, ScrollArea, Widget}; use crate::gui::Colors; +use crate::gui::icons::{ARCHIVE_BOX, BROOM, CLIPBOARD_TEXT, COPY, HAND_COINS}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::View; +use crate::gui::views::{Root, View}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; use crate::wallet::Wallet; /// Receiving tab content. -#[derive(Default)] -pub struct WalletReceive; +pub struct WalletReceive { + /// Slatepack text from sender to create response. + message_edit: String, + /// Generated Slatepack response. + response_edit: String, + /// Flag to check if there is an error happened on receive. + receive_error: bool, + /// Flag to check if response was copied to the clipboard. + response_copied: bool, +} + +impl Default for WalletReceive { + fn default() -> Self { + Self { + message_edit: "".to_string(), + response_edit: "".to_string(), + receive_error: false, + response_copied: false, + } + } +} impl WalletTab for WalletReceive { fn get_type(&self) -> WalletTabType { @@ -53,14 +73,161 @@ impl WalletTab for WalletReceive { ..Default::default() }) .show_inside(ui, |ui| { - self.receive_ui(ui, wallet); + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.receive_ui(ui, wallet, cb); + }); + }); }); } } +/// Hint for Slatepack Message input. +const RECEIVE_SLATEPACK_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK."; + impl WalletReceive { /// Draw receiving content. - pub fn receive_ui(&self, ui: &mut egui::Ui, wallet: &mut Wallet) { + pub fn receive_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + ui.add_space(2.0); + View::sub_title(ui, format!("{} {}", HAND_COINS, t!("wallets.manually"))); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(3.0); + // Setup manual sending description. + let response_empty = self.response_edit.is_empty(); + let desc_text = if response_empty { + t!("wallets.receive_paste_slatepack") + } else { + t!("wallets.receive_send_slatepack") + }; + ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT)); + ui.add_space(3.0); + + // Show Slatepack text input. + let message = if response_empty { + &mut self.message_edit + } else { + &mut self.response_edit + }; + + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(3.0); + ScrollArea::vertical() + .max_height(128.0) + .id_source(Id::from("receive_input").with(wallet.config.id)) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let message_before = message.clone(); + egui::TextEdit::multiline(message) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(response_empty) + .hint_text(RECEIVE_SLATEPACK_HINT) + .desired_width(f32::INFINITY) + .show(ui); + // Clear an error when message changed. + if &message_before != message { + self.receive_error = false; + } + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(10.0); + + // Show receiving input control buttons. + self.receive_buttons_ui(ui, wallet, cb); + } + + /// Draw manual receiving input control buttons. + fn receive_buttons_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + let field_is_empty = self.message_edit.is_empty() && self.response_edit.is_empty(); + let columns_num = if !field_is_empty { 2 } else { 1 }; + + // Draw buttons to clear/copy/paste. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(columns_num, |columns| { + let first_column_content = |ui: &mut egui::Ui| { + if !field_is_empty { + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::BUTTON, || { + self.receive_error = false; + self.response_copied = false; + self.message_edit.clear(); + self.response_edit.clear(); + }); + } else if self.message_edit.is_empty() { + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::BUTTON, || { + self.message_edit = cb.get_string_from_buffer(); + self.receive_error = false; + }); + } + }; + if columns_num == 1 { + columns[0].vertical_centered(first_column_content); + } else { + columns[0].vertical_centered_justified(first_column_content); + } + if !field_is_empty { + columns[1].vertical_centered_justified(|ui| { + if !self.message_edit.is_empty() { + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::BUTTON, || { + self.message_edit = cb.get_string_from_buffer(); + self.receive_error = false; + }); + } else if !self.response_edit.is_empty() { + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::BUTTON, || { + cb.copy_string_to_buffer(self.response_edit.clone()); + self.response_copied = true; + }); + } + }); + } + }); + }); + + // Draw button to create response. + if !self.message_edit.is_empty() && !self.receive_error { + ui.add_space(8.0); + let create_text = format!("{} {}", ARCHIVE_BOX, t!("wallets.create_response")); + View::button(ui, create_text, Colors::GOLD, || { + match wallet.receive(self.message_edit.clone()) { + Ok(response) => { + self.response_edit = response.trim().to_string(); + self.message_edit.clear(); + // Copy response to clipboard. + cb.copy_string_to_buffer(response); + self.response_copied = true; + }, + Err(_) => self.receive_error = true + } + }); + ui.add_space(8.0); + } else if self.receive_error { + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.receive_slatepack_err")) + .size(16.0) + .color(Colors::RED)); + ui.add_space(8.0); + } else if self.response_copied { + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.response_copied")) + .size(16.0) + .color(Colors::GREEN)); + ui.add_space(8.0); + } } } \ No newline at end of file diff --git a/src/wallet/config.rs b/src/wallet/config.rs index 27ad52f..9f0b43f 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -42,6 +42,8 @@ pub struct WalletConfig { pub const BASE_DIR_NAME: &'static str = "wallets"; /// Wallet configuration file name. const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; +/// Slatepacks directory name. +const SLATEPACKS_DIR_NAME: &'static str = "slatepacks"; /// Default value of minimal amount of confirmations. const MIN_CONFIRMATIONS_DEFAULT: u64 = 10; @@ -111,6 +113,16 @@ impl WalletConfig { config_path.to_str().unwrap().to_string() } + /// Get slatepacks data path for current wallet. + pub fn get_slatepacks_path(&self) -> PathBuf { + let mut slatepacks_dir = PathBuf::from(self.get_data_path()); + slatepacks_dir.push(SLATEPACKS_DIR_NAME); + if !slatepacks_dir.exists() { + let _ = fs::create_dir_all(slatepacks_dir.clone()); + } + slatepacks_dir + } + /// Save wallet config. pub fn save(&self) { let config_path = Self::get_config_file_path(self.chain_type, self.id); diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 74501e5..501d971 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -90,8 +90,8 @@ pub type WalletInstance = Arc< /// Contains wallet data to show. #[derive(Clone)] pub struct WalletData { - /// Wallet balance. + /// Wallet balance information. pub info: WalletInfo, - /// Transactions. + /// Wallet transactions. pub txs: Vec } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 4688b9f..3951a42 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -13,10 +13,13 @@ // limitations under the License. use std::{fs, thread}; +use std::collections::BTreeSet; +use std::fs::File; +use std::io::Write; use std::net::{SocketAddr, TcpListener}; use std::path::PathBuf; use std::sync::{Arc, mpsc, RwLock}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering}; use std::thread::Thread; use std::time::Duration; @@ -26,13 +29,14 @@ use grin_chain::SyncStatus; use grin_core::global; use grin_keychain::{ExtKeychain, Keychain}; use grin_util::Mutex; -use grin_util::secp::SecretKey; use grin_util::types::ZeroingString; use grin_wallet_api::Owner; +use grin_wallet_controller::command::parse_slatepack; +use grin_wallet_controller::controller; use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; -use grin_wallet_libwallet::{Error, NodeClient, StatusMessage, WalletInst, WalletLCProvider}; -use grin_wallet_libwallet::api_impl::owner::{retrieve_summary_info, retrieve_txs}; +use grin_wallet_libwallet::{AcctPathMapping, Error, NodeClient, StatusMessage, TxLogEntryType, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, retrieve_txs}; use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig}; @@ -46,7 +50,7 @@ pub struct Wallet { /// Wallet instance, initializing on wallet opening and clearing on wallet closing. instance: Option, /// [`WalletInstance`] external connection id applied after opening. - instance_ext_conn_id: Option, + instance_ext_conn_id: Arc, /// Wallet sync thread. sync_thread: Arc>>, @@ -60,7 +64,7 @@ pub struct Wallet { is_open: Arc, /// Flag to check if wallet is loading. closing: Arc, - /// Flag to check if wallet was deleted to remove it from list. + /// Flag to check if wallet was deleted to remove it from the list. deleted: Arc, /// Error on wallet loading. @@ -78,7 +82,10 @@ pub struct Wallet { /// Flag to check if wallet repairing and restoring missing outputs is needed. repair_needed: Arc, /// Wallet repair progress in percents. - repair_progress: Arc + repair_progress: Arc, + + /// Identifiers for transactions to cancel. + cancel_txs: Arc>> } /// Default Foreign API server host. @@ -92,7 +99,7 @@ impl Wallet { Self { config, instance: None, - instance_ext_conn_id: None, + instance_ext_conn_id: Arc::new(AtomicI64::new(0)), sync_thread: Arc::from(RwLock::new(None)), foreign_api_server: Arc::new(RwLock::new(None)), reopen: Arc::new(AtomicBool::new(false)), @@ -106,6 +113,7 @@ impl Wallet { sync_attempts: Arc::new(AtomicU8::new(0)), repair_needed: Arc::new(AtomicBool::new(false)), repair_progress: Arc::new(AtomicU8::new(0)), + cancel_txs: Arc::new(RwLock::new(BTreeSet::new())), } } @@ -203,7 +211,10 @@ impl Wallet { if self.sync_thread.write().unwrap().is_none() || self.instance.is_none() { let new_instance = Self::create_wallet_instance(self.config.clone())?; self.instance = Some(new_instance); - self.instance_ext_conn_id = self.config.ext_conn_id; + self.instance_ext_conn_id.store(match self.config.ext_conn_id { + None => 0, + Some(conn_id) => conn_id + }, Ordering::Relaxed); } // Open the wallet. @@ -211,7 +222,7 @@ impl Wallet { let mut wallet_lock = instance.lock(); let lc = wallet_lock.lc_provider()?; match lc.open_wallet(None, ZeroingString::from(password), false, false) { - Ok(keychain) => { + Ok(_) => { // Reset an error on opening. self.set_sync_error(false); self.reset_sync_attempts(); @@ -220,7 +231,7 @@ impl Wallet { let mut thread_w = self.sync_thread.write().unwrap(); if thread_w.is_none() { // Start wallet synchronization. - let thread = start_sync(self.clone(), keychain.clone()); + let thread = start_sync(self.clone()); *thread_w = Some(thread); } else { println!("unfreeze thread"); @@ -236,11 +247,16 @@ impl Wallet { Ok(()) } - /// Get current external connection id applied to [`WalletInstance`] - /// after opening if sync is running or take it from configuration. + /// Get external connection id applied to [`WalletInstance`] + /// after opening if sync is running or take it from config. pub fn get_current_ext_conn_id(&self) -> Option { if self.sync_thread.read().unwrap().is_some() { - self.instance_ext_conn_id + let ext_conn_id = self.instance_ext_conn_id.load(Ordering::Relaxed); + if ext_conn_id == 0 { + None + } else { + Some(ext_conn_id) + } } else { self.config.ext_conn_id } @@ -301,6 +317,27 @@ impl Wallet { let _ = lc.close_wallet(None); } + /// Create account into wallet. + pub fn create_account(&self, label: String) -> Result<(), Error> { + let mut api = Owner::new(self.instance.clone().unwrap(), None); + controller::owner_single_use(None, None, Some(&mut api), |api, m| { + api.create_account_path(m, &label)?; + println!("Account: '{}' Created!", label); + Ok(()) + }) + } + + /// Get list of accounts for the wallet. + pub fn accounts(&self) -> Vec { + let mut api = Owner::new(self.instance.clone().unwrap(), None); + let mut accounts = vec![]; + let _ = controller::owner_single_use(None, None, Some(&mut api), |api, m| { + accounts = api.accounts(m)?; + Ok(()) + }); + accounts + } + /// Set wallet reopen status. pub fn set_reopen(&self, reopen: bool) { self.reopen.store(reopen, Ordering::Relaxed); @@ -359,6 +396,79 @@ impl Wallet { r_data.clone() } + /// Receive transaction via Slatepack Message. + pub fn receive(&self, message: String) -> Result { + let mut api = Owner::new(self.instance.clone().unwrap(), None); + match parse_slatepack(&mut api, None, None, Some(message.clone())) { + Ok((mut slate, _)) => { + controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { + let account = if let Some(acc) = self.config.clone().account { + acc + } else { + "default".to_string() + }; + slate = api.receive_tx(&slate, Some(account.as_str()), None)?; + Ok(()) + })?; + let mut response = "".to_string(); + controller::owner_single_use(None, None, Some(&mut api), |api, m| { + response = api.create_slatepack_message(m, &slate, Some(0), vec![])?; + Ok(()) + })?; + + // Create a directory to which slatepack files will be output. + let mut slatepack_dir = self.config.get_slatepacks_path(); + let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state); + slatepack_dir.push(slatepack_file_name); + + // Write Slatepack response into the file. + let mut output = File::create(slatepack_dir)?; + output.write_all(response.as_bytes())?; + output.sync_all()?; + + Ok(response) + } + Err(_) => { + Err(Error::GenericError("Parsing error".to_string())) + } + } + } + + pub fn send(&self) { + + } + + /// Cancel transaction. + pub fn cancel(&mut self, id: u32) { + // Set cancelling status. + { + let mut cancelling_w = self.cancel_txs.write().unwrap(); + cancelling_w.insert(id); + } + + // Launch tx cancelling at separate thread. + let mut wallet_cancel = self.clone(); + let instance = wallet_cancel.instance.clone().unwrap(); + thread::spawn(move || { + let _ = cancel_tx(instance, None, &None, Some(id), None); + // Wake up wallet thread to update statuses. + let thread_r = wallet_cancel.sync_thread.read().unwrap(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); + } + }); + } + + /// Check if transaction is cancelling. + pub fn is_cancelling(&self, id: &u32) -> bool { + let cancelling_r = self.cancel_txs.read().unwrap(); + cancelling_r.contains(id) + } + + pub fn finalize(&self) { + + } + /// Change wallet password. pub fn change_password(&self, old: String, new: String) -> Result<(), Error> { let instance = self.instance.clone().unwrap(); @@ -441,7 +551,7 @@ const SYNC_DELAY: Duration = Duration::from_millis(60 * 1000); const SYNC_ATTEMPTS: u8 = 10; /// Launch thread to sync wallet data from node. -fn start_sync(mut wallet: Wallet, keychain: Option) -> Thread { +fn start_sync(mut wallet: Wallet) -> Thread { // Reset progress values. wallet.info_sync_progress.store(0, Ordering::Relaxed); wallet.txs_sync_progress.store(0, Ordering::Relaxed); @@ -480,7 +590,7 @@ fn start_sync(mut wallet: Wallet, keychain: Option) -> Thread { wallet.foreign_api_server.read().unwrap().is_some() }; if !api_server_exists { - match start_api_server(&mut wallet, keychain.clone()) { + match start_api_server(&mut wallet) { Ok(api_server) => { let mut api_server_w = wallet.foreign_api_server.write().unwrap(); *api_server_w = Some(api_server); @@ -492,9 +602,9 @@ fn start_sync(mut wallet: Wallet, keychain: Option) -> Thread { // Scan outputs if repair is needed or sync data if there is no error. if !wallet.sync_error() { if wallet.is_repairing() { - scan_wallet(&wallet, keychain.clone()) + scan_wallet(&wallet) } else { - sync_wallet_data(&wallet, keychain.clone()); + sync_wallet_data(&wallet); } } @@ -525,8 +635,7 @@ fn start_sync(mut wallet: Wallet, keychain: Option) -> Thread { } /// Start Foreign API server to accept txs via Tor and receive mining rewards from Stratum server. -fn start_api_server(wallet: &mut Wallet, - keychain: Option) -> Result { +fn start_api_server(wallet: &mut Wallet) -> Result { // Find free port. let free_port = (DEFAULT_FOREIGN_API_PORT..).find(|port| { return match TcpListener::bind((DEFAULT_FOREIGN_API_HOST, port.to_owned())) { @@ -545,7 +654,7 @@ fn start_api_server(wallet: &mut Wallet, // Start Foreign API server thread. let instance = wallet.instance.clone().unwrap(); let api_handler_v2 = ForeignAPIHandlerV2::new(instance, - Arc::new(Mutex::new(keychain)), + Arc::new(Mutex::new(None)), false, Mutex::new(None)); let mut router = Router::new(); @@ -567,7 +676,7 @@ fn start_api_server(wallet: &mut Wallet, } /// Retrieve [`WalletData`] from node. -fn sync_wallet_data(wallet: &Wallet, keychain: Option) { +fn sync_wallet_data(wallet: &Wallet) { println!("SYNC start, attempts: {}", wallet.get_sync_attempts()); let wallet_info = wallet.clone(); @@ -595,7 +704,7 @@ fn sync_wallet_data(wallet: &Wallet, keychain: Option) { if let Some(instance) = &wallet.instance { match retrieve_summary_info( instance.clone(), - keychain.as_ref(), + None, &Some(info_tx), true, wallet.config.min_confirmations @@ -631,7 +740,7 @@ fn sync_wallet_data(wallet: &Wallet, keychain: Option) { // Retrieve txs. match retrieve_txs( instance.clone(), - keychain.as_ref(), + None, &Some(txs_tx), true, None, @@ -647,9 +756,24 @@ fn sync_wallet_data(wallet: &Wallet, keychain: Option) { if wallet.txs_sync_progress() == 100 { // Reset attempts. wallet.reset_sync_attempts(); - // Set wallet data. + + // Setup transactions. + let mut txs = txs.1; + // Sort txs by creation date. + txs.sort_by_key(|tx| -tx.creation_ts.timestamp()); + // Update txs statuses. + for tx in &txs { + if tx.tx_type == TxLogEntryType::TxSentCancelled + || tx.tx_type == TxLogEntryType::TxReceivedCancelled { + // Remove cancelling status. + let mut cancel_w = wallet.cancel_txs.write().unwrap(); + cancel_w.remove(&tx.id); + } + } + + // Update wallet data. let mut w_data = wallet.data.write().unwrap(); - *w_data = Some(WalletData { info: info.1, txs: txs.1 }); + *w_data = Some(WalletData { info: info.1, txs }); return; } } @@ -691,7 +815,7 @@ fn sync_wallet_data(wallet: &Wallet, keychain: Option) { } /// Scan wallet's outputs, repairing and restoring missing outputs if required. -fn scan_wallet(wallet: &Wallet, keychain: Option) { +fn scan_wallet(wallet: &Wallet) { println!("repair the wallet"); let (info_tx, info_rx) = mpsc::channel::(); // Update scan progress at separate thread. @@ -716,7 +840,7 @@ fn scan_wallet(wallet: &Wallet, keychain: Option) { // Start wallet scanning. let api = Owner::new(wallet.instance.clone().unwrap(), Some(info_tx)); - match api.scan(keychain.as_ref(), Some(1), false) { + match api.scan(None, Some(1), false) { Ok(()) => { println!("repair was complete"); // Set sync error if scanning was not complete and wallet is open.