wallet + config + ui: load wallet info, wallet connections and app config refactoring, check external connections availability, config subdir argument format, item button color, radio buttons stroke, chain type selection from connections, update translations

This commit is contained in:
ardocrat 2023-08-09 02:22:16 +03:00
parent 8ed6bc0578
commit 076a692e1b
27 changed files with 647 additions and 856 deletions

View file

@ -47,6 +47,9 @@ network:
autorun: Autorun autorun: Autorun
disabled_server: 'Enable integrated node or add another connection method by pressing %{dots} in the top-left corner of the screen.' disabled_server: 'Enable integrated node or add another connection method by pressing %{dots} in the top-left corner of the screen.'
no_ips: There are no available IP addresses on your system, server cannot be started, check your network connectivity. no_ips: There are no available IP addresses on your system, server cannot be started, check your network connectivity.
available: Available
not_available: Not available
availability_check: Availability check
sync_status: sync_status:
node_restarting: Node is restarting node_restarting: Node is restarting
node_down: Node is down node_down: Node is down

View file

@ -47,6 +47,9 @@ network:
autorun: Автозапуск autorun: Автозапуск
disabled_server: 'Включите встроенный узел или добавьте другой способ подключения, нажав %{dots} в левом-верхнем углу экрана.' disabled_server: 'Включите встроенный узел или добавьте другой способ подключения, нажав %{dots} в левом-верхнем углу экрана.'
no_ips: В вашей системе отсутствуют доступные IP адреса, запуск сервера невозможен, проверьте ваше подключение к сети. no_ips: В вашей системе отсутствуют доступные IP адреса, запуск сервера невозможен, проверьте ваше подключение к сети.
available: Доступно
not_available: Недоступно
availability_check: Проверка доступности
sync_status: sync_status:
node_restarting: Узел перезапускается node_restarting: Узел перезапускается
node_down: Узел выключен node_down: Узел выключен

127
src/config.rs Normal file
View file

@ -0,0 +1,127 @@
// 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 grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize};
use crate::node::NodeConfig;
use crate::Settings;
use crate::wallet::ConnectionsConfig;
/// Application configuration.
#[derive(Serialize, Deserialize)]
pub struct AppConfig {
/// Run node server on startup.
pub auto_start_node: bool,
/// Chain type for node and wallets.
pub(crate) chain_type: ChainTypes,
/// Flag to show wallet list at dual panel wallets mode.
show_wallets_at_dual_panel: bool,
/// Flag to show all connections at network panel or integrated node info.
show_connections_network_panel: bool,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
auto_start_node: false,
chain_type: ChainTypes::default(),
show_wallets_at_dual_panel: true,
show_connections_network_panel: false,
}
}
}
impl AppConfig {
/// Application configuration file name.
pub const FILE_NAME: &'static str = "app.toml";
/// Save application configuration to the file.
pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(Self::FILE_NAME, None));
}
/// Change global [`ChainTypes`] and load new [`NodeConfig`].
pub fn change_chain_type(chain_type: &ChainTypes) {
let current_chain_type = Self::chain_type();
if current_chain_type != *chain_type {
// Save chain type at app config.
{
let mut w_app_config = Settings::app_config_to_update();
w_app_config.chain_type = *chain_type;
w_app_config.save();
}
// Load node configuration for selected chain type.
{
let mut w_node_config = Settings::node_config_to_update();
let node_config = NodeConfig::for_chain_type(chain_type);
w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers;
}
// Load connections configuration
{
let mut w_node_config = Settings::conn_config_to_update();
*w_node_config = ConnectionsConfig::for_chain_type(chain_type);
}
}
}
/// Get current [`ChainTypes`] for node and wallets.
pub fn chain_type() -> ChainTypes {
let r_config = Settings::app_config_to_read();
r_config.chain_type
}
/// Check if integrated node is starting with application.
pub fn autostart_node() -> bool {
let r_config = Settings::app_config_to_read();
r_config.auto_start_node
}
/// Toggle integrated node autostart.
pub fn toggle_node_autostart() {
let autostart = Self::autostart_node();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.auto_start_node = !autostart;
w_app_config.save();
}
/// Check if it's needed to show wallet list at dual panel wallets mode.
pub fn show_wallets_at_dual_panel() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_wallets_at_dual_panel
}
/// Toggle flag to show wallet list at dual panel wallets mode.
pub fn toggle_show_wallets_at_dual_panel() {
let show = Self::show_wallets_at_dual_panel();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.show_wallets_at_dual_panel = !show;
w_app_config.save();
}
/// Check if it's needed to show all connections or integrated node info at network panel.
pub fn show_connections_network_panel() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_connections_network_panel
}
/// Toggle flag to show all connections or integrated node info at network panel.
pub fn toggle_show_connections_network_panel() {
let show = Self::show_connections_network_panel();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.show_connections_network_panel = !show;
w_app_config.save();
}
}

View file

@ -17,25 +17,25 @@ use url::Url;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLAY, STOP, TRASH, X_CIRCLE}; use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, PENCIL, POWER, TRASH, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, NodeSetup, View};
use crate::gui::views::types::{ModalContainer, ModalPosition}; use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection}; use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Network connections content. /// Network connections content.
pub struct ConnectionsContent { pub struct ConnectionsContent {
/// Flag to check if modal was just opened. /// Flag to check if [`Modal`] was just opened to focus on input field.
first_modal_launch: bool, first_modal_launch: bool,
/// External connection URL value for [`Modal`]. /// External connection URL value for [`Modal`].
ext_node_url_edit: String, ext_node_url_edit: String,
/// External connection API secret value for [`Modal`]. /// External connection API secret value for [`Modal`].
ext_node_secret_edit: String, ext_node_secret_edit: String,
/// Flag to show URL format error. /// Flag to show URL format error at [`Modal`].
ext_node_url_error: bool, ext_node_url_error: bool,
/// Flag to check if existing connection is editing. /// Editing external connection identifier for [`Modal`].
edit_ext_conn: Option<ExternalConnection>, ext_conn_id_edit: Option<i64>,
/// [`Modal`] identifiers allowed at this ui container. /// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str> modal_ids: Vec<&'static str>
@ -43,12 +43,15 @@ pub struct ConnectionsContent {
impl Default for ConnectionsContent { impl Default for ConnectionsContent {
fn default() -> Self { fn default() -> Self {
if AppConfig::show_connections_network_panel() {
ExternalConnection::start_ext_conn_availability_check();
}
Self { Self {
first_modal_launch: true, first_modal_launch: true,
ext_node_url_edit: "".to_string(), ext_node_url_edit: "".to_string(),
ext_node_secret_edit: "".to_string(), ext_node_secret_edit: "".to_string(),
ext_node_url_error: false, ext_node_url_error: false,
edit_ext_conn: None, ext_conn_id_edit: None,
modal_ids: vec![ modal_ids: vec![
Self::NETWORK_EXT_CONNECTION_MODAL Self::NETWORK_EXT_CONNECTION_MODAL
] ]
@ -81,20 +84,30 @@ impl ConnectionsContent {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, frame, cb); self.current_modal_ui(ui, frame, cb);
// Show network type selection.
let saved_chain_type = AppConfig::chain_type();
NodeSetup::chain_type_ui(ui);
// Start external connections availability check on network type change.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::start_ext_conn_availability_check();
}
ui.add_space(5.0);
// Show integrated node info content. // Show integrated node info content.
Self::integrated_node_item_ui(ui); Self::integrated_node_item_ui(ui);
ui.add_space(8.0); let ext_conn_list = ConnectionsConfig::ext_conn_list();
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::GRAY)); if !ext_conn_list.is_empty() {
ui.add_space(6.0); ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::GRAY));
// Show external connections. ui.add_space(6.0);
let ext_conn_list = ConnectionsConfig::external_connections(); // Show external connections.
for (index, conn) in ext_conn_list.iter().enumerate() { for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
// Draw connection list item. // Draw connection list item.
self.ext_conn_item_ui(ui, conn, index, ext_conn_list.len(), cb); self.ext_conn_item_ui(ui, conn, index, ext_conn_list.len(), cb);
}); });
}
} }
} }
@ -115,26 +128,26 @@ impl ConnectionsContent {
ui.style_mut().visuals.widgets.hovered.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); ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0);
// Draw button to show integrated node. // Draw button to show integrated node info.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, || { View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
AppConfig::toggle_show_connections_network_panel(); AppConfig::toggle_show_connections_network_panel();
}); });
if !Node::is_running() { if !Node::is_running() {
// Draw button to start integrated node. // Draw button to start integrated node.
View::item_button(ui, Rounding::none(), PLAY, || { View::item_button(ui, Rounding::none(), POWER, Some(Colors::GREEN), || {
Node::start(); Node::start();
}); });
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
// Show button to open closed wallet. // Draw button to open closed wallet.
View::item_button(ui, Rounding::none(), STOP, || { View::item_button(ui, Rounding::none(), POWER, Some(Colors::RED), || {
Node::stop(false); Node::stop(false);
}); });
} }
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(7.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
ui.label(RichText::new(t!("network.node")) ui.label(RichText::new(t!("network.node"))
@ -157,7 +170,6 @@ impl ConnectionsContent {
}; };
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text()); let status_text = format!("{} {}", status_icon, Node::get_sync_status_text());
ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY)); ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY));
ui.add_space(4.0);
}) })
}); });
}); });
@ -172,28 +184,28 @@ impl ConnectionsContent {
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Setup layout size. // Setup layout size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(42.0); rect.set_height(53.0);
// Draw round background. // Draw round background.
let bg_rect = rect.clone(); let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false); let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::FILL, View::ITEM_STROKE); ui.painter().rect(bg_rect, item_rounding, Colors::FILL_DARK, View::ITEM_STROKE);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw buttons for non-default connections. // Draw buttons for non-default connections.
if conn.url != ExternalConnection::DEFAULT_EXTERNAL_NODE_URL { if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
let button_rounding = View::item_rounding(index, len, true); let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, TRASH, || { View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_external_connection(conn); ConnectionsConfig::remove_ext_conn(conn.id);
}); });
View::item_button(ui, Rounding::none(), PENCIL, || { View::item_button(ui, Rounding::none(), PENCIL, None, || {
// Setup values for Modal. // Setup values for Modal.
self.first_modal_launch = true; self.first_modal_launch = true;
self.ext_node_url_edit = conn.url.clone(); self.ext_node_url_edit = conn.url.clone();
self.ext_node_secret_edit = conn.secret.clone().unwrap_or("".to_string()); self.ext_node_secret_edit = conn.secret.clone().unwrap_or("".to_string());
self.ext_node_url_error = false; self.ext_node_url_error = false;
self.edit_ext_conn = Some(conn.clone()); self.ext_conn_id_edit = Some(conn.id);
// Show modal. // Show modal.
Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL) Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -206,11 +218,29 @@ impl ConnectionsContent {
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0); ui.add_space(6.0);
// Draw connections URL. ui.vertical(|ui| {
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url); // Draw connections URL.
ui.label(RichText::new(conn_text) ui.add_space(4.0);
.color(Colors::TEXT_BUTTON) let conn_text = format!("{} {}", COMPUTER_TOWER, conn.url);
.size(16.0)); ui.label(RichText::new(conn_text)
.color(Colors::TEXT)
.size(15.0));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY));
ui.add_space(3.0);
});
}); });
}); });
}); });
@ -223,7 +253,7 @@ impl ConnectionsContent {
self.ext_node_url_edit = "".to_string(); self.ext_node_url_edit = "".to_string();
self.ext_node_secret_edit = "".to_string(); self.ext_node_secret_edit = "".to_string();
self.ext_node_url_error = false; self.ext_node_url_error = false;
self.edit_ext_conn = None; self.ext_conn_id_edit = None;
// Show modal. // Show modal.
Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL) Modal::new(Self::NETWORK_EXT_CONNECTION_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -246,7 +276,7 @@ impl ConnectionsContent {
// Draw node URL text edit. // Draw node URL text edit.
let url_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit) let url_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit)
.id(Id::from(modal.id).with("node_url_edit")) .id(Id::from(modal.id).with(self.ext_conn_id_edit))
.font(TextStyle::Heading) .font(TextStyle::Heading)
.desired_width(ui.available_width()) .desired_width(ui.available_width())
.cursor_at_end(true) .cursor_at_end(true)
@ -315,13 +345,12 @@ impl ConnectionsContent {
}; };
// Update or create new connections. // Update or create new connections.
let ext_conn = ExternalConnection::new(url.clone(), secret); let mut ext_conn = ExternalConnection::new(url, secret);
if let Some(edit_conn) = self.edit_ext_conn.clone() { if let Some(id) = self.ext_conn_id_edit {
ConnectionsConfig::update_external_connection(edit_conn, ext_conn); ext_conn.id = id;
self.edit_ext_conn = None;
} else {
ConnectionsConfig::add_external_connection(ext_conn);
} }
ConnectionsConfig::add_ext_conn(ext_conn);
self.ext_conn_id_edit = None;
// Close modal. // Close modal.
cb.hide_keyboard(); cb.hide_keyboard();

View file

@ -23,6 +23,7 @@ use crate::gui::views::{ConnectionsContent, NetworkMetrics, NetworkMining, Netwo
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::types::TitleType; use crate::gui::views::types::TitleType;
use crate::node::Node; use crate::node::Node;
use crate::wallet::ExternalConnection;
/// Network content. /// Network content.
pub struct NetworkContent { pub struct NetworkContent {
@ -178,6 +179,9 @@ impl NetworkContent {
if !show_connections { if !show_connections {
View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || {
AppConfig::toggle_show_connections_network_panel(); AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() {
ExternalConnection::start_ext_conn_availability_check();
}
}); });
} else { } else {
View::title_button(ui, PLUS_CIRCLE, || { View::title_button(ui, PLUS_CIRCLE, || {

View file

@ -108,7 +108,7 @@ impl NodeSetup {
ui.add_space(4.0); ui.add_space(4.0);
// Show chain type setup. // Show chain type setup.
self.chain_type_ui(ui); Self::chain_type_ui(ui);
// Show loading indicator or controls to stop/start/restart node. // Show loading indicator or controls to stop/start/restart node.
if Node::is_stopping() || Node::is_restarting() || Node::is_starting() { if Node::is_stopping() || Node::is_restarting() || Node::is_starting() {
@ -220,7 +220,7 @@ impl NodeSetup {
} }
/// Draw [`ChainTypes`] setup content. /// Draw [`ChainTypes`] setup content.
fn chain_type_ui(&mut self, ui: &mut egui::Ui) { pub fn chain_type_ui(ui: &mut egui::Ui) {
let saved_chain_type = AppConfig::chain_type(); let saved_chain_type = AppConfig::chain_type();
let mut selected_chain_type = saved_chain_type; let mut selected_chain_type = saved_chain_type;

View file

@ -908,7 +908,7 @@ fn peer_item_ui(ui: &mut egui::Ui,
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw delete button for non-default seed peers. // Draw delete button for non-default seed peers.
if peer_type != &PeerType::DefaultSeed { if peer_type != &PeerType::DefaultSeed {
View::item_button(ui, View::item_rounding(index, len, true), TRASH, || { View::item_button(ui, View::item_rounding(index, len, true), TRASH, None, || {
match peer_type { match peer_type {
PeerType::CustomSeed => { PeerType::CustomSeed => {
NodeConfig::remove_custom_seed(peer_addr); NodeConfig::remove_custom_seed(peer_addr);

View file

@ -104,6 +104,8 @@ impl View {
/// Title button with transparent background fill color, contains only icon. /// Title button with transparent background fill color, contains only icon.
pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) { pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) {
ui.scope(|ui| { ui.scope(|ui| {
// Disable stroke when inactive.
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
// Setup stroke around title buttons on click. // Setup stroke around title buttons on click.
ui.style_mut().visuals.widgets.hovered.bg_stroke = Self::HOVER_STROKE; ui.style_mut().visuals.widgets.hovered.bg_stroke = Self::HOVER_STROKE;
ui.style_mut().visuals.widgets.active.bg_stroke = Self::DEFAULT_STROKE; ui.style_mut().visuals.widgets.active.bg_stroke = Self::DEFAULT_STROKE;
@ -173,8 +175,12 @@ impl View {
} }
} }
/// Draw list item [`Button`] with given vertical padding and rounding on left and right sides. /// Draw list item [`Button`] with provided rounding.
pub fn item_button(ui: &mut egui::Ui, r: Rounding, icon: &'static str, action: impl FnOnce()) { pub fn item_button(ui: &mut egui::Ui,
rounding: Rounding,
text: &'static str,
color: Option<Color32>,
action: impl FnOnce()) {
// Setup button size. // Setup button size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_width(32.0); rect.set_width(32.0);
@ -193,9 +199,12 @@ impl View {
ui.visuals_mut().widgets.hovered.bg_stroke = Self::HOVER_STROKE; ui.visuals_mut().widgets.hovered.bg_stroke = Self::HOVER_STROKE;
ui.visuals_mut().widgets.active.bg_stroke = Self::ITEM_STROKE; ui.visuals_mut().widgets.active.bg_stroke = Self::ITEM_STROKE;
// Setup button text color.
let text_color = if let Some(c) = color { c } else { Colors::ITEM_BUTTON };
// Show button. // Show button.
let br = Button::new(RichText::new(icon).size(20.0).color(Colors::ITEM_BUTTON)) let br = Button::new(RichText::new(text).size(20.0).color(text_color))
.rounding(r) .rounding(rounding)
.min_size(button_size) .min_size(button_size)
.ui(ui); .ui(ui);
br.surrender_focus(); br.surrender_focus();

View file

@ -24,7 +24,7 @@ use crate::gui::views::{Modal, Root, TitlePanel, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TitleType}; use crate::gui::views::types::{ModalContainer, ModalPosition, TitleType};
use crate::gui::views::wallets::creation::WalletCreation; use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, Wallets}; use crate::wallet::{ConnectionsConfig, ExternalConnection, Wallet, Wallets};
/// Wallets content. /// Wallets content.
pub struct WalletsContent { pub struct WalletsContent {
@ -140,9 +140,7 @@ impl WalletsContent {
} }
if create_wallet || !show_wallet { if create_wallet || !show_wallet {
// Show wallet creation content. // Show wallet creation content.
self.creation_content.ui(ui, frame, cb, |mut wallet| { self.creation_content.ui(ui, frame, cb, |wallet| {
// Load the wallet.
Wallets::load(&mut wallet);
// Add created wallet to list. // Add created wallet to list.
self.wallets.add(wallet); self.wallets.add(wallet);
}); });
@ -365,17 +363,23 @@ impl WalletsContent {
if !wallet.is_open() { if !wallet.is_open() {
// Show button to open closed wallet. // Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, || { View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id)); self.wallets.select(Some(id));
self.show_open_wallet_modal(cb); self.show_open_wallet_modal(cb);
}); });
} else if !is_selected { } else {
// Show button to select opened wallet. if !is_selected {
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, || { // Show button to select opened wallet.
self.wallets.select(Some(id)); View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
}); self.wallets.select(Some(id));
});
}
// Show button to close opened wallet. // Show button to close opened wallet.
View::item_button(ui, Rounding::none(), LOCK_KEY, || { View::item_button(ui, if !is_selected {
Rounding::none()
} else {
View::item_rounding(0, 1, true)
}, LOCK_KEY, None, || {
let _ = wallet.close(); let _ = wallet.close();
}); });
} }
@ -390,9 +394,12 @@ impl WalletsContent {
View::ellipsize_text(ui, wallet.config.name.to_owned(), 18.0, name_color); View::ellipsize_text(ui, wallet.config.name.to_owned(), 18.0, name_color);
// Setup wallet connection text. // Setup wallet connection text.
let external_url = &wallet.config.external_node_url; let conn_text = if let Some(id) = wallet.config.ext_conn_id {
let conn_text = if let Some(url) = external_url { let ext_conn_url = match ConnectionsConfig::ext_conn(id) {
format!("{} {}", GLOBE_SIMPLE, url) None => ExternalConnection::DEFAULT_MAIN_URL.to_string(),
Some(ext_conn) => ext_conn.url
};
format!("{} {}", GLOBE_SIMPLE, ext_conn_url)
} else { } else {
format!("{} {}", COMPUTER_TOWER, t!("network.node")) format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}; };
@ -525,7 +532,7 @@ impl WalletsContent {
if self.pass_edit.is_empty() { if self.pass_edit.is_empty() {
return; return;
} }
match self.wallets.open_selected(self.pass_edit.clone()) { match self.wallets.launch_selected(self.pass_edit.clone()) {
Ok(_) => { Ok(_) => {
// Clear values. // Clear values.
self.pass_edit = "".to_string(); self.pass_edit = "".to_string();

View file

@ -271,8 +271,8 @@ impl WalletCreation {
let name = self.name_edit.clone(); let name = self.name_edit.clone();
let pass = self.pass_edit.clone(); let pass = self.pass_edit.clone();
let phrase = self.mnemonic_setup.mnemonic.get_phrase(); let phrase = self.mnemonic_setup.mnemonic.get_phrase();
let ext_conn = self.network_setup.get_ext_conn_url(); let conn_method = &self.network_setup.method;
let wallet = Wallet::create(name, pass.clone(), phrase, ext_conn).unwrap(); let wallet = Wallet::create(name, pass.clone(), phrase, conn_method).unwrap();
// Open created wallet. // Open created wallet.
wallet.open(pass).unwrap(); wallet.open(pass).unwrap();
// Pass created wallet to callback. // Pass created wallet to callback.

View file

@ -20,13 +20,13 @@ use crate::gui::icons::{GLOBE, GLOBE_SIMPLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::types::{ModalContainer, ModalPosition}; use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::wallets::setup::ConnectionMethod;
use crate::wallet::{ConnectionsConfig, ExternalConnection}; use crate::wallet::{ConnectionsConfig, ExternalConnection};
use crate::wallet::types::ConnectionMethod;
/// Wallet node connection method setup content. /// Wallet node connection method setup content.
pub struct ConnectionSetup { pub struct ConnectionSetup {
/// Selected connection method. /// Selected connection method.
method: ConnectionMethod, pub method: ConnectionMethod,
/// Flag to check if modal was just opened. /// Flag to check if modal was just opened.
first_modal_launch: bool, first_modal_launch: bool,
@ -82,14 +82,6 @@ impl ConnectionSetup {
// Self { method: ConnectionMethod::Integrated } // Self { method: ConnectionMethod::Integrated }
// } // }
/// Get external connection URL.
pub fn get_ext_conn_url(&self) -> Option<String> {
match &self.method {
ConnectionMethod::Integrated => None,
ConnectionMethod::External(url) => Some(url.clone())
}
}
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, frame, cb); self.current_modal_ui(ui, frame, cb);
@ -122,10 +114,10 @@ impl ConnectionSetup {
ui.add_space(12.0); ui.add_space(12.0);
// Show external nodes URLs selection. // Show external nodes URLs selection.
for conn in ConnectionsConfig::external_connections() { for conn in ConnectionsConfig::ext_conn_list() {
View::radio_value(ui, View::radio_value(ui,
&mut self.method, &mut self.method,
ConnectionMethod::External(conn.url.clone()), ConnectionMethod::External(conn.id),
conn.url); conn.url);
ui.add_space(12.0); ui.add_space(12.0);
} }
@ -162,7 +154,7 @@ impl ConnectionSetup {
// Draw node URL text edit. // Draw node URL text edit.
let url_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit) let url_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit)
.id(Id::from(modal.id).with("node_url_edit")) .id(Id::from(modal.id))
.font(TextStyle::Heading) .font(TextStyle::Heading)
.desired_width(ui.available_width()) .desired_width(ui.available_width())
.cursor_at_end(true) .cursor_at_end(true)
@ -230,10 +222,10 @@ impl ConnectionSetup {
Some(self.ext_node_secret_edit.to_owned()) Some(self.ext_node_secret_edit.to_owned())
}; };
let ext_conn = ExternalConnection::new(url.clone(), secret); let ext_conn = ExternalConnection::new(url.clone(), secret);
ConnectionsConfig::add_external_connection(ext_conn); ConnectionsConfig::add_ext_conn(ext_conn.clone());
// Set added connection as current. // Set added connection as current.
self.method = ConnectionMethod::External(url); self.method = ConnectionMethod::External(ext_conn.id);
// Close modal. // Close modal.
cb.hide_keyboard(); cb.hide_keyboard();

View file

@ -13,7 +13,4 @@
// limitations under the License. // limitations under the License.
mod connection; mod connection;
pub use connection::ConnectionSetup; pub use connection::ConnectionSetup;
mod types;
pub use types::*;

View file

@ -1,22 +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.
/// Wallet node connection method type.
#[derive(PartialEq)]
pub enum ConnectionMethod {
/// Integrated node connection.
Integrated,
/// External node connection.
External(String)
}

View file

@ -15,13 +15,13 @@
use egui::{Margin, RichText}; use egui::{Margin, RichText};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{DOWNLOAD, UPLOAD, WALLET, WRENCH}; use crate::gui::icons::{DOWNLOAD, POWER, UPLOAD, WALLET, WRENCH};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View; use crate::gui::views::{Root, View};
use crate::gui::views::wallets::{WalletInfo, WalletReceive, WalletSend, WalletSettings}; use crate::gui::views::wallets::{WalletInfo, WalletReceive, WalletSend, WalletSettings};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::node::Node; use crate::node::Node;
use crate::wallet::{Wallet, Wallets}; use crate::wallet::Wallet;
/// Selected and opened wallet content. /// Selected and opened wallet content.
pub struct WalletContent { pub struct WalletContent {
@ -71,11 +71,12 @@ impl WalletContent {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
self.current_tab.ui(ui, frame, wallet, cb);
}); });
// Refresh content after delay for loaded wallet. // Refresh content after delay for loaded wallet.
if wallet.is_loaded() { if wallet.get_info().is_some() {
ui.ctx().request_repaint_after(Wallets::INFO_UPDATE_DELAY); ui.ctx().request_repaint_after(Wallet::INFO_UPDATE_DELAY);
} else { } else {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
@ -117,34 +118,48 @@ impl WalletContent {
} }
/// Content to draw when wallet is loading. /// Content to draw when wallet is loading.
fn loading_ui(ui: &mut egui::Ui, wallet: &Wallet) { pub fn show_loading_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet) -> bool {
if wallet.config.external_node_url.is_none() && !Node::is_running() { if wallet.config.ext_conn_id.is_none() && !Node::is_running() {
let dual_panel_root = Root::is_dual_panel_mode(frame);
View::center_content(ui, if !dual_panel_root { 162.0 } else { 96.0 }, |ui| {
let text = t!("wallets.enable_node", "settings" => WRENCH);
ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT));
// Show button to enable integrated node at non-dual root panel mode.
if !dual_panel_root {
ui.add_space(8.0);
let enable_node_text = format!("{} {}", POWER, t!("network.enable_node"));
View::button(ui, enable_node_text, Colors::GOLD, || {
Node::start();
});
}
});
return true;
} else { } else {
if let Some(error) = &wallet.loading_error { if wallet.get_info().is_none() || wallet.loading_progress() < 100 {
// View::center_content(ui, 162.0, |ui| { if let Some(error) = &wallet.loading_error {
// let text = t!("wallets.enable_node", "settings" => WRENCH); println!("e: {}", error);
// View::big_loading_spinner(ui); let text = t!("wallets.wallet_loading_err");
// ui.add_space(18.0);
// let text = if wallet.loading_progress == 0 {
// t!("wallet_loading")
// } else {
// format!("{}: {}%", t!("wallet_loading"), wallet.loading_progress)
// };
// ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT));
// });
} else if !wallet.is_loaded() {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
let text = if wallet.loading_progress == 0 {
t!("wallets.wallet_loading")
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), wallet.loading_progress)
};
ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT));
}); //TODO: button to retry.
} else {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
// Setup loading progress text.
let progress = wallet.loading_progress();
let text = if progress == 0 {
t!("wallets.wallet_loading")
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), progress)
};
ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT));
});
}
return true;
} else {
println!("12345 info!!!");
} }
} }
false
} }
} }

View file

@ -15,6 +15,8 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::types::WalletTab; use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::WalletTabType; use crate::gui::views::wallets::wallet::types::WalletTabType;
use crate::gui::views::wallets::wallet::WalletContent;
use crate::wallet::Wallet;
/// Wallet info tab content. /// Wallet info tab content.
#[derive(Default)] #[derive(Default)]
@ -25,6 +27,11 @@ impl WalletTab for WalletInfo {
WalletTabType::Info WalletTabType::Info
} }
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { fn ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
WalletContent::show_loading_ui(ui, frame, wallet);
} }
} }

View file

@ -14,6 +14,7 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::wallet::Wallet;
/// Receive funds tab content. /// Receive funds tab content.
#[derive(Default)] #[derive(Default)]
@ -24,6 +25,6 @@ impl WalletTab for WalletReceive {
WalletTabType::Receive WalletTabType::Receive
} }
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
} }
} }

View file

@ -14,6 +14,7 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::wallet::Wallet;
/// Send funds tab content. /// Send funds tab content.
#[derive(Default)] #[derive(Default)]
@ -24,6 +25,6 @@ impl WalletTab for WalletSend {
WalletTabType::Send WalletTabType::Send
} }
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
} }
} }

View file

@ -14,6 +14,7 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::wallet::Wallet;
/// Wallet settings tab content. /// Wallet settings tab content.
#[derive(Default)] #[derive(Default)]
@ -24,6 +25,6 @@ impl WalletTab for WalletSettings {
WalletTabType::Settings WalletTabType::Settings
} }
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
} }
} }

View file

@ -13,11 +13,16 @@
// limitations under the License. // limitations under the License.
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::wallet::Wallet;
/// Wallet tab content interface. /// Wallet tab content interface.
pub trait WalletTab { pub trait WalletTab {
fn get_type(&self) -> WalletTabType; fn get_type(&self) -> WalletTabType;
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks); fn ui(&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
wallet: &Wallet,
cb: &dyn PlatformCallbacks);
} }
/// Type of [`WalletTab`] content. /// Type of [`WalletTab`] content.

View file

@ -19,7 +19,6 @@ use egui::{Context, Stroke};
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp; use winit::platform::android::activity::AndroidApp;
pub use settings::{AppConfig, Settings};
use crate::gui::{Colors, PlatformApp}; use crate::gui::{Colors, PlatformApp};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
@ -29,7 +28,13 @@ i18n!("locales");
mod node; mod node;
mod wallet; mod wallet;
mod settings; mod settings;
pub use settings::Settings;
mod config;
pub use config::AppConfig;
use crate::gui::views::View;
pub mod gui; pub mod gui;
@ -123,8 +128,10 @@ pub fn setup_visuals(ctx: &Context) {
// Setup selection color. // Setup selection color.
visuals.selection.stroke = Stroke { width: 1.0, color: Colors::TEXT }; visuals.selection.stroke = Stroke { width: 1.0, color: Colors::TEXT };
visuals.selection.bg_fill = Colors::GOLD; visuals.selection.bg_fill = Colors::GOLD;
// Disable stroke around panels by default // Disable stroke around panels by default.
visuals.widgets.noninteractive.bg_stroke = Stroke::NONE; visuals.widgets.noninteractive.bg_stroke = Stroke::NONE;
// Setup stroke around inactive widgets.
visuals.widgets.inactive.bg_stroke = View::DEFAULT_STROKE;
// Setup visuals // Setup visuals
ctx.set_visuals(visuals); ctx.set_visuals(visuals);
} }

View file

@ -45,9 +45,7 @@ impl PeersConfig {
/// Save peers config to the file. /// Save peers config to the file.
pub fn save(&self) { pub fn save(&self) {
let chain_type = AppConfig::chain_type(); let chain_type = AppConfig::chain_type();
let chain_name = chain_type.shortname(); let config_path = Settings::get_config_path(Self::FILE_NAME, Some(chain_type.shortname()));
let sub_dir = Some(chain_name.as_str());
let config_path = Settings::get_config_path(Self::FILE_NAME, sub_dir);
Settings::write_to_file(self, config_path); Settings::write_to_file(self, config_path);
} }
@ -147,8 +145,7 @@ impl NodeConfig {
// Initialize peers config. // Initialize peers config.
let peers_config = { let peers_config = {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let path = Settings::get_config_path(PeersConfig::FILE_NAME, sub_dir); let path = Settings::get_config_path(PeersConfig::FILE_NAME, sub_dir);
let config = Settings::read_from_file::<PeersConfig>(path.clone()); let config = Settings::read_from_file::<PeersConfig>(path.clone());
if !path.exists() || config.is_err() { if !path.exists() || config.is_err() {
@ -160,8 +157,7 @@ impl NodeConfig {
// Initialize node config. // Initialize node config.
let node_config = { let node_config = {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir); let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir);
let config = Settings::read_from_file::<ConfigMembers>(path.clone()); let config = Settings::read_from_file::<ConfigMembers>(path.clone());
if !path.exists() || config.is_err() { if !path.exists() || config.is_err() {
@ -176,9 +172,8 @@ impl NodeConfig {
/// Save default node config for specified [`ChainTypes`]. /// Save default node config for specified [`ChainTypes`].
fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers { fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str()); let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir.clone());
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir);
let mut default_config = GlobalConfig::for_chain(chain_type); let mut default_config = GlobalConfig::for_chain(chain_type);
default_config.update_paths(&Settings::get_base_path(sub_dir)); default_config.update_paths(&Settings::get_base_path(sub_dir));
let config = default_config.members.unwrap(); let config = default_config.members.unwrap();
@ -188,8 +183,7 @@ impl NodeConfig {
/// Save default peers config for specified [`ChainTypes`]. /// Save default peers config for specified [`ChainTypes`].
fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig { fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let path = Settings::get_config_path(PeersConfig::FILE_NAME, sub_dir); let path = Settings::get_config_path(PeersConfig::FILE_NAME, sub_dir);
let config = PeersConfig::default(); let config = PeersConfig::default();
Settings::write_to_file(&config, path); Settings::write_to_file(&config, path);
@ -198,8 +192,7 @@ impl NodeConfig {
/// Save node config to the file. /// Save node config to the file.
pub fn save(&self) { pub fn save(&self) {
let chain_name = self.node.server.chain_type.shortname(); let sub_dir = Some(self.node.server.chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir); let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, sub_dir);
Settings::write_to_file(&self.node, config_path); Settings::write_to_file(&self.node, config_path);
} }
@ -241,8 +234,7 @@ impl NodeConfig {
/// Get path for secret file. /// Get path for secret file.
fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf { fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let grin_path = Settings::get_base_path(sub_dir); let grin_path = Settings::get_base_path(sub_dir);
let mut api_secret_path = grin_path; let mut api_secret_path = grin_path;
api_secret_path.push(secret_file_name); api_secret_path.push(secret_file_name);

View file

@ -18,144 +18,46 @@ use std::path::PathBuf;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use grin_config::ConfigError; use grin_config::ConfigError;
use grin_core::global::ChainTypes;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::config::AppConfig;
use crate::node::NodeConfig; use crate::node::NodeConfig;
use crate::wallet::ConnectionsConfig;
lazy_static! { lazy_static! {
/// Static settings state to be accessible globally. /// Static settings state to be accessible globally.
static ref SETTINGS_STATE: Arc<Settings> = Arc::new(Settings::init()); static ref SETTINGS_STATE: Arc<Settings> = Arc::new(Settings::init());
} }
/// Application configuration file name.
const APP_CONFIG_FILE_NAME: &'static str = "app.toml";
/// Common application settings.
#[derive(Serialize, Deserialize)]
pub struct AppConfig {
/// Run node server on startup.
pub auto_start_node: bool,
/// Chain type for node and wallets.
chain_type: ChainTypes,
/// Flag to initially show wallet list at dual panel wallets mode.
show_wallets_at_dual_panel: bool,
/// Flag to initially show all connections at network panel.
show_connections_network_panel: bool,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
auto_start_node: false,
chain_type: ChainTypes::default(),
show_wallets_at_dual_panel: true,
show_connections_network_panel: false,
}
}
}
impl AppConfig {
/// Save app config to file.
pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(APP_CONFIG_FILE_NAME, None));
}
/// Change chain type and load new [`NodeConfig`].
pub fn change_chain_type(chain_type: &ChainTypes) {
let current_chain_type = Self::chain_type();
if current_chain_type != *chain_type {
// Save chain type at app config.
{
let mut w_app_config = Settings::app_config_to_update();
w_app_config.chain_type = *chain_type;
w_app_config.save();
}
// Load node config for selected chain type.
{
let mut w_node_config = Settings::node_config_to_update();
let node_config = NodeConfig::for_chain_type(chain_type);
w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers;
}
}
}
/// Get current [`ChainTypes`] for node and wallets.
pub fn chain_type() -> ChainTypes {
let r_config = Settings::app_config_to_read();
r_config.chain_type
}
/// Check if integrated node is starting with application.
pub fn autostart_node() -> bool {
let r_config = Settings::app_config_to_read();
r_config.auto_start_node
}
/// Toggle integrated node autostart.
pub fn toggle_node_autostart() {
let autostart = Self::autostart_node();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.auto_start_node = !autostart;
w_app_config.save();
}
/// Toggle flag to show wallet list at dual panel wallets mode.
pub fn show_wallets_at_dual_panel() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_wallets_at_dual_panel
}
/// Toggle flag to show wallet list at dual panel wallets mode.
pub fn toggle_show_wallets_at_dual_panel() {
let show = Self::show_wallets_at_dual_panel();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.show_wallets_at_dual_panel = !show;
w_app_config.save();
}
/// Toggle flag to show all connections at network panel.
pub fn show_connections_network_panel() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_connections_network_panel
}
/// Toggle flag to show all connections at network panel.
pub fn toggle_show_connections_network_panel() {
let show = Self::show_connections_network_panel();
let mut w_app_config = Settings::app_config_to_update();
w_app_config.show_connections_network_panel = !show;
w_app_config.save();
}
}
/// Main application directory name. /// Main application directory name.
const MAIN_DIR_NAME: &'static str = ".grim"; const MAIN_DIR_NAME: &'static str = ".grim";
/// Provides access to application, integrated node and wallets configs. /// Contains initialized configurations.
pub struct Settings { pub struct Settings {
/// Application config instance. /// Application configuration.
app_config: Arc<RwLock<AppConfig>>, app_config: Arc<RwLock<AppConfig>>,
/// Integrated node config instance. /// Integrated node configuration.
node_config: Arc<RwLock<NodeConfig>>, node_config: Arc<RwLock<NodeConfig>>,
/// Wallet connections configuration.
conn_config: Arc<RwLock<ConnectionsConfig>>,
} }
impl Settings { impl Settings {
/// Initialize settings with app and node configs. /// Initialize settings with app and node configs.
fn init() -> Self { fn init() -> Self {
let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); let app_config_path = Settings::get_config_path(AppConfig::FILE_NAME, None);
let app_config = Self::init_config::<AppConfig>(path); let app_config = Self::init_config::<AppConfig>(app_config_path);
let chain_type = &app_config.chain_type;
Self { Self {
node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))), node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(chain_type))),
conn_config: Arc::new(RwLock::new(ConnectionsConfig::for_chain_type(chain_type))),
app_config: Arc::new(RwLock::new(app_config)), app_config: Arc::new(RwLock::new(app_config)),
} }
} }
/// Initialize config from provided file path or set [`Default`] if file not exists. /// Initialize configuration from provided file path or set [`Default`] if file not exists.
pub fn init_config<T: Default + Serialize + DeserializeOwned>(path: PathBuf) -> T { pub fn init_config<T: Default + Serialize + DeserializeOwned>(path: PathBuf) -> T {
let parsed = Self::read_from_file::<T>(path.clone()); let parsed = Self::read_from_file::<T>(path.clone());
if !path.exists() || parsed.is_err() { if !path.exists() || parsed.is_err() {
@ -167,29 +69,38 @@ impl Settings {
} }
} }
/// Get node configuration to read values.
/// Get node config to read values.
pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> { pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> {
SETTINGS_STATE.node_config.read().unwrap() SETTINGS_STATE.node_config.read().unwrap()
} }
/// Get node config to update values. /// Get node configuration to update values.
pub fn node_config_to_update() -> RwLockWriteGuard<'static, NodeConfig> { pub fn node_config_to_update() -> RwLockWriteGuard<'static, NodeConfig> {
SETTINGS_STATE.node_config.write().unwrap() SETTINGS_STATE.node_config.write().unwrap()
} }
/// Get app config to read values. /// Get app configuration to read values.
pub fn app_config_to_read() -> RwLockReadGuard<'static, AppConfig> { pub fn app_config_to_read() -> RwLockReadGuard<'static, AppConfig> {
SETTINGS_STATE.app_config.read().unwrap() SETTINGS_STATE.app_config.read().unwrap()
} }
/// Get app config to update values. /// Get app configuration to update values.
pub fn app_config_to_update() -> RwLockWriteGuard<'static, AppConfig> { pub fn app_config_to_update() -> RwLockWriteGuard<'static, AppConfig> {
SETTINGS_STATE.app_config.write().unwrap() SETTINGS_STATE.app_config.write().unwrap()
} }
/// Get base directory path for config. /// Get connections configuration to read values.
pub fn get_base_path(sub_dir: Option<&str>) -> PathBuf { pub fn conn_config_to_read() -> RwLockReadGuard<'static, ConnectionsConfig> {
SETTINGS_STATE.conn_config.read().unwrap()
}
/// Get connections configuration to update values.
pub fn conn_config_to_update() -> RwLockWriteGuard<'static, ConnectionsConfig> {
SETTINGS_STATE.conn_config.write().unwrap()
}
/// Get base directory path for configuration.
pub fn get_base_path(sub_dir: Option<String>) -> PathBuf {
// Check if dir exists. // Check if dir exists.
let mut path = match dirs::home_dir() { let mut path = match dirs::home_dir() {
Some(p) => p, Some(p) => p,
@ -206,14 +117,14 @@ impl Settings {
path path
} }
/// Get config file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and sub-directory if needed.
pub fn get_config_path(config_name: &str, sub_dir: Option<&str>) -> PathBuf { pub fn get_config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut settings_path = Self::get_base_path(sub_dir); let mut settings_path = Self::get_base_path(sub_dir);
settings_path.push(config_name); settings_path.push(config_name);
settings_path settings_path
} }
/// Read config from the file. /// Read configuration from the file.
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> { pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
let file_content = fs::read_to_string(config_path.clone())?; let file_content = fs::read_to_string(config_path.clone())?;
let parsed = toml::from_str::<T>(file_content.as_str()); let parsed = toml::from_str::<T>(file_content.as_str());
@ -228,7 +139,7 @@ impl Settings {
} }
} }
/// Write config to the file. /// Write configuration to the file.
pub fn write_to_file<T: Serialize>(config: &T, path: PathBuf) { pub fn write_to_file<T: Serialize>(config: &T, path: PathBuf) {
let conf_out = toml::to_string(config).unwrap(); let conf_out = toml::to_string(config).unwrap();
let mut file = File::create(path.to_str().unwrap()).unwrap(); let mut file = File::create(path.to_str().unwrap()).unwrap();

View file

@ -19,33 +19,49 @@ use grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings}; use crate::{AppConfig, Settings};
use crate::wallet::types::ConnectionMethod;
/// Wallet configuration. /// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct WalletConfig { pub struct WalletConfig {
/// Chain type for current wallet. /// Chain type for current wallet.
pub(crate) chain_type: ChainTypes, pub chain_type: ChainTypes,
/// Identifier for a wallet. /// Identifier for a wallet.
pub(crate) id: i64, pub id: i64,
/// Human-readable wallet name for ui. /// Human-readable wallet name for ui.
pub(crate) name: String, pub name: String,
/// External node connection URL. /// External connection identifier.
pub(crate) external_node_url: Option<String>, pub ext_conn_id: Option<i64>,
/// Minimal amount of confirmations.
pub min_confirmations: u64
} }
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
/// Base wallets directory name. /// Base wallets directory name.
pub const BASE_DIR_NAME: &'static str = "wallets"; pub const BASE_DIR_NAME: &'static str = "wallets";
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
/// Minimal amount of confirmations default value.
const MIN_CONFIRMATIONS_DEFAULT: u64 = 10;
impl WalletConfig { impl WalletConfig {
/// Create wallet config. /// Create wallet config.
pub fn create(name: String, external_node_url: Option<String>) -> WalletConfig { pub fn create(name: String, conn_method: &ConnectionMethod) -> WalletConfig {
// Setup configuration path.
let id = chrono::Utc::now().timestamp(); let id = chrono::Utc::now().timestamp();
let chain_type = AppConfig::chain_type(); let chain_type = AppConfig::chain_type();
let config_path = Self::get_config_file_path(chain_type, id); let config_path = Self::get_config_file_path(chain_type, id);
// Write configuration to the file.
let config = WalletConfig { chain_type, id, name, external_node_url }; let config = WalletConfig {
chain_type,
id,
name,
ext_conn_id: match conn_method {
ConnectionMethod::Integrated => None,
ConnectionMethod::External(id) => Some(*id)
},
min_confirmations: MIN_CONFIRMATIONS_DEFAULT,
};
Settings::write_to_file(&config, config_path); Settings::write_to_file(&config, config_path);
config config
} }
@ -62,8 +78,7 @@ impl WalletConfig {
/// Get wallets base directory path for provided [`ChainTypes`]. /// Get wallets base directory path for provided [`ChainTypes`].
pub fn get_base_path(chain_type: ChainTypes) -> PathBuf { pub fn get_base_path(chain_type: ChainTypes) -> PathBuf {
let chain_name = chain_type.shortname(); let sub_dir = Some(chain_type.shortname());
let sub_dir = Some(chain_name.as_str());
let mut wallets_path = Settings::get_base_path(sub_dir); let mut wallets_path = Settings::get_base_path(sub_dir);
wallets_path.push(BASE_DIR_NAME); wallets_path.push(BASE_DIR_NAME);
// Create wallets base directory if it doesn't exist. // Create wallets base directory if it doesn't exist.
@ -98,10 +113,4 @@ impl WalletConfig {
let config_path = Self::get_config_file_path(self.chain_type, self.id); let config_path = Self::get_config_file_path(self.chain_type, self.id);
Settings::write_to_file(self, config_path); Settings::write_to_file(self, config_path);
} }
/// Set external node connection URL.
pub fn save_external_node_url(&mut self, url: Option<String>) {
self.external_node_url = url;
self.save();
}
} }

View file

@ -12,66 +12,73 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::{Arc, RwLock}; use grin_core::global::ChainTypes;
use lazy_static::lazy_static;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::config::AppConfig;
use crate::Settings; use crate::Settings;
use crate::wallet::ExternalConnection; use crate::wallet::ExternalConnection;
lazy_static! {
/// Static connections state to be accessible globally.
static ref CONNECTIONS_STATE: Arc<RwLock<ConnectionsConfig>> = Arc::new(
RwLock::new(
Settings::init_config(Settings::get_config_path(CONFIG_FILE_NAME, None))
)
);
}
/// Wallet connections configuration. /// Wallet connections configuration.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ConnectionsConfig { pub struct ConnectionsConfig {
/// Network type for connections.
chain_type: ChainTypes,
/// URLs of external connections for wallets. /// URLs of external connections for wallets.
external: Vec<ExternalConnection> external: Vec<ExternalConnection>
} }
impl Default for ConnectionsConfig { impl ConnectionsConfig {
fn default() -> Self { /// Wallet connections configuration file name.
Self { pub const FILE_NAME: &'static str = "connections.toml";
external: vec![
ExternalConnection::default() /// Initialize configuration for provided [`ChainTypes`].
], pub fn for_chain_type(chain_type: &ChainTypes) -> Self {
let path = Settings::get_config_path(Self::FILE_NAME, Some(chain_type.shortname()));
let parsed = Settings::read_from_file::<ConnectionsConfig>(path.clone());
if !path.exists() || parsed.is_err() {
let default_config = ConnectionsConfig {
chain_type: *chain_type,
external: if chain_type == &ChainTypes::Mainnet {
vec![
ExternalConnection::default_main()
]
} else {
vec![]
},
};
Settings::write_to_file(&default_config, path);
default_config
} else {
parsed.unwrap()
} }
} }
}
/// Wallet configuration file name. /// Save connections configuration to the file.
const CONFIG_FILE_NAME: &'static str = "connections.toml";
impl ConnectionsConfig {
/// Save connections config to file.
pub fn save(&self) { pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(CONFIG_FILE_NAME, None)); let chain_type = AppConfig::chain_type();
let sub_dir = Some(chain_type.shortname());
Settings::write_to_file(self, Settings::get_config_path(Self::FILE_NAME, sub_dir));
} }
/// Get external connections for the wallet. /// Get [`ExternalConnection`] list.
pub fn external_connections() -> Vec<ExternalConnection> { pub fn ext_conn_list() -> Vec<ExternalConnection> {
let r_config = CONNECTIONS_STATE.read().unwrap(); let r_config = Settings::conn_config_to_read();
r_config.external.clone() r_config.external.clone()
} }
/// Save external connection for the wallet in app config. /// Save [`ExternalConnection`] in configuration.
pub fn add_external_connection(conn: ExternalConnection) { pub fn add_ext_conn(conn: ExternalConnection) {
// Do not update default connection. // Do not update default connection.
if conn.url == ExternalConnection::DEFAULT_EXTERNAL_NODE_URL { if conn.url == ExternalConnection::DEFAULT_MAIN_URL {
return; return;
} }
let mut w_config = CONNECTIONS_STATE.write().unwrap(); let mut w_config = Settings::conn_config_to_update();
let mut exists = false; let mut exists = false;
for mut c in w_config.external.iter_mut() { for mut c in w_config.external.iter_mut() {
// Update connection if URL exists. // Update connection if config exists.
if c.url == conn.url { if c.id == conn.id {
c.url = conn.url.clone();
c.secret = conn.secret.clone(); c.secret = conn.secret.clone();
exists = true; exists = true;
break; break;
@ -84,39 +91,33 @@ impl ConnectionsConfig {
w_config.save(); w_config.save();
} }
/// Save external connection for the wallet in app config. /// Get [`ExternalConnection`] by provided identifier.
pub fn update_external_connection(conn: ExternalConnection, updated: ExternalConnection) { pub fn ext_conn(id: i64) -> Option<ExternalConnection> {
// Do not update default connection. let r_config = Settings::conn_config_to_read();
if conn.url == ExternalConnection::DEFAULT_EXTERNAL_NODE_URL {
return;
}
let mut w_config = CONNECTIONS_STATE.write().unwrap();
for mut c in w_config.external.iter_mut() {
// Update connection if URL exists.
if c.url == conn.url {
c.url = updated.url.clone();
c.secret = updated.secret.clone();
break;
}
}
w_config.save();
}
/// Get external node connection secret from provided URL.
pub fn get_external_connection_secret(url: String) -> Option<String> {
let r_config = CONNECTIONS_STATE.read().unwrap();
for c in &r_config.external { for c in &r_config.external {
if c.url == url { if c.id == id {
return c.secret.clone(); return Some(c.clone());
} }
} }
None None
} }
/// Remove external node connection. /// Set [`ExternalConnection`] availability flag.
pub fn remove_external_connection(conn: &ExternalConnection) { pub fn update_ext_conn_availability(id: i64, available: bool) {
let mut w_config = CONNECTIONS_STATE.write().unwrap(); let mut w_config = Settings::conn_config_to_update();
let index = w_config.external.iter().position(|c| c.url == conn.url); for mut c in w_config.external.iter_mut() {
if c.id == id {
c.available = Some(available);
w_config.save();
break;
}
}
}
/// Remove external node connection by provided identifier.
pub fn remove_ext_conn(id: i64) {
let mut w_config = Settings::conn_config_to_update();
let index = w_config.external.iter().position(|c| c.id == id);
if let Some(i) = index { if let Some(i) = index {
w_config.external.remove(i); w_config.external.remove(i);
w_config.save(); w_config.save();

View file

@ -12,28 +12,85 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::time::Duration;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
/// External node connection for the wallet. use crate::AppConfig;
use crate::wallet::ConnectionsConfig;
/// External connection for the wallet.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ExternalConnection { pub struct ExternalConnection {
/// Connection identifier.
pub id: i64,
/// Node URL. /// Node URL.
pub url: String, pub url: String,
/// Optional API secret key. /// Optional API secret key.
pub secret: Option<String> pub secret: Option<String>,
/// Flag to check if server is available.
#[serde(skip_serializing)]
pub available: Option<bool>
} }
impl ExternalConnection { impl ExternalConnection {
/// Default external node URL. /// Default external node URL for main network.
pub const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; pub const DEFAULT_MAIN_URL: &'static str = "https://grinnode.live:3413";
pub fn new(url: String, secret: Option<String>) -> Self { /// External connections availability check delay.
Self { url, secret } const AVAILABILITY_CHECK_DELAY: Duration = Duration::from_millis(10 * 1000);
/// Create default external connection.
pub fn default_main() -> Self {
Self { id: 1, url: Self::DEFAULT_MAIN_URL.to_string(), secret: None, available: None }
} }
}
impl Default for ExternalConnection { /// Create new external connection.
fn default() -> Self { pub fn new(url: String, secret: Option<String>) -> Self {
Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None } let id = chrono::Utc::now().timestamp();
Self { id, url, secret, available: None }
}
/// Check connection availability.
fn check_conn_availability(&self) {
// Check every connection at separate thread.
let conn = self.clone();
std::thread::spawn(move || {
let url = url::Url::parse(conn.url.as_str()).unwrap();
let addr = url.socket_addrs(|| None).unwrap();
match std::net::TcpStream::connect_timeout(&addr[0], Self::AVAILABILITY_CHECK_DELAY) {
Ok(_) => {
ConnectionsConfig::update_ext_conn_availability(conn.id, true);
}
Err(_) => {
ConnectionsConfig::update_ext_conn_availability(conn.id, false);
}
}
});
}
/// Start external connections availability check at another thread.
pub fn start_ext_conn_availability_check() {
std::thread::spawn(move || {
let chain_type = AppConfig::chain_type();
loop {
// Stop checking if connections are not showing or network type was changed.
if !AppConfig::show_connections_network_panel()
|| chain_type != AppConfig::chain_type() {
break;
}
// Check external connections URLs availability.
let conn_list = ConnectionsConfig::ext_conn_list();
for conn in conn_list {
// Check every connection at separate thread.
conn.check_conn_availability();
}
// Pause checking for delay value.
std::thread::sleep(Self::AVAILABILITY_CHECK_DELAY);
}
});
} }
} }

View file

@ -76,4 +76,13 @@ pub type WalletInstance = Arc<
>, >,
>, >,
>, >,
>; >;
/// Wallet node connection method type.
#[derive(PartialEq)]
pub enum ConnectionMethod {
/// Integrated node.
Integrated,
/// External node, contains connection identifier.
External(i64)
}

View file

@ -12,30 +12,25 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::{cmp, thread}; use std::thread;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::{Arc, mpsc, RwLock};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::Duration; use std::time::Duration;
use grin_core::global; use grin_core::global;
use grin_core::global::ChainTypes; use grin_core::global::ChainTypes;
use grin_keychain::{ExtKeychain, Identifier, Keychain}; use grin_keychain::{ExtKeychain, Keychain};
use grin_util::types::ZeroingString; use grin_util::types::ZeroingString;
use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner};
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, scan, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider}; use grin_wallet_libwallet::{Error, NodeClient, StatusMessage, WalletBackend, WalletInfo, WalletInst, WalletLCProvider};
use log::debug; use grin_wallet_libwallet::api_impl::owner::retrieve_summary_info;
use parking_lot::Mutex; use parking_lot::Mutex;
use uuid::Uuid;
use crate::AppConfig; use crate::AppConfig;
use crate::node::NodeConfig; use crate::node::NodeConfig;
use crate::wallet::{ConnectionsConfig, WalletConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig};
use crate::wallet::selection::lock_tx_context; use crate::wallet::types::{ConnectionMethod, WalletInstance};
use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate};
use crate::wallet::types::WalletInstance;
use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs};
/// [`Wallet`] list wrapper. /// [`Wallet`] list wrapper.
pub struct Wallets { pub struct Wallets {
@ -55,12 +50,6 @@ impl Default for Wallets {
} }
impl Wallets { impl Wallets {
/// Delay in seconds to update outputs after wallet opening.
pub const OUTPUTS_UPDATE_DELAY: Duration = Duration::from_millis(30 * 1000);
/// Delay in seconds to update wallet info.
pub const INFO_UPDATE_DELAY: Duration = Duration::from_millis(10 * 1000);
/// Initialize wallets from base directory for provided [`ChainType`]. /// Initialize wallets from base directory for provided [`ChainType`].
fn init(chain_type: ChainTypes) -> Vec<Wallet> { fn init(chain_type: ChainTypes) -> Vec<Wallet> {
let mut wallets = Vec::new(); let mut wallets = Vec::new();
@ -109,87 +98,15 @@ impl Wallets {
false false
} }
/// Open selected wallet. /// Open and load selected wallet.
pub fn open_selected(&mut self, password: String) -> Result<(), Error> { pub fn launch_selected(&mut self, password: String) -> Result<(), Error> {
for w in self.list.iter_mut() { for w in self.list.iter_mut() {
if Some(w.config.id) == self.selected_id { if Some(w.config.id) == self.selected_id {
return w.open(password); return w.open(password.clone());
} }
} }
Err(Error::GenericError("Wallet is not selected".to_string())) Err(Error::GenericError("Wallet is not selected".to_string()))
} }
/// Load the wallet by scanning available outputs at separate thread.
pub fn load(w: &mut Wallet) {
if !w.is_open() {
return;
}
let mut wallet = w.clone();
thread::spawn(move || {
// Get pmmr range output indexes.
match wallet.pmmr_range() {
Ok((mut lowest_index, highest_index)) => {
println!("pmmr_range: {} {}", lowest_index, highest_index);
let mut from_index = lowest_index;
loop {
// Stop loop if wallet is not open.
if !wallet.is_open() {
break;
}
// Scan outputs for last retrieved index.
println!("scan_outputs from {} to {}", from_index, highest_index);
match wallet.scan_outputs(from_index, highest_index) {
Ok(last_index) => {
println!("retrieved index: {}", last_index);
if lowest_index == 0 {
lowest_index = last_index;
}
// Finish scanning or start new scan from last retrieved index.
if last_index == highest_index {
println!("scan finished");
wallet.loading_progress = 100;
wallet.is_loaded.store(true, Ordering::Relaxed);
} else {
println!("continue scan");
from_index = last_index;
}
// Update loading progress.
let range = highest_index - lowest_index;
let progress = last_index - lowest_index;
wallet.loading_progress = cmp::min(
(progress / range) as u8 * 100,
99
);
println!("progress: {}", wallet.loading_progress);
}
Err(e) => {
if !wallet.is_loaded() {
wallet.loading_error = Some(e);
break;
}
}
}
// Stop loop if wallet is not open.
if !wallet.is_open() {
break;
}
// Scan outputs at next cycle again.
println!("finished scanning, waiting");
thread::sleep(Self::OUTPUTS_UPDATE_DELAY);
}
}
Err(e) => {
wallet.loading_progress = 0;
wallet.loading_error = Some(e);
}
}
});
}
} }
/// Contains wallet instance and config. /// Contains wallet instance and config.
@ -199,29 +116,33 @@ pub struct Wallet {
instance: WalletInstance, instance: WalletInstance,
/// Wallet configuration. /// Wallet configuration.
pub(crate) config: WalletConfig, pub config: WalletConfig,
/// Flag to check if wallet is open. /// Flag to check if wallet is open.
is_open: Arc<AtomicBool>, is_open: Arc<AtomicBool>,
/// Flag to check if wallet is loaded and ready to use.
is_loaded: Arc<AtomicBool>,
/// Error on wallet loading. /// Error on wallet loading.
pub loading_error: Option<Error>, pub loading_error: Option<Error>,
/// Loading progress in percents /// Loading progress in percents
pub loading_progress: u8, pub loading_progress: Arc<AtomicU8>,
/// Wallet balance information.
info: Arc<RwLock<Option<WalletInfo>>>
} }
impl Wallet { impl Wallet {
/// Delay in seconds to update wallet info.
pub const INFO_UPDATE_DELAY: Duration = Duration::from_millis(20 * 1000);
/// Create wallet from provided instance and config. /// Create wallet from provided instance and config.
fn new(instance: WalletInstance, config: WalletConfig) -> Self { fn new(instance: WalletInstance, config: WalletConfig) -> Self {
Self { Self {
instance, instance,
config, config,
is_loaded: Arc::new(AtomicBool::new(false)), is_open: Arc::from(AtomicBool::new(false)),
is_open: Arc::new(AtomicBool::new(false)),
loading_error: None, loading_error: None,
loading_progress: 0, loading_progress: Arc::new(AtomicU8::new(0)),
info: Arc::new(RwLock::new(None)),
} }
} }
@ -230,9 +151,9 @@ impl Wallet {
name: String, name: String,
password: String, password: String,
mnemonic: String, mnemonic: String,
external_node_url: Option<String> conn_method: &ConnectionMethod
) -> Result<Wallet, Error> { ) -> Result<Wallet, Error> {
let config = WalletConfig::create(name, external_node_url); let config = WalletConfig::create(name, conn_method);
let instance = Self::create_wallet_instance(config.clone())?; let instance = Self::create_wallet_instance(config.clone())?;
let w = Wallet::new(instance, config); let w = Wallet::new(instance, config);
{ {
@ -278,8 +199,12 @@ impl Wallet {
} }
// Setup node client. // Setup node client.
let (node_api_url, node_secret) = if let Some(url) = &config.external_node_url { let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id {
(url.to_owned(), ConnectionsConfig::get_external_connection_secret(url.to_owned())) if let Some(conn) = ConnectionsConfig::ext_conn(id) {
(conn.url, conn.secret)
} else {
(ExternalConnection::DEFAULT_MAIN_URL.to_string(), None)
}
} else { } else {
let api_url = format!("http://{}", NodeConfig::get_api_address()); let api_url = format!("http://{}", NodeConfig::get_api_address());
let api_secret = NodeConfig::get_foreign_api_secret(); let api_secret = NodeConfig::get_foreign_api_secret();
@ -314,16 +239,85 @@ impl Wallet {
Ok(Arc::new(Mutex::new(wallet))) Ok(Arc::new(Mutex::new(wallet)))
} }
/// Open wallet instance. /// Open wallet and start info updating at separate thread.
pub fn open(&self, password: String) -> Result<(), Error> { pub fn open(&self, password: String) -> Result<(), Error> {
let mut wallet_lock = self.instance.lock(); let mut wallet_lock = self.instance.lock();
let lc = wallet_lock.lc_provider()?; let lc = wallet_lock.lc_provider()?;
lc.close_wallet(None)?; lc.close_wallet(None)?;
lc.open_wallet(None, ZeroingString::from(password), false, false)?;
self.is_open.store(true, Ordering::Relaxed); let mut wallet = self.clone();
match lc.open_wallet(None, ZeroingString::from(password), false, false) {
Ok(result) => {
self.is_open.store(true, Ordering::Relaxed);
let keychain_mask = result.clone();
// Launch loop at separate thread to update wallet info.
thread::spawn(move || loop {
// Stop updating if wallet was closed.
if !wallet.is_open() {
break;
}
let (tx, rx) = mpsc::channel::<StatusMessage>();
// Update progress at separate thread.
let wallet_scan = wallet.clone();
thread::spawn(move || {
while let Ok(m) = rx.recv() {
println!("m: {}", serde_json::to_string::<StatusMessage>(&m.clone()).unwrap());
match m {
StatusMessage::UpdatingOutputs(_) => {}
StatusMessage::UpdatingTransactions(_) => {}
StatusMessage::FullScanWarn(_) => {}
StatusMessage::Scanning(_, progress) => {
wallet_scan
.loading_progress
.store(progress, Ordering::Relaxed);
}
StatusMessage::ScanningComplete(_) => {
wallet_scan
.loading_progress
.store(100, Ordering::Relaxed);
}
StatusMessage::UpdateWarning(_) => {}
}
}
});
// Retrieve wallet info.
match retrieve_summary_info(
wallet.instance.clone(),
keychain_mask.as_ref(),
&Some(tx),
true,
wallet.config.min_confirmations
) {
Ok(info) => {
let mut w_info = wallet.info.write().unwrap();
*w_info = Some(info.1);
}
Err(e) => {
println!("Error!: {}", e);
wallet.loading_error = Some(e);
}
}
// Repeat after default delay or after 1 second if update was not complete.
let delay = if wallet.loading_progress() == 100 {
Self::INFO_UPDATE_DELAY
} else {
Duration::from_millis(1000)
};
thread::sleep(delay);
});
}
Err(e) => return Err(e)
}
Ok(()) Ok(())
} }
pub fn loading_progress(&self) -> u8 {
self.loading_progress.load(Ordering::Relaxed)
}
/// Check if wallet is open. /// Check if wallet is open.
pub fn is_open(&self) -> bool { pub fn is_open(&self) -> bool {
self.is_open.load(Ordering::Relaxed) self.is_open.load(Ordering::Relaxed)
@ -340,377 +334,9 @@ impl Wallet {
Ok(()) Ok(())
} }
/// Check if wallet is loaded and ready to use. /// Get wallet info.
pub fn is_loaded(&self) -> bool { pub fn get_info(&self) -> Option<WalletInfo> {
self.is_loaded.load(Ordering::Relaxed) let r_info = self.info.read().unwrap();
} r_info.clone()
/// Scan wallet outputs to check/repair the wallet.
fn scan_outputs(
&self,
last_retrieved_index: u64,
highest_index: u64
) -> Result<u64, Error> {
let wallet = self.instance.clone();
let info = scan(
wallet.clone(),
None,
false,
last_retrieved_index,
highest_index,
&None,
)?;
let result = info.last_pmmr_index;
let parent_key_id = {
wallet_lock!(wallet.clone(), w);
w.parent_key_id().clone()
};
{
wallet_lock!(wallet, w);
let mut batch = w.batch(None)?;
batch.save_last_confirmed_height(&parent_key_id, info.height)?;
batch.commit()?;
};
Ok(result)
}
/// Get pmmr indices representing the outputs for the wallet.
fn pmmr_range(&self) -> Result<(u64, u64), Error> {
wallet_lock!(self.instance.clone(), w);
let pmmr_range = w.w2n_client().height_range_to_pmmr_indices(0, None)?;
Ok(pmmr_range)
}
/// Create transaction.
pub fn tx_create(
&self,
amount: u64,
minimum_confirmations: u64,
selection_strategy_is_use_all: bool,
) -> Result<(Vec<TxLogEntry>, String), Error> {
let parent_key_id = {
wallet_lock!(self.instance, w);
w.parent_key_id().clone()
};
let slate = {
wallet_lock!(self.instance, w);
let mut slate = new_tx_slate(&mut **w, amount, false, 2, false, None)?;
let height = w.w2n_client().get_chain_tip()?.0;
let context = add_inputs_to_slate(
&mut **w,
None,
&mut slate,
height,
minimum_confirmations,
500,
1,
selection_strategy_is_use_all,
&parent_key_id,
true,
false,
false,
)?;
{
let mut batch = w.batch(None)?;
batch.save_private_context(slate.id.as_bytes(), &context)?;
batch.commit()?;
}
lock_tx_context(&mut **w, None, &slate, height, &context, None)?;
slate.compact()?;
slate
};
let packer = Slatepacker::new(SlatepackerArgs {
sender: None, // sender
recipients: vec![],
dec_key: None,
});
let slatepack = packer.create_slatepack(&slate)?;
let api = Owner::new(self.instance.clone(), None);
let txs = api.retrieve_txs(None, false, None, Some(slate.id), None)?;
let result = (
txs.1,
SlatepackArmor::encode(&slatepack)?,
);
Ok(result)
}
/// Callback to check slate compatibility at current node.
fn check_middleware(
name: ForeignCheckMiddlewareFn,
node_version_info: Option<NodeVersionInfo>,
slate: Option<&Slate>,
) -> Result<(), Error> {
match name {
ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()),
_ => {
let mut bhv = 3;
if let Some(n) = node_version_info {
bhv = n.block_header_version;
}
if let Some(s) = slate {
if bhv > 4
&& s.version_info.block_header_version
< slate_versions::GRIN_BLOCK_HEADER_VERSION
{
Err(Error::Compatibility(
"Incoming Slate is not compatible with this wallet. \
Please upgrade the node or use a different one."
.into(),
))?;
}
}
Ok(())
}
}
}
/// Receive transaction.
pub fn tx_receive(
&self,
account: &str,
slate_armored: &str
) -> Result<(Vec<TxLogEntry>, String), Error> {
let foreign_api =
Foreign::new(self.instance.clone(), None, Some(Self::check_middleware), false);
let owner_api = Owner::new(self.instance.clone(), None);
let mut slate =
owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
let slatepack =
owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
let _ret_address = slatepack.sender;
slate = foreign_api.receive_tx(&slate, Some(&account), None)?;
let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?;
let packer = Slatepacker::new(SlatepackerArgs {
sender: None, // sender
recipients: vec![],
dec_key: None,
});
let slatepack = packer.create_slatepack(&slate)?;
let result = (
txs.1,
SlatepackArmor::encode(&slatepack)?,
);
Ok(result)
}
/// Cancel transaction.
pub fn tx_cancel(&self, id: u32) -> Result<String, Error> {
wallet_lock!(self.instance, w);
let parent_key_id = w.parent_key_id();
cancel_tx(&mut **w, None, &parent_key_id, Some(id), None)?;
Ok("".to_owned())
}
/// Get transaction info.
pub fn get_tx(&self, tx_slate_id: &str) -> Result<(bool, Vec<TxLogEntry>), Error> {
let api = Owner::new(self.instance.clone(), None);
let uuid = Uuid::parse_str(tx_slate_id).unwrap();
let txs = api.retrieve_txs(None, true, None, Some(uuid), None)?;
Ok(txs)
}
/// Finalize transaction.
pub fn tx_finalize(&self, slate_armored: &str) -> Result<(bool, Vec<TxLogEntry>), Error> {
let owner_api = Owner::new(self.instance.clone(), None);
let mut slate =
owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
let slatepack =
owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?;
let _ret_address = slatepack.sender;
slate = owner_api.finalize_tx(None, &slate)?;
let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?;
Ok(txs)
}
/// Post transaction to node for broadcasting.
pub fn tx_post(&self, tx_slate_id: &str) -> Result<(), Error> {
let api = Owner::new(self.instance.clone(), None);
let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap();
let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?;
if txs[0].confirmed {
return Err(Error::GenericError(format!(
"Transaction with id {} is already confirmed. Not posting.",
tx_slate_id
)));
}
let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?;
match stored_tx {
Some(stored_tx) => {
api.post_tx(None, &stored_tx, true)?;
Ok(())
}
None => Err(Error::GenericError(format!(
"Transaction with id {} does not have transaction data. Not posting.",
tx_slate_id
))),
}
}
/// Get transactions and base wallet info.
pub fn get_txs_info(
&self,
minimum_confirmations: u64
) -> Result<(bool, Vec<TxLogEntry>, WalletInfo), Error> {
let refreshed = Self::update_state(self.instance.clone()).unwrap_or(false);
let wallet_info = {
wallet_lock!(self.instance, w);
let parent_key_id = w.parent_key_id();
Self::get_info(&mut **w, &parent_key_id, minimum_confirmations)?
};
let api = Owner::new(self.instance.clone(), None);
let txs = api.retrieve_txs(None, false, None, None, None)?;
Ok((refreshed, txs.1, wallet_info))
}
/// Update wallet instance state.
fn update_state<'a, L, C, K>(
wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
) -> Result<bool, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let parent_key_id = {
wallet_lock!(wallet_inst, w);
w.parent_key_id().clone()
};
let mut client = {
wallet_lock!(wallet_inst, w);
w.w2n_client().clone()
};
let tip = client.get_chain_tip()?;
// Step1: Update outputs and transactions purely based on UTXO state.
{
wallet_lock!(wallet_inst, w);
if !match refresh_output_state(&mut **w, None, tip.0, &parent_key_id, true) {
Ok(_) => true,
Err(_) => false,
} {
// We are unable to contact the node.
return Ok(false);
}
}
let mut txs = {
wallet_lock!(wallet_inst, w);
retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)?
};
for tx in txs.iter_mut() {
// Step 2: Cancel any transactions with an expired TTL.
if let Some(e) = tx.ttl_cutoff_height {
if tip.0 >= e {
wallet_lock!(wallet_inst, w);
let parent_key_id = w.parent_key_id();
cancel_tx(&mut **w, None, &parent_key_id, Some(tx.id), None)?;
continue;
}
}
// Step 3: Update outstanding transactions with no change outputs by kernel.
if tx.confirmed {
continue;
}
if tx.amount_debited != 0 && tx.amount_credited != 0 {
continue;
}
if let Some(e) = tx.kernel_excess {
let res = client.get_kernel(&e, tx.kernel_lookup_min_height, Some(tip.0));
let kernel = match res {
Ok(k) => k,
Err(_) => return Ok(false),
};
if let Some(k) = kernel {
debug!("Kernel Retrieved: {:?}", k);
wallet_lock!(wallet_inst, w);
let mut batch = w.batch(None)?;
tx.confirmed = true;
tx.update_confirmation_ts();
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
batch.commit()?;
}
}
}
return Ok(true);
}
/// Get summary info about the wallet.
fn get_info<'a, T: ?Sized, C, K>(
wallet: &mut T,
parent_key_id: &Identifier,
minimum_confirmations: u64,
) -> Result<WalletInfo, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let current_height = wallet.last_confirmed_height()?;
let outputs = wallet
.iter()
.filter(|out| out.root_key_id == *parent_key_id);
let mut unspent_total = 0;
let mut immature_total = 0;
let mut awaiting_finalization_total = 0;
let mut unconfirmed_total = 0;
let mut locked_total = 0;
let mut reverted_total = 0;
for out in outputs {
match out.status {
OutputStatus::Unspent => {
if out.is_coinbase && out.lock_height > current_height {
immature_total += out.value;
} else if out.num_confirmations(current_height) < minimum_confirmations {
// Treat anything less than minimum confirmations as "unconfirmed".
unconfirmed_total += out.value;
} else {
unspent_total += out.value;
}
}
OutputStatus::Unconfirmed => {
// We ignore unconfirmed coinbase outputs completely.
if !out.is_coinbase {
if minimum_confirmations == 0 {
unconfirmed_total += out.value;
} else {
awaiting_finalization_total += out.value;
}
}
}
OutputStatus::Locked => {
locked_total += out.value;
}
OutputStatus::Reverted => reverted_total += out.value,
OutputStatus::Spent => {}
}
}
Ok(WalletInfo {
last_confirmed_height: current_height,
minimum_confirmations,
total: unspent_total + unconfirmed_total + immature_total,
amount_awaiting_finalization: awaiting_finalization_total,
amount_awaiting_confirmation: unconfirmed_total,
amount_immature: immature_total,
amount_locked: locked_total,
amount_currently_spendable: unspent_total,
amount_reverted: reverted_total,
})
} }
} }