Ability to change data location (#50)

Closes https://code.gri.mw/GUI/grim/issues/9
- Change node chain data directory
- Change wallet data directory
- Check for valid bridge Tor binary

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/50
This commit is contained in:
ardocrat
2026-02-27 00:29:55 +00:00
parent 366bbaeac6
commit ee4752a95f
20 changed files with 567 additions and 144 deletions
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, S
confirmation: Bestätigung
enter_url: URL eingeben
max_short: MAX
files_location: Dateistandort
moving_files: Dateien verschieben
wrong_path_error: Falscher Weg angegeben
wallets:
await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: Application closed unexpectedly last time, you can share c
confirmation: Confirmation
enter_url: Enter URL
max_short: MAX
files_location: Files location
moving_files: Moving files
wrong_path_error: Wrong path specified
wallets:
await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: L'application s'est fermée de manière inattendue la dern
confirmation: Confirmation
enter_url: Entrez l'URL
max_short: MAX
files_location: Emplacement du fichier
moving_files: Déplacer des fichiers
wrong_path_error: Chemin incorrect spécifié
wallets:
await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: В прошлый раз приложение неожид
confirmation: Подтверждение
enter_url: Введите URL-адрес
max_short: МАКС
files_location: Расположение файлов
moving_files: Перемещение файлов
wrong_path_error: Указан неправильный путь
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlen
confirmation: Onay
enter_url: URL'yi girin
max_short: MAKS
files_location: Dosya konumu
moving_files: Dosyalari Tasima
wrong_path_error: Yanlis yol belirtildi
wallets:
await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor
+3
View File
@@ -33,6 +33,9 @@ crash_report_warning: 上次应用程序意外关闭,您可以报告开发人
confirmation: 确认
enter_url: 输入 URL
max_short: 最大數量
files_location: 檔案位置
moving_files: 檔案移動
wrong_path_error: 指定錯誤路徑
wallets:
await_conf_amount: 等待确认中
await_fin_amount: 等待确定中
+8 -3
View File
@@ -194,7 +194,7 @@ impl Content {
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
@@ -203,7 +203,12 @@ impl Content {
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("sync_status.shutdown"))
let exit_status_text = if Node::data_dir_changing() {
t!("moving_files")
} else {
t!("sync_status.shutdown")
};
ui.label(RichText::new(exit_status_text)
.size(17.0)
.color(Colors::text(false)));
});
@@ -228,7 +233,7 @@ impl Content {
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
+41 -33
View File
@@ -39,6 +39,8 @@ pub struct FilePickContent {
/// Flag to check if file is picking.
file_picking: Arc<AtomicBool>,
/// Flag to check if folder should be picked.
pick_folder: bool,
/// Flag to parse file content after pick.
parse_file: bool,
/// Flag to check if file is parsing.
@@ -54,12 +56,19 @@ impl FilePickContent {
content_type,
active: false,
file_picking: Arc::new(AtomicBool::new(false)),
pick_folder: false,
parse_file: true,
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None)),
}
}
/// Pick folder.
pub fn pick_folder(mut self) -> Self {
self.pick_folder = true;
self
}
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
@@ -79,7 +88,11 @@ impl FilePickContent {
if let Some(path) = cb.picked_file() {
self.file_picking.store(false, Ordering::Relaxed);
if !path.is_empty() {
self.on_file_pick(path);
if self.parse_file {
self.parse_file(path);
} else {
pick(path);
}
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
@@ -106,29 +119,15 @@ impl FilePickContent {
match self.content_type {
FilePickContentType::Button => {
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui,
text,
Colors::blue(),
Colors::white_or_black(false),
|| {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
pick(path);
return;
}
self.on_file_pick(path);
}
});
let text_color = Colors::blue();
let fill = Colors::white_or_black(false);
View::colored_text_button(ui, text, text_color, fill, || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
pick(path);
return;
}
self.on_file_pick(path);
}
self.on_file_pick(pick, cb);
});
}
FilePickContentType::Tab => {
@@ -138,30 +137,39 @@ impl FilePickContent {
false => None
};
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
pick(path);
return;
}
self.on_file_pick(path);
}
self.on_file_pick(pick, cb);
});
}
}
}
}
/// Handle picked file path.
fn on_file_pick(&self, path: String) {
/// Handle pick file request.
fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) {
let path = if self.pick_folder {
cb.pick_folder()
} else {
cb.pick_file()
};
if path.is_none() {
return;
}
let path = path.unwrap();
// Wait for asynchronous file pick result if path is empty.
if path.is_empty() {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Do not parse result.
if !self.parse_file {
return;
// Parse result if needed.
if self.parse_file {
self.parse_file(path);
} else {
on_pick(path);
}
}
/// Handle picked file path.
fn parse_file(&self, path: String) {
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
+1 -1
View File
@@ -274,7 +274,7 @@ impl NetworkContent {
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing();
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
let title_content = if show_settings {
TitleContentType::Title(t!("settings"))
} else if !show_connections {
+118 -9
View File
@@ -12,21 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Id, Layout, RichText};
use grin_core::global::ChainTypes;
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, PLUG, POWER, SHIELD, SHIELD_SLASH};
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::network::NetworkContent;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig};
use crate::AppConfig;
/// Integrated node general setup section content.
pub struct NodeSetup {
/// Data path value value for [`Modal`].
data_path_edit: String,
/// Button to pick directory for chain data.
pick_data_dir: FilePickContent,
/// IP Addresses available at system.
available_ips: Vec<String>,
@@ -45,20 +52,26 @@ pub struct NodeSetup {
ftl_edit: String,
}
/// Identifier for chain data path value [`Modal`].
const DATA_PATH_MODAL: &'static str = "node_data_path";
/// Identifier for API port value [`Modal`].
pub const API_PORT_MODAL: &'static str = "api_port";
const API_PORT_MODAL: &'static str = "node_api_port";
/// Identifier for API secret value [`Modal`].
pub const API_SECRET_MODAL: &'static str = "api_secret";
const API_SECRET_MODAL: &'static str = "node_api_secret";
/// Identifier for Foreign API secret value [`Modal`].
pub const FOREIGN_API_SECRET_MODAL: &'static str = "foreign_api_secret";
const FOREIGN_API_SECRET_MODAL: &'static str = "node_foreign_api_secret";
/// Identifier for FTL value [`Modal`].
pub const FTL_MODAL: &'static str = "ftl";
const FTL_MODAL: &'static str = "node_ftl";
impl Default for NodeSetup {
fn default() -> Self {
let (api_ip, api_port) = NodeConfig::get_api_ip_port();
let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port);
Self {
data_path_edit: NodeConfig::get_chain_data_path(),
pick_data_dir: FilePickContent::new(
FilePickContentType::ItemButton(View::item_rounding(0, 1, true))
).no_parse().pick_folder(),
available_ips: NodeConfig::get_ip_addrs(),
api_port_edit: api_port,
api_port_available_edit: is_api_port_available,
@@ -72,6 +85,7 @@ impl Default for NodeSetup {
impl ContentContainer for NodeSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
DATA_PATH_MODAL,
API_PORT_MODAL,
API_SECRET_MODAL,
FOREIGN_API_SECRET_MODAL,
@@ -84,6 +98,7 @@ impl ContentContainer for NodeSetup {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb),
API_PORT_MODAL => self.api_port_modal(ui, modal, cb),
API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
@@ -92,7 +107,7 @@ impl ContentContainer for NodeSetup {
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
@@ -102,7 +117,8 @@ impl ContentContainer for NodeSetup {
ui.add_space(2.0);
// 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()
|| Node::data_dir_changing() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
@@ -154,6 +170,13 @@ impl ContentContainer for NodeSetup {
});
ui.add_space(6.0);
// Show data location selection for Desktop when it already started or turned off.
if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() &&
View::is_desktop() {
self.pick_data_dir_ui(ui, cb);
ui.add_space(6.0);
}
if self.available_ips.is_empty() {
// Show message when IP addresses are not available on the system.
NetworkSettings::no_ip_address_ui(ui);
@@ -212,6 +235,92 @@ impl ContentContainer for NodeSetup {
}
impl NodeSetup {
/// Draw content to change chain data directory.
fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
Node::change_data_dir(path);
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.data_path_edit = NodeConfig::get_chain_data_path();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network.node"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let path = NodeConfig::get_chain_data_path();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
}
/// Draw data path input [`Modal`] content.
fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let on_save = |path: &String| {
Node::change_data_dir(path.clone());
Modal::close();
};
ui.label(RichText::new(format!("{}:", t!("files_location")))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw chain data path text edit.
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
edit.ui(ui, &mut self.data_path_edit, cb);
if edit.enter_pressed {
on_save(&self.data_path_edit);
}
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);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(&self.data_path_edit);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw [`ChainTypes`] setup content.
pub fn chain_type_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
+5
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fs;
use egui::{Align, Id, Layout, RichText, StrokeKind};
use egui::os::OperatingSystem;
use url::Url;
@@ -412,6 +413,10 @@ impl TorSettingsContent {
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let bridge = TorConfig::get_bridge().unwrap();
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
if !exists {
return;
}
if bridge.binary_path() != c.bridge_bin_path_edit {
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
c.settings_changed = true;
+1 -1
View File
@@ -542,7 +542,7 @@ impl WalletsContent {
ui.painter().rect(rect, rounding, bg, View::item_stroke(), StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if !wallet.is_open() {
if !wallet.is_open() && !wallet.files_moving() {
// Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.show_opening_modal(wallet, None, cb);
+4 -2
View File
@@ -473,7 +473,7 @@ fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.is_closing() {
} else if wallet.is_closing() || wallet.files_moving() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.get_current_connection() == ConnectionMethod::Integrated {
@@ -539,7 +539,9 @@ fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress();
if wallet.is_closing() {
if wallet.files_moving() {
t!("moving_files")
} else if wallet.is_closing() {
t!("wallets.wallet_closing")
} else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE)
+174 -59
View File
@@ -12,14 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use eframe::emath::Align;
use eframe::epaint::StrokeKind;
use egui::{Id, Layout, RichText};
use crate::gui::Colors;
use crate::gui::icons::{CLOCK_COUNTDOWN, PASSWORD, PENCIL};
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::Wallet;
/// Common wallet settings content.
@@ -34,6 +36,11 @@ pub struct CommonSettings {
/// New wallet password [`Modal`] value.
new_pass_edit: String,
/// Data path value value for [`Modal`].
data_path_edit: String,
/// Button to pick directory for wallet data.
pick_data_dir: FilePickContent,
/// Minimum confirmations number value.
min_confirmations_edit: String,
}
@@ -42,6 +49,8 @@ pub struct CommonSettings {
const NAME_EDIT_MODAL: &'static str = "wallet_name_edit_modal";
/// Identifier for wallet password [`Modal`].
const PASS_EDIT_MODAL: &'static str = "wallet_pass_edit_modal";
/// Identifier for wallet data path value [`Modal`].
const DATA_PATH_MODAL: &'static str = "wallet_data_path";
/// Identifier for minimum confirmations [`Modal`].
const MIN_CONFIRMATIONS_EDIT_MODAL: &'static str = "wallet_min_conf_edit_modal";
@@ -50,6 +59,7 @@ impl WalletContentContainer for CommonSettings {
vec![
NAME_EDIT_MODAL,
PASS_EDIT_MODAL,
DATA_PATH_MODAL,
MIN_CONFIRMATIONS_EDIT_MODAL
]
}
@@ -60,66 +70,25 @@ impl WalletContentContainer for CommonSettings {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
NAME_EDIT_MODAL => {
self.name_modal_ui(ui, wallet, modal, cb);
}
PASS_EDIT_MODAL => {
self.pass_modal_ui(ui, wallet, modal, cb);
}
MIN_CONFIRMATIONS_EDIT_MODAL => {
self.min_conf_modal_ui(ui, wallet, modal, cb);
}
NAME_EDIT_MODAL => self.name_modal_ui(ui, wallet, modal, cb),
PASS_EDIT_MODAL => self.pass_modal_ui(ui, wallet, modal, cb),
DATA_PATH_MODAL => self.data_path_modal_ui(ui, wallet, cb),
MIN_CONFIRMATIONS_EDIT_MODAL => self.min_conf_modal_ui(ui, wallet, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) {
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let config = wallet.get_config();
// Show wallet name.
ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.name"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(2.0);
ui.label(RichText::new(&config.name)
.size(16.0)
.color(Colors::white_or_black(true)));
ui.add_space(8.0);
// Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::white_or_black(false), || {
self.name_edit = config.name;
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.pass")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
// Show wallet password setup.
let pass_text = format!("{} {}", PASSWORD, t!("change"));
View::button(ui, pass_text, Colors::white_or_black(false), || {
// Setup modal values.
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
self.name_ui(ui, config.name);
// Show data dir for desktop.
if View::is_desktop() {
ui.add_space(-4.0);
self.data_dir_ui(ui, wallet, cb);
}
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.min_tx_conf_count")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
@@ -135,7 +104,7 @@ impl WalletContentContainer for CommonSettings {
.show();
});
ui.add_space(12.0);
ui.add_space(8.0);
// Setup ability to post wallet transactions with Dandelion.
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
@@ -144,7 +113,7 @@ impl WalletContentContainer for CommonSettings {
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.add_space(4.0);
});
}
}
@@ -156,12 +125,69 @@ impl Default for CommonSettings {
wrong_pass: false,
old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(),
data_path_edit: "".to_string(),
pick_data_dir: FilePickContent::new(
FilePickContentType::ItemButton(View::item_rounding(1, 2, true))
).no_parse().pick_folder(),
min_confirmations_edit: "".to_string(),
}
}
}
impl CommonSettings {
/// Draw content to change wallet name and password.
fn name_ui(&mut self, ui: &mut egui::Ui, name: String) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
View::item_button(ui, View::item_rounding(0, 2, true), PASSWORD, None, || {
self.old_pass_edit = "".to_string();
self.new_pass_edit = "".to_string();
self.wrong_pass = false;
// Show wallet password modal.
Modal::new(PASS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.name_edit = name.clone();
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, name, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
}
/// Draw wallet name [`Modal`] content.
fn name_modal_ui(&mut self,
ui: &mut egui::Ui,
@@ -307,6 +333,95 @@ impl CommonSettings {
});
}
/// Draw content to change wallet data directory.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
wallet.change_data_path(path);
});
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.wallet"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let path = wallet.get_config().data_path.unwrap_or_default();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.0);
});
});
});
}
/// Draw data path input [`Modal`] content.
fn data_path_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let on_save = |path: &String| {
wallet.change_data_path(path.clone());
Modal::close();
};
ui.label(RichText::new(format!("{}:", t!("files_location")))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw chain data path text edit.
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
edit.ui(ui, &mut self.data_path_edit, cb);
if edit.enter_pressed {
on_save(&self.data_path_edit);
}
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);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(&self.data_path_edit);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw wallet name [`Modal`] content.
fn min_conf_modal_ui(&mut self,
ui: &mut egui::Ui,
@@ -107,7 +107,7 @@ impl WalletContentContainer for RecoverySettings {
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
Colors::green(),
Colors::white_or_black(false), || {
wallet.delete_db(true);
wallet.delete_db();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.restore_wallet_desc"))
@@ -119,7 +119,12 @@ impl WalletTransportContent {
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Show button to show QR code address.
View::item_button(ui, View::item_rounding(1, 2, true), QR_CODE, None, || {
let r = if awaiting_balance {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, r, QR_CODE, None, || {
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false)
.with_max_size(320.0));
});
+13
View File
@@ -304,6 +304,19 @@ impl NodeConfig {
true
}
/// Get chain data path.
pub fn get_chain_data_path() -> String {
let r_config = Settings::node_config_to_read();
r_config.node.server.db_root.clone()
}
/// Save chain data path.
pub fn save_chain_data_path(path: String) {
let mut w_config = Settings::node_config_to_update();
w_config.node.server.db_root = path;
w_config.save();
}
/// Get stratum server IP address and port.
pub fn get_stratum_address() -> (String, String) {
let r_config = Settings::node_config_to_read();
+57 -10
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{fs, thread};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
@@ -59,6 +59,8 @@ pub struct Node {
exit_after_stop: AtomicBool,
/// Flag to reset peers data and restart the [`Server`].
reset_peers: AtomicBool,
/// Flag to change data directory and restart the [`Server`].
change_data_dir: AtomicBool,
/// An error occurred on [`Server`] start.
error: Arc<RwLock<Option<Error>>>
@@ -77,6 +79,7 @@ impl Default for Node {
start_stratum_needed: AtomicBool::new(false),
error: Arc::new(RwLock::new(None)),
reset_peers: AtomicBool::new(false),
change_data_dir: AtomicBool::new(false),
}
}
}
@@ -187,10 +190,10 @@ impl Node {
/// Check if [`Server`] is not syncing (disabled or just running after synchronization).
pub fn not_syncing() -> bool {
return match Node::get_sync_status() {
match Node::get_sync_status() {
None => true,
Some(ss) => ss == SyncStatus::NoSync
};
}
}
/// Get synchronization status, empty when [`Server`] is not running.
@@ -218,7 +221,7 @@ impl Node {
let r_err = NODE_STATE.error.read();
if r_err.is_some() {
let e = r_err.as_ref().unwrap();
// Setup a flag to show an error to clean up data.
// Flag setup to show an error to clean up data.
let store_err = match e {
Error::Store(_) => true,
Error::Chain(_) => true,
@@ -228,7 +231,7 @@ impl Node {
return Some(NodeError::Storage);
}
// Setup a flag to show P2P or API server error.
// Flag setup to show P2P or API server error.
let p2p_api_err = match e {
Error::P2P(_) => Some(NodeError::P2P),
Error::API(_) => Some(NodeError::API),
@@ -238,7 +241,7 @@ impl Node {
return p2p_api_err;
}
// Setup a flag to show configuration error.
// Flag setup to show configuration error.
let config_err = match e {
Error::Configuration(_) => true,
_ => false
@@ -282,7 +285,7 @@ impl Node {
NODE_STATE.restart_needed.store(false, Ordering::Relaxed);
}
Err(e) => {
// Setup an error.
// Set an error.
{
let mut w_err = NODE_STATE.error.write();
*w_err = Some(e);
@@ -336,7 +339,7 @@ impl Node {
}
}
Err(e) => {
// Setup an error.
// Set an error.
{
let mut w_err = NODE_STATE.error.write();
*w_err = Some(e);
@@ -372,6 +375,48 @@ impl Node {
}
}
/// Change chain data directory.
pub fn change_data_dir(path: String) {
if Self::data_dir_changing() || NodeConfig::get_chain_data_path() == path {
return;
}
NODE_STATE.change_data_dir.store(true, Ordering::Relaxed);
thread::spawn(move || {
let running = Node::is_running();
if running {
Node::stop(false);
// Wait node to stop before moving files.
while Node::is_running() {
thread::sleep(Self::STATS_UPDATE_DELAY);
}
}
let cfg_path = NodeConfig::get_chain_data_path();
let old = Path::new(cfg_path.as_str());
let new = Path::new(path.as_str());
if !old.exists() {
NodeConfig::save_chain_data_path(path);
} else {
fs::create_dir_all(&new).unwrap_or_default();
if let Ok(_) = fs::rename(old, new) {
NodeConfig::save_chain_data_path(path);
} else {
fs::remove_dir_all(old).unwrap();
NodeConfig::save_chain_data_path(path);
}
}
NODE_STATE.change_data_dir.store(false, Ordering::Relaxed);
// Restart node after migration.
if running && !Node::is_stopping() {
Node::start();
}
});
}
/// Check if chain data directory is changing.
pub fn data_dir_changing() -> bool {
NODE_STATE.change_data_dir.load(Ordering::Relaxed)
}
/// Clean-up [`Server`] data if server is not running.
pub fn clean_up_data() {
if Self::is_running() {
@@ -412,14 +457,16 @@ impl Node {
/// Get synchronization status i18n text.
pub fn get_sync_status_text() -> String {
if Node::data_dir_changing() {
return t!("moving_files");
}
if Node::is_stopping() {
return t!("sync_status.shutdown");
};
if Node::is_starting() {
return t!("sync_status.initial");
};
if Node::is_restarting() {
return t!("sync_status.node_restarting");
}
+58 -11
View File
@@ -48,14 +48,18 @@ pub struct WalletConfig {
pub api_port: Option<u16>,
/// Delay in blocks before another transaction broadcasting attempt.
pub tx_broadcast_timeout: Option<u64>,
/// Wallet data directory path.
pub data_path: Option<String>,
/// Config version.
ver: Option<i32>
}
/// Base wallets directory name.
const BASE_DIR_NAME: &'static str = "wallets";
/// Base wallets directory name.
const DB_DIR_NAME: &'static str = "db";
/// Wallet data directory name.
const DATA_DIR_NAME: &'static str = "wallet_data";
/// Wallet configuration file name.
const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml";
/// Slatepacks directory name.
@@ -66,7 +70,12 @@ const SEED_FILE: &str = "wallet.seed";
/// Default value of minimal amount of confirmations.
const MIN_CONFIRMATIONS_DEFAULT: u64 = 10;
const WALLET_CONFIG_VERSION: i32 = 1;
impl WalletConfig {
/// Wallet data directory name.
pub const DATA_DIR_NAME: &'static str = "wallet_data";
/// Default account name value.
pub const DEFAULT_ACCOUNT_LABEL: &'static str = "default";
@@ -94,6 +103,8 @@ impl WalletConfig {
enable_tor_listener: Some(false),
api_port: Some(rand::rng().random_range(10000..30000)),
tx_broadcast_timeout: Some(Self::BROADCASTING_TIMEOUT_DEFAULT),
data_path: Some(Self::wallet_path(id.to_string())),
ver: Some(WALLET_CONFIG_VERSION),
};
Settings::write_to_file(&config, config_path);
config
@@ -103,7 +114,8 @@ impl WalletConfig {
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) {
if let Ok(mut config) = Settings::read_from_file::<WalletConfig>(config_path) {
config.migrate();
return Some(config)
}
None
@@ -169,24 +181,32 @@ impl WalletConfig {
config_path
}
/// Get current wallet path.
pub fn get_wallet_path(&self) -> String {
/// Get wallet path from provided identifier.
fn wallet_path(id: String) -> String {
let chain_type = AppConfig::chain_type();
let mut data_path = Self::get_base_path(chain_type);
data_path.push(self.id.to_string());
data_path.push(id);
data_path.to_str().unwrap().to_string()
}
/// Get current wallet path.
pub fn get_wallet_path(&self) -> String {
Self::wallet_path(self.id.to_string())
}
/// Get wallet data path.
pub fn get_data_path(&self) -> String {
let mut data_path = PathBuf::from(self.get_wallet_path());
data_path.push(DATA_DIR_NAME);
data_path.to_str().unwrap().to_string()
if let Some(path) = &self.data_path {
path.clone()
} else {
self.get_wallet_path()
}
}
/// Get wallet seed path.
pub fn seed_path(&self) -> String {
let mut path = PathBuf::from(self.get_data_path());
path.push(Self::DATA_DIR_NAME);
path.push(SEED_FILE);
path.to_str().unwrap().to_string()
}
@@ -194,13 +214,15 @@ impl WalletConfig {
/// Get wallet database data path.
pub fn get_db_path(&self) -> String {
let mut path = PathBuf::from(self.get_data_path());
path.push(Self::DATA_DIR_NAME);
path.push(DB_DIR_NAME);
path.to_str().unwrap().to_string()
}
/// Get Slatepack file path for transaction.
pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf {
let mut path = PathBuf::from(self.get_wallet_path());
let mut path = PathBuf::from(self.get_data_path());
path.push(Self::DATA_DIR_NAME);
path.push(SLATEPACKS_DIR_NAME);
if !path.exists() {
let _ = fs::create_dir_all(path.clone());
@@ -212,7 +234,8 @@ impl WalletConfig {
/// Get Slatepack file path for Slate.
pub fn get_slate_path(&self, slate: &Slate) -> PathBuf {
let mut path = PathBuf::from(self.get_wallet_path());
let mut path = PathBuf::from(self.get_data_path());
path.push(Self::DATA_DIR_NAME);
path.push(SLATEPACKS_DIR_NAME);
if !path.exists() {
let _ = fs::create_dir_all(path.clone());
@@ -231,4 +254,28 @@ impl WalletConfig {
}
path.to_str().unwrap().to_string()
}
/// Check config version to migrate if needed.
fn migrate(&mut self) {
match self.ver {
None => {
// Migrate Slatepack data.
let mut old_slate_path = PathBuf::from(self.get_wallet_path());
old_slate_path.push(SLATEPACKS_DIR_NAME);
if old_slate_path.exists() {
let mut new_slate_path = PathBuf::from(self.get_data_path());
new_slate_path.push(SLATEPACKS_DIR_NAME);
let _ = fs::rename(&old_slate_path, &new_slate_path);
}
// Write data path to config.
if self.data_path.is_none() {
self.data_path = Some(self.get_wallet_path());
}
// Migrate to 1st version.
self.ver = Some(1);
}
Some(_) => {}
}
self.save();
}
}
+62 -13
View File
@@ -93,7 +93,7 @@ pub struct Wallet {
/// Running wallet foreign API server and port.
foreign_api_server: Arc<RwLock<Option<(ApiServer, u16)>>>,
/// Running wallet foreign API server and port.
/// Wallet secret key for transport service.
secret_key: Arc<RwLock<Option<SecretKey>>>,
/// Flag to check if wallet repairing and restoring missing outputs is needed.
@@ -101,6 +101,9 @@ pub struct Wallet {
/// Wallet repair progress in percents.
repair_progress: Arc<AtomicU8>,
/// Flag to check if wallet files are moving.
files_moving: Arc<AtomicBool>,
/// Flag to check if Slatepack message file is opening.
message_opening: Arc<AtomicBool>,
@@ -142,6 +145,7 @@ impl Wallet {
syncing: Arc::new(AtomicBool::new(false)),
repair_needed: Arc::new(AtomicBool::new(false)),
repair_progress: Arc::new(AtomicU8::new(0)),
files_moving: Arc::new(AtomicBool::new(false)),
message_opening: Arc::new(AtomicBool::from(false)),
send_creating: Arc::new(AtomicBool::new(false)),
fee_calculating: Arc::new(AtomicU8::new(0)),
@@ -190,7 +194,7 @@ impl Wallet {
/// Initialize [`Wallet`] from provided data path.
pub fn init(data_path: PathBuf) -> Option<Wallet> {
let wallet_config = WalletConfig::load(data_path.clone());
let wallet_config = WalletConfig::load(data_path);
if let Some(config) = wallet_config {
return Some(Wallet::new(config));
}
@@ -284,7 +288,7 @@ impl Wallet {
let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap())
as Box<dyn WalletInst<'static, L, C, K>>;
let lc = wallet.lc_provider()?;
lc.set_top_level_directory(config.get_wallet_path().as_str())?;
lc.set_top_level_directory(config.get_data_path().as_str())?;
Ok(Arc::new(Mutex::new(wallet)))
}
@@ -1089,28 +1093,73 @@ impl Wallet {
self.repair_progress.load(Ordering::Relaxed)
}
/// Deleting wallet database files.
pub fn delete_db(&self, reopen: bool) {
let wallet_delete = self.clone();
/// Change wallet data path, migrating all files to new directory.
pub fn change_data_path(&self, path: String) {
let wallet = self.clone();
wallet.files_moving.store(true, Ordering::Relaxed);
// Close wallet if open.
if self.is_open() {
self.close();
}
thread::spawn(move || {
// Wait wallet to be closed.
if wallet_delete.is_open() {
thread::sleep(Duration::from_millis(300));
while wallet.is_open() || wallet.syncing() {
thread::sleep(Duration::from_millis(100));
}
// Move wallet db files.
if let Some(old_path) = wallet.get_config().data_path {
let mut old = PathBuf::from(old_path.as_str());
old.push(WalletConfig::DATA_DIR_NAME);
let mut new = PathBuf::from(path.as_str());
new.push(WalletConfig::DATA_DIR_NAME);
if old.exists() {
fs::create_dir_all(&new).unwrap_or_default();
if let Ok(_) = fs::rename(old.as_path(), new.as_path()) {
// Save new path to config.
let mut w_config = wallet.config.write();
w_config.data_path = Some(path);
w_config.save();
}
}
}
wallet.files_moving.store(false, Ordering::Relaxed);
// Mark wallet to reopen.
if !wallet.is_open() {
wallet.set_reopen(true);
}
});
}
/// Deleting wallet database files.
pub fn delete_db(&self) {
let wallet = self.clone();
wallet.files_moving.store(true, Ordering::Relaxed);
// Close wallet if open.
if self.is_open() {
self.close();
}
thread::spawn(move || {
// Wait wallet to be closed.
while wallet.is_open() || wallet.syncing() {
thread::sleep(Duration::from_millis(100));
}
// Remove wallet db files.
let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path());
// Start sync to close thread.
wallet_delete.sync();
wallet_delete.repair();
let _ = fs::remove_dir_all(wallet.get_config().get_db_path());
wallet.files_moving.store(false, Ordering::Relaxed);
// Mark wallet to repair.
wallet.repair();
// Mark wallet to reopen.
wallet_delete.set_reopen(reopen);
if !wallet.is_open() {
wallet.set_reopen(true);
}
});
}
/// Check if data files are moving.
pub fn files_moving(&self) -> bool {
self.files_moving.load(Ordering::Relaxed)
}
/// Get recovery phrase.
pub fn get_recovery(&self, password: String) -> Result<ZeroingString, Error> {
let r_inst = self.instance.as_ref().read();