ui + config: wallet connections setup, initial wallet config, wallet list state, update translations

This commit is contained in:
ardocrat 2023-07-25 03:42:52 +03:00
parent 5cefb61bb3
commit f461f27e4c
23 changed files with 564 additions and 127 deletions

1
Cargo.lock generated
View file

@ -2249,6 +2249,7 @@ dependencies = [
"tokio 1.29.1",
"tokio-util 0.7.8",
"toml 0.7.6",
"url",
"wgpu",
"winit",
"zeroize",

View file

@ -51,6 +51,7 @@ toml = "0.7.4"
serde = "1"
pnet = "0.33.0"
zeroize = "1.6.0"
url = "2.4.0"
# stratum server
serde_derive = "1"

View file

@ -18,7 +18,11 @@ wallets:
not_valid_word: Entered word is not valid
create_phrase_desc: Safely write down and save your recovery phrase.
restore_phrase_desc: Enter words from your saved recovery phrase.
setup_conn_desc: Choose wallet connection method.
setup_conn_desc: Choose how your wallet connects to the network.
conn_method: Connection method
ext_conn: 'External connections:'
add_node_url: Add node URL
invalid_url: Entered URL is invalid
network:
self: Network
node: Integrated node
@ -150,6 +154,7 @@ modal:
cancel: Cancel
save: Save
confirmation: Confirmation
add: Add
modal_exit:
description: Are you sure you want to quit the application?
exit: Exit

View file

@ -18,7 +18,11 @@ wallets:
not_valid_word: Введено недопустимое слово
create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления.
restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления.
setup_conn_desc: Выберите способ подключения кошелька
setup_conn_desc: Выберите способ подключения вашего кошелька к сети.
conn_method: Способ подключения
ext_conn: 'Внешние подключения:'
add_node_url: Добавить URL узла
invalid_url: Введенный URL-адрес недействителен
network:
self: Сеть
node: Встроенный узел
@ -150,6 +154,7 @@ modal:
cancel: Отмена
save: Сохранить
confirmation: Подтверждение
add: Добавить
modal_exit:
description: Вы уверены, что хотите выйти из приложения?
exit: Выход

View file

@ -20,7 +20,7 @@ use lazy_static::lazy_static;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Wallets, Modal, ModalContainer, Network, View};
use crate::gui::views::{WalletsContent, Modal, ModalContainer, Network, View};
use crate::node::Node;
lazy_static! {
@ -32,8 +32,8 @@ lazy_static! {
pub struct Root {
/// Side panel [`Network`] content.
network: Network,
/// Central panel [`Wallets`] content.
wallets: Wallets,
/// Central panel [`WalletsContent`] content.
wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool,
@ -52,7 +52,7 @@ impl Default for Root {
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self {
network: Network::default(),
wallets: Wallets::default(),
wallets: WalletsContent::default(),
exit_allowed,
show_exit_progress: false,
allowed_modal_ids: vec![
@ -117,7 +117,7 @@ impl Root {
(is_panel_open, panel_width)
}
/// Check if ui can show [`Network`] and [`Wallets`] at same time.
/// Check if ui can show [`Network`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
let w = frame.info().window_info.size.x;
let h = frame.info().window_info.size.y;

View file

@ -14,14 +14,15 @@
use egui::{Margin, RichText, TextStyle, vec2, Widget};
use egui_extras::{RetainedImage, Size, StripBuilder};
use crate::built_info;
use crate::built_info;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, EYE, EYE_SLASH, PLUS_CIRCLE, SHARE_FAT};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalPosition, View};
use crate::gui::views::wallets::creation::{ConnectionSetup, MnemonicSetup};
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::{PhraseMode, Step};
use crate::gui::views::wallets::setup::ConnectionSetup;
/// Wallet creation content.
pub struct WalletCreation {
@ -51,8 +52,8 @@ impl Default for WalletCreation {
Self {
step: None,
modal_just_opened: true,
name_edit: "".to_string(),
pass_edit: "".to_string(),
name_edit: String::from(""),
pass_edit: String::from(""),
hide_pass: true,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSetup::default(),
@ -89,17 +90,16 @@ impl WalletCreation {
let (step_text, step_available) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode;
let size_value = self.mnemonic_setup.mnemonic.size.value();
let text = if mode == &PhraseMode::Generate {
t!("wallets.create_phrase_desc", "number" => size_value)
t!("wallets.create_phrase_desc")
} else {
t!("wallets.restore_phrase_desc", "number" => size_value)
t!("wallets.restore_phrase_desc")
};
let available = !self
.mnemonic_setup
.mnemonic
.words
.contains(&"".to_string());
.contains(&String::from(""));
(text, available)
}
Step::ConfirmMnemonic => {
@ -108,7 +108,7 @@ impl WalletCreation {
.mnemonic_setup
.mnemonic
.confirm_words
.contains(&"".to_string());
.contains(&String::from(""));
(text, available)
},
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
@ -190,13 +190,9 @@ impl WalletCreation {
}
Some(step) => {
match step {
Step::EnterMnemonic => {
self.mnemonic_setup.ui(ui);
}
Step::ConfirmMnemonic => {
self.mnemonic_setup.confirm_ui(ui);
}
Step::SetupConnection => {}
Step::EnterMnemonic => self.mnemonic_setup.ui(ui),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui),
Step::SetupConnection => self.network_setup.ui(ui, cb)
}
}
}
@ -216,8 +212,8 @@ impl WalletCreation {
Step::EnterMnemonic => {
// Clear values if it needs to go back on first step.
self.step = None;
self.name_edit = "".to_string();
self.pass_edit = "".to_string();
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
}
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
@ -255,8 +251,8 @@ impl WalletCreation {
// Reset modal values.
self.hide_pass = false;
self.modal_just_opened = true;
self.name_edit = "".to_string();
self.pass_edit = "".to_string();
self.name_edit = String::from("");
self.pass_edit = String::from("");
// Show modal.
Modal::new(Self::NAME_PASS_MODAL)
.position(ModalPosition::CenterTop)

View file

@ -38,7 +38,7 @@ impl Default for MnemonicSetup {
Self {
mnemonic: Mnemonic::default(),
word_num_edit: 0,
word_edit: "".to_string(),
word_edit: String::from(""),
valid_word_edit: true
}
}
@ -192,12 +192,12 @@ impl MnemonicSetup {
}
/// Draw word list item for current mode.
fn word_item_ui(&mut self, ui: &mut egui::Ui, word_number: usize, word: &String, edit: bool) {
fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &String, edit: bool) {
if edit {
ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::BUTTON, || {
// Setup modal values.
self.word_num_edit = word_number;
self.word_num_edit = num;
self.word_edit = word.clone();
self.valid_word_edit = true;
// Show word edit modal.
@ -206,12 +206,12 @@ impl MnemonicSetup {
.title(t!("wallets.saved_phrase"))
.show();
});
ui.label(RichText::new(format!("#{} {}", word_number, word))
ui.label(RichText::new(format!("#{} {}", num, word))
.size(17.0)
.color(Colors::BLACK));
} else {
ui.add_space(12.0);
let text = format!("#{} {}", word_number, word);
let text = format!("#{} {}", num, word);
ui.label(RichText::new(text).size(17.0).color(Colors::BLACK));
}
}
@ -274,14 +274,18 @@ impl MnemonicSetup {
self.valid_word_edit = false;
return;
}
self.valid_word_edit = true;
// Select list where to save word.
let words = match self.mnemonic.mode {
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
PhraseMode::Import => &mut self.mnemonic.words
};
// Save word at list.
words.remove(word_index);
words.insert(word_index, self.word_edit.clone());
// Close modal or go to next word to edit.
let close_modal = words.len() == self.word_num_edit
|| !words.get(self.word_num_edit).unwrap().is_empty();
@ -290,7 +294,7 @@ impl MnemonicSetup {
modal.close();
} else {
self.word_num_edit += 1;
self.word_edit = "".to_string();
self.word_edit = String::from("");
}
};
// Call save on Enter key press.

View file

@ -15,9 +15,6 @@
mod mnemonic;
pub use mnemonic::MnemonicSetup;
mod connection;
pub use connection::ConnectionSetup;
mod creation;
pub use creation::WalletCreation;

View file

@ -14,6 +14,7 @@
use grin_keychain::mnemonic::{from_entropy, search};
use rand::{Rng, thread_rng};
use zeroize::{Zeroize, ZeroizeOnDrop};
/// Wallet creation step.
#[derive(PartialEq)]
@ -27,7 +28,8 @@ pub enum Step {
}
/// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)]
/// Will be completely cleaned from memory on drop.
#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub enum PhraseMode {
/// Generate new mnemonic phrase.
Generate,
@ -36,7 +38,8 @@ pub enum PhraseMode {
}
/// Mnemonic phrase size based on words count.
#[derive(PartialEq, Clone)]
/// Will be completely cleaned from memory on drop.
#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 }
impl PhraseSize {
@ -72,6 +75,8 @@ impl PhraseSize {
}
/// Mnemonic phrase container.
/// Will be completely cleaned from memory on drop.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Mnemonic {
/// Phrase setup mode.
pub(crate) mode: PhraseMode,
@ -130,7 +135,7 @@ impl Mnemonic {
}
from_entropy(&entropy).unwrap()
.split(" ")
.map(|s| s.to_string())
.map(|s| String::from(s))
.collect::<Vec<String>>()
},
PhraseMode::Import => {
@ -143,7 +148,7 @@ impl Mnemonic {
fn empty_words(size: &PhraseSize) -> Vec<String> {
let mut words = Vec::with_capacity(size.value());
for _ in 0..size.value() {
words.push("".to_string())
words.push(String::from(""))
}
words
}

View file

@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod wallets;
mod creation;
mod wallet;
mod setup;
mod wallets;
pub use wallets::*;

View file

@ -0,0 +1,165 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText, ScrollArea, TextStyle, Widget};
use url::Url;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{GLOBE, GLOBE_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::wallets::setup::ConnectionMethod;
/// Wallet node connection method setup content.
pub struct ConnectionSetup {
/// Selected connection method.
method: ConnectionMethod,
/// External node connection URL value for [`Modal`].
ext_node_url_edit: String,
/// Flag to show URL format error.
ext_node_url_error: bool,
}
impl Default for ConnectionSetup {
fn default() -> Self {
Self {
method: ConnectionMethod::Integrated,
ext_node_url_edit: "".to_string(),
ext_node_url_error: false
}
}
}
impl ConnectionSetup {
/// External node connection [`Modal`] identifier.
pub const ADD_CONNECTION_URL_MODAL: &'static str = "add_connection_url_modal";
//TODO: Setup for provided wallet
// pub fn new() -> Self {
// Self { method: ConnectionMethod::Integrated }
// }
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_source("wallet_connection_setup")
.auto_shrink([false; 2])
.show(ui, |ui| {
View::sub_title(ui, format!("{} {}", GLOBE, t!("wallets.conn_method")));
View::horizontal_line(ui, Colors::STROKE);
ui.add_space(4.0);
ui.vertical_centered(|ui| {
// Show integrated node selection.
ui.add_space(6.0);
View::radio_value(ui,
&mut self.method,
ConnectionMethod::Integrated,
t!("network.node"));
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::GRAY));
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", GLOBE_SIMPLE, t!("wallets.add_node_url"));
View::button(ui, add_node_text, Colors::GOLD, || {
// Setup values for Modal.
self.ext_node_url_edit = "".to_string();
self.ext_node_url_error = false;
// Show modal.
Modal::new(Self::ADD_CONNECTION_URL_MODAL)
.title(t!("wallets.ext_conn"))
.show();
cb.show_keyboard();
});
ui.add_space(12.0);
// Show external nodes URLs selection.
for conn in AppConfig::external_nodes_urls() {
View::radio_value(ui,
&mut self.method,
ConnectionMethod::External(conn.clone()),
conn);
ui.add_space(12.0);
}
});
});
}
/// Draw external connections setup.
fn external_conn_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
}
/// Draw modal content.
pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Draw external node URL text edit.
let text_edit_resp = egui::TextEdit::singleline(&mut self.ext_node_url_edit)
.id(Id::from(modal.id))
.font(TextStyle::Heading)
.desired_width(ui.available_width())
.cursor_at_end(true)
.ui(ui);
text_edit_resp.request_focus();
if text_edit_resp.clicked() {
cb.show_keyboard();
}
// Show error when specified URL is not valid.
if self.ext_node_url_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.invalid_url"))
.size(17.0)
.color(Colors::RED));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Add button callback.
let on_add = || {
let error = Url::parse(self.ext_node_url_edit.as_str()).is_err();
self.ext_node_url_error = error;
if !error {
AppConfig::add_external_node_url(self.ext_node_url_edit.clone());
// Close modal.
cb.hide_keyboard();
modal.close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.add"), Colors::WHITE, on_add);
});
});
ui.add_space(6.0);
});
}
}

View file

@ -12,15 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::gui::platform::PlatformCallbacks;
mod connection;
pub use connection::ConnectionSetup;
#[derive(Default)]
pub struct ConnectionSetup {
}
impl ConnectionSetup {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
}
}
mod types;
pub use types::*;

View file

@ -0,0 +1,22 @@
// 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

@ -17,16 +17,17 @@ use egui::Margin;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::wallet::Wallet;
/// Selected wallet list item content.
pub struct WalletContent {
/// Current wallet instance.
item: String
wallet: Wallet
}
impl WalletContent {
fn new(item: String) -> Self {
Self { item }
fn new(wallet: Wallet) -> Self {
Self { wallet }
}
}

View file

@ -21,12 +21,14 @@ use crate::gui::icons::{ARROW_LEFT, GEAR, GLOBE, PLUS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View};
use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation};
use crate::gui::views::wallets::setup::ConnectionSetup;
use crate::gui::views::wallets::wallet::WalletContent;
use crate::wallet::{Wallet, WalletList};
/// Wallets content.
pub struct Wallets {
pub struct WalletsContent {
/// List of wallets.
list: Vec<String>,
list: Vec<Wallet>,
/// Selected list item content.
item_content: Option<WalletContent>,
@ -37,28 +39,28 @@ pub struct Wallets {
modal_ids: Vec<&'static str>
}
impl Default for Wallets {
impl Default for WalletsContent {
fn default() -> Self {
//TODO load list.
Self {
list: vec![],
list: WalletList::list(),
item_content: None,
creation_content: WalletCreation::default(),
modal_ids: vec![
WalletCreation::NAME_PASS_MODAL,
MnemonicSetup::WORD_INPUT_MODAL
MnemonicSetup::WORD_INPUT_MODAL,
ConnectionSetup::ADD_CONNECTION_URL_MODAL
]
}
}
}
impl ModalContainer for Wallets {
impl ModalContainer for WalletsContent {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
}
impl Wallets {
impl WalletsContent {
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) {
// Show modal content for current ui container.
if self.can_draw_modal() {
@ -70,6 +72,9 @@ impl Wallets {
MnemonicSetup::WORD_INPUT_MODAL => {
self.creation_content.mnemonic_setup.modal_ui(ui, modal, cb);
}
ConnectionSetup::ADD_CONNECTION_URL_MODAL => {
self.creation_content.network_setup.modal_ui(ui, modal, cb);
}
_ => {}
}
});
@ -176,7 +181,7 @@ impl Wallets {
}
}
/// Check if ui can show [`Wallets`] list and [`WalletContent`] content at same time.
/// Check if ui can show [`WalletsContent`] list and [`WalletContent`] content at same time.
fn is_dual_panel_mode(ui: &mut egui::Ui, frame: &mut eframe::Frame) -> bool {
let dual_panel_root = Root::is_dual_panel_mode(frame);
let max_width = ui.available_width();

View file

@ -93,13 +93,13 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator
options.default_theme = eframe::Theme::Light;
options.renderer = eframe::Renderer::Wgpu;
options.initial_window_size = Some(egui::Vec2::new(1200.0, 720.0));
// Setup translations.
setup_i18n();
// Start integrated node if needed.
if Settings::app_config_to_read().auto_start_node {
Node::start();
}
// Launch graphical interface.
let _ = eframe::run_native("Grim", options, app_creator);
}

View file

@ -45,34 +45,11 @@ impl PeersConfig {
/// Save peers config to the file.
pub fn save(&self) {
let chain_type = AppConfig::chain_type();
let config_path = Settings::get_config_path(Self::FILE_NAME, Some(&chain_type));
let chain_name = Some(chain_type.shortname());
let config_path = Settings::get_config_path(Self::FILE_NAME, chain_name);
Settings::write_to_file(self, config_path);
}
/// Save seed peer.
pub fn save_seed(&mut self, peer: String) {
self.seeds.insert(self.seeds.len(), peer);
self.save();
}
/// Save allowed peer.
pub fn save_allowed(&mut self, peer: String) {
self.allowed.insert(self.allowed.len(), peer);
self.save();
}
/// Save denied peer.
pub fn save_denied(&mut self, peer: String) {
self.denied.insert(self.denied.len(), peer);
self.save();
}
/// Save preferred peer.
pub fn save_preferred(&mut self, peer: String) {
self.preferred.insert(self.preferred.len(), peer);
self.save();
}
/// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available.
pub fn peer_to_addr(peer: String) -> Option<PeerAddr> {
match SocketAddr::from_str(peer.as_str()) {
@ -169,7 +146,8 @@ impl NodeConfig {
// Initialize peers config.
let peers_config = {
let path = Settings::get_config_path(PeersConfig::FILE_NAME, Some(chain_type));
let chain_name = Some(chain_type.shortname());
let path = Settings::get_config_path(PeersConfig::FILE_NAME, chain_name);
let config = Settings::read_from_file::<PeersConfig>(path.clone());
if !path.exists() || config.is_err() {
Self::save_default_peers_config(chain_type)
@ -180,7 +158,8 @@ impl NodeConfig {
// Initialize node config.
let node_config = {
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type));
let chain_name = Some(chain_type.shortname());
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name);
let config = Settings::read_from_file::<ConfigMembers>(path.clone());
if !path.exists() || config.is_err() {
Self::save_default_node_server_config(chain_type)
@ -194,9 +173,10 @@ impl NodeConfig {
/// Save default node config for specified [`ChainTypes`].
fn save_default_node_server_config(chain_type: &ChainTypes) -> ConfigMembers {
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type));
let chain_name = Some(chain_type.shortname());
let path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name.clone());
let mut default_config = GlobalConfig::for_chain(chain_type);
default_config.update_paths(&Settings::get_working_path(Some(chain_type)));
default_config.update_paths(&Settings::get_base_path(chain_name));
let config = default_config.members.unwrap();
Settings::write_to_file(&config, path);
config
@ -204,7 +184,8 @@ impl NodeConfig {
/// Save default peers config for specified [`ChainTypes`].
fn save_default_peers_config(chain_type: &ChainTypes) -> PeersConfig {
let path = Settings::get_config_path(PeersConfig::FILE_NAME, Some(chain_type));
let chain_name = Some(chain_type.shortname());
let path = Settings::get_config_path(PeersConfig::FILE_NAME, chain_name);
let config = PeersConfig::default();
Settings::write_to_file(&config, path);
config
@ -212,10 +193,8 @@ impl NodeConfig {
/// Save node config to the file.
pub fn save(&self) {
let config_path = Settings::get_config_path(
SERVER_CONFIG_FILE_NAME,
Some(&self.node.server.chain_type)
);
let chain_name = Some(self.node.server.chain_type.shortname());
let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, chain_name);
Settings::write_to_file(&self.node, config_path);
}
@ -256,7 +235,7 @@ impl NodeConfig {
/// Get path for secret file.
fn get_secret_path(chain_type: &ChainTypes, secret_file_name: &str) -> PathBuf {
let grin_path = Settings::get_working_path(Some(chain_type));
let grin_path = Settings::get_base_path(Some(chain_type.shortname()));
let mut api_secret_path = grin_path;
api_secret_path.push(secret_file_name);
api_secret_path
@ -446,7 +425,7 @@ impl NodeConfig {
/// Get API server IP and port.
pub fn get_api_ip_port() -> (String, String) {
let saved_addr = Self::get_api_address().as_str();
let saved_addr = Self::get_api_address();
let (addr, port) = saved_addr.split_once(":").unwrap();
(addr.into(), port.into())
}

View file

@ -12,12 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod stratum;
mod mine_block;
mod node;
pub use node::Node;
mod config;
mod stratum;
mod mine_block;
pub use config::{NodeConfig, PeersConfig};
pub use config::*;

View file

@ -24,21 +24,28 @@ use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;
use crate::node::NodeConfig;
use crate::wallet::WalletList;
lazy_static! {
/// Static settings state to be accessible globally.
static ref SETTINGS_STATE: Arc<Settings> = Arc::new(Settings::init());
}
/// Application configuration file name.
const APP_CONFIG_FILE_NAME: &'static str = "app.toml";
/// Default external node URL.
const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413";
/// 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
chain_type: ChainTypes,
/// URLs of external nodes for wallets.
external_nodes_urls: Vec<String>
}
impl Default for AppConfig {
@ -46,12 +53,15 @@ impl Default for AppConfig {
Self {
auto_start_node: false,
chain_type: ChainTypes::default(),
external_nodes_urls: vec![
DEFAULT_EXTERNAL_NODE_URL.to_string()
],
}
}
}
impl AppConfig {
/// Initialize application config from the disk.
/// Initialize application config from the file.
pub fn init() -> Self {
let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None);
let parsed = Settings::read_from_file::<AppConfig>(path.clone());
@ -64,7 +74,7 @@ impl AppConfig {
}
}
/// Save app config to disk.
/// Save app config to file.
pub fn save(&self) {
Settings::write_to_file(self, Settings::get_config_path(APP_CONFIG_FILE_NAME, None));
}
@ -82,6 +92,9 @@ impl AppConfig {
let node_config = NodeConfig::for_chain_type(chain_type);
w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers;
// Reload wallets.
WalletList::reload(chain_type);
}
}
@ -104,14 +117,31 @@ impl AppConfig {
w_app_config.auto_start_node = !autostart;
w_app_config.save();
}
/// Get external nodes URLs.
pub fn external_nodes_urls() -> Vec<String> {
let r_config = Settings::app_config_to_read();
r_config.external_nodes_urls.clone()
}
/// Add external node URL.
pub fn add_external_node_url(address: String) {
let mut w_config = Settings::app_config_to_update();
w_config.external_nodes_urls.insert(0, address);
w_config.save();
}
}
const WORKING_DIRECTORY_NAME: &'static str = ".grim";
/// Main application directory name.
const MAIN_DIR_NAME: &'static str = ".grim";
/// Provides access to app and node configs.
/// Provides access to application, integrated node and wallets configs.
pub struct Settings {
/// Application config instance.
app_config: Arc<RwLock<AppConfig>>,
node_config: Arc<RwLock<NodeConfig>>
/// Integrated node config instance.
node_config: Arc<RwLock<NodeConfig>>,
}
impl Settings {
@ -120,7 +150,7 @@ impl Settings {
let app_config = AppConfig::init();
Self {
node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))),
app_config: Arc::new(RwLock::new(app_config))
app_config: Arc::new(RwLock::new(app_config)),
}
}
@ -144,16 +174,16 @@ impl Settings {
SETTINGS_STATE.app_config.write().unwrap()
}
/// Get working directory path for the application.
pub fn get_working_path(chain_type: Option<&ChainTypes>) -> PathBuf {
/// Get base directory path for config.
pub fn get_base_path(sub_dir: Option<String>) -> PathBuf {
// Check if dir exists.
let mut path = match dirs::home_dir() {
Some(p) => p,
None => PathBuf::new(),
};
path.push(WORKING_DIRECTORY_NAME);
if chain_type.is_some() {
path.push(chain_type.unwrap().shortname());
path.push(MAIN_DIR_NAME);
if sub_dir.is_some() {
path.push(sub_dir.unwrap());
}
// Create if the default path doesn't exist.
if !path.exists() {
@ -162,15 +192,14 @@ impl Settings {
path
}
/// Get config file path from provided name and [`ChainTypes`] if needed.
pub fn get_config_path(config_name: &str, chain_type: Option<&ChainTypes>) -> PathBuf {
let main_path = Self::get_working_path(chain_type);
let mut settings_path = main_path.clone();
/// Get config file path from provided name and sub-directory if needed.
pub fn get_config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut settings_path = Self::get_base_path(sub_dir);
settings_path.push(config_name);
settings_path
}
/// Read config from a file
/// Read config from the file.
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
let file_content = fs::read_to_string(config_path.clone())?;
let parsed = toml::from_str::<T>(file_content.as_str());
@ -185,7 +214,7 @@ impl Settings {
}
}
/// Write config to a file
/// Write config to the file.
pub fn write_to_file<T: Serialize>(config: &T, path: PathBuf) {
let conf_out = toml::to_string(config).unwrap();
let mut file = File::create(path.to_str().unwrap()).unwrap();

95
src/wallet/config.rs Normal file
View file

@ -0,0 +1,95 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ffi::OsString;
use std::path::PathBuf;
use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings};
use crate::wallet::WalletList;
/// Wallet configuration.
#[derive(Serialize, Deserialize, Clone)]
pub struct WalletConfig {
/// Identifier for a wallet.
id: OsString,
/// Readable wallet name.
name: String,
/// External node connection URL.
external_node_url: Option<String>,
}
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
impl WalletConfig {
/// Create wallet config.
pub fn create(id: OsString, name: String) -> WalletConfig {
let config_path = Self::get_config_path(&id);
let config = WalletConfig {
id,
name,
external_node_url: None,
};
Settings::write_to_file(&config, config_path);
config
}
/// Load config from provided wallet dir.
pub fn load(wallet_dir: PathBuf) -> Option<WalletConfig> {
let mut config_path: PathBuf = wallet_dir.clone();
config_path.push(CONFIG_FILE_NAME);
if let Ok(config) = Settings::read_from_file::<WalletConfig>(config_path) {
return Some(config)
}
None
}
/// Get config file path for provided wallet identifier.
fn get_config_path(id: &OsString) -> PathBuf {
let chain_type = AppConfig::chain_type();
let mut config_path = WalletList::get_wallets_base_dir(&chain_type);
config_path.push(id);
config_path.push(CONFIG_FILE_NAME);
config_path
}
/// Save wallet config.
fn save(&self) {
let config_path = Self::get_config_path(&self.id);
Settings::write_to_file(self, config_path);
}
/// Get readable wallet name.
pub fn get_name(&self) -> &String {
&self.name
}
/// Set readable wallet name.
pub fn set_name(&mut self, name: String) {
self.name = name;
self.save();
}
/// Get external node connection URL.
pub fn get_external_node_url(&self) -> &Option<String> {
&self.external_node_url
}
/// Set external node connection URL.
pub fn set_external_node_url(&mut self, url: Option<String>) {
self.external_node_url = url;
self.save();
}
}

81
src/wallet/list.rs Normal file
View file

@ -0,0 +1,81 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use grin_core::global::ChainTypes;
use lazy_static::lazy_static;
use crate::{AppConfig, Settings};
use crate::wallet::Wallet;
lazy_static! {
/// Global wallets state.
static ref WALLETS_STATE: Arc<RwLock<WalletList >> = Arc::new(RwLock::new(WalletList::load()));
}
/// List of created wallets.
pub struct WalletList {
list: Vec<Wallet>
}
/// Base wallets directory name.
pub const BASE_DIR_NAME: &'static str = "wallets";
impl WalletList {
/// Load list of wallets.
fn load() -> Self {
Self { list: Self::load_wallets(&AppConfig::chain_type()) }
}
/// Load wallets for provided [`ChainType`].
fn load_wallets(chain_type: &ChainTypes) -> Vec<Wallet> {
let mut wallets = Vec::new();
let wallets_dir = Self::get_wallets_base_dir(chain_type);
// Load wallets from directory.
for dir in wallets_dir.read_dir().unwrap() {
let wallet = Wallet::load(dir.unwrap().path());
if let Some(w) = wallet {
wallets.push(w);
}
continue;
}
wallets
}
/// Get wallets base directory for provided [`ChainTypes`].
pub fn get_wallets_base_dir(chain_type: &ChainTypes) -> PathBuf {
let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname()));
wallets_path.push(BASE_DIR_NAME);
// Create wallets directory if it doesn't exist.
if !wallets_path.exists() {
let _ = fs::create_dir_all(wallets_path.clone());
}
wallets_path
}
/// Get list of wallets.
pub fn list() -> Vec<Wallet> {
let r_state = WALLETS_STATE.read().unwrap();
r_state.list.clone()
}
/// Reload list of wallets for provided [`ChainTypes`].
pub fn reload(chain_type: &ChainTypes) {
let mut w_state = WALLETS_STATE.write().unwrap();
w_state.list = Self::load_wallets(chain_type);
}
}

View file

@ -13,5 +13,10 @@
// limitations under the License.
mod wallet;
pub use wallet::Wallet;
// pub use self::wallet::{init, init_from_seed};
mod config;
pub use config::*;
mod list;
pub use list::WalletList;

View file

@ -12,3 +12,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ffi::OsString;
use std::path::PathBuf;
use crate::node::NodeConfig;
use crate::wallet::WalletConfig;
/// Wallet loaded from config.
#[derive(Clone)]
pub struct Wallet {
/// Identifier for a wallet, name of wallet directory.
id: OsString,
/// Base path for wallet data.
pub(crate) path: String,
/// Loaded file config.
pub(crate) config: WalletConfig,
}
impl Wallet {
/// Create new wallet from provided name.
pub fn create(name: String ) {
}
/// Load wallet from provided data path.
pub fn load(data_path: PathBuf) -> Option<Wallet> {
if !data_path.is_dir() {
return None;
}
let wallet_config = WalletConfig::load(data_path.clone());
if let Some(config) = wallet_config {
// Set id as wallet directory name.
let id = data_path.file_name().unwrap().to_os_string();
let path = data_path.to_str().unwrap().to_string();
return Some(Self { id, path, config });
}
None
}
/// Get wallet node connection URL.
pub fn get_connection_url(&self) -> String {
match self.config.get_external_node_url() {
None => {
format!("http://{}", NodeConfig::get_api_address())
}
Some(url) => url.to_string()
}
}
}