diff --git a/Cargo.lock b/Cargo.lock index 2feae6d..039a622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,9 +775,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -1542,9 +1542,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "git2" @@ -1589,6 +1589,17 @@ dependencies = [ "regex", ] +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "glow" version = "0.11.2" @@ -1603,9 +1614,9 @@ dependencies = [ [[package]] name = "glutin" -version = "0.30.8" +version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f9b771a65f0a1e3ddb6aa16f867d87dc73c922411c255e6c4ab7f6d45c7327" +checksum = "23b0385782048be65f0a9dd046c469d6a758a53fe1aa63a8111dea394d2ffa2f" dependencies = [ "bitflags 1.3.2", "cfg_aliases", @@ -1706,9 +1717,12 @@ dependencies = [ "egui_extras", "env_logger 0.10.0", "futures 0.3.28", + "grin_api", "grin_chain", "grin_config", "grin_core", + "grin_keychain", + "grin_p2p", "grin_servers", "grin_util", "jni", @@ -1716,10 +1730,16 @@ dependencies = [ "log", "once_cell", "openssl-sys", + "pnet", "pollster 0.3.0", + "rand 0.6.5", "rust-i18n", "serde", + "serde_derive", + "serde_json", "sys-locale", + "tokio", + "tokio-util 0.2.0", "toml 0.7.4", "wgpu", "winit", @@ -1728,7 +1748,6 @@ dependencies = [ [[package]] name = "grin_api" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "bytes 0.5.6", "easy-jsonrpc-mw", @@ -1760,7 +1779,6 @@ dependencies = [ [[package]] name = "grin_chain" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "bit-vec", "bitflags 1.3.2", @@ -1783,7 +1801,6 @@ dependencies = [ [[package]] name = "grin_config" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "dirs", "grin_core", @@ -1799,7 +1816,6 @@ dependencies = [ [[package]] name = "grin_core" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "blake2-rfc", "byteorder", @@ -1825,7 +1841,6 @@ dependencies = [ [[package]] name = "grin_keychain" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "blake2-rfc", "byteorder", @@ -1847,7 +1862,6 @@ dependencies = [ [[package]] name = "grin_p2p" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "bitflags 1.3.2", "bytes 0.5.6", @@ -1869,7 +1883,6 @@ dependencies = [ [[package]] name = "grin_pool" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "blake2-rfc", "chrono", @@ -1902,7 +1915,6 @@ dependencies = [ [[package]] name = "grin_servers" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "chrono", "fs2", @@ -1932,7 +1944,6 @@ dependencies = [ [[package]] name = "grin_store" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "byteorder", "croaring", @@ -1951,7 +1962,6 @@ dependencies = [ [[package]] name = "grin_util" version = "5.2.0-beta.1" -source = "git+https://github.com/mimblewimble/grin.git#fd1410ebeb39fea6dc7bed5ebd55842466abdc69" dependencies = [ "backtrace", "base64 0.12.3", @@ -2256,6 +2266,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-terminal" version = "0.4.7" @@ -2750,9 +2769,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.38" +version = "0.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d0df99cfcd2530b2e694f6e17e7f37b8e26bb23983ac530c0c97408837c631" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2784,6 +2803,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "nodrop" version = "0.1.14" @@ -3204,6 +3229,97 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "pnet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi 0.3.9", +] + +[[package]] +name = "pnet_macros" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" +dependencies = [ + "proc-macro2 1.0.60", + "quote 1.0.28", + "regex", + "syn 1.0.109", +] + +[[package]] +name = "pnet_macros_support" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "pnet_transport" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + [[package]] name = "png" version = "0.17.9" @@ -3528,19 +3644,20 @@ dependencies = [ [[package]] name = "rust-i18n" -version = "1.2.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5340b7b546416b54cb3dc2184038b6ed6e45654e7b2f52bb206b52bb86c6d493" +checksum = "9a516a7ceb61ddcdad9cf723de82b86f6ed8c78ebe25255c5686a061bf7318a6" dependencies = [ "anyhow", "clap", - "glob", + "globwalk", "itertools", "once_cell", "quote 1.0.28", "regex", "rust-i18n-extract", "rust-i18n-macro", + "rust-i18n-support", "serde", "serde_derive", "toml 0.5.11", @@ -3548,9 +3665,9 @@ dependencies = [ [[package]] name = "rust-i18n-extract" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec44568e2cdf4bfb7a62381bbc6fcdf0a27c60cd503dfa12c59e6c17cf3177fa" +checksum = "e89ac25fb50c8d0893ee6436056fb4a0cc6f6e1df99239d7c104421d007d445e" dependencies = [ "anyhow", "ignore", @@ -3566,9 +3683,9 @@ dependencies = [ [[package]] name = "rust-i18n-macro" -version = "1.3.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ef5911f7c8324f62c44151fa7461bbdf6a00cbfa9beb04d963d9d02ec05634" +checksum = "e09ef5c1e310112eea3c19c4e18e3e62968b002eb535ff5b242ca1200742f996" dependencies = [ "glob", "once_cell", @@ -3583,16 +3700,17 @@ dependencies = [ [[package]] name = "rust-i18n-support" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6bbf2d058c3558bef952564ceb9afcb19631cde22b47dc44f436e62ecfb916" +checksum = "14eb094cd0072c5f09f333eea36fcd8c64961f9eb61dbd09e82eff51c58e8414" dependencies = [ - "glob", + "globwalk", "once_cell", "proc-macro2 1.0.60", "serde", "serde_json", "serde_yaml", + "toml 0.7.4", ] [[package]] @@ -3783,9 +3901,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" dependencies = [ "itoa 1.0.6", "ryu", @@ -4465,11 +4583,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -5088,9 +5205,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index ff48609..c33fabc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,15 @@ build = "src/build/build.rs" [dependencies] log = "0.4" #android-activity = { version = "0.4", features = ["game-activity"] } -#grin_api = "5.1.2" -grin_chain = { git = "https://github.com/mimblewimble/grin.git" } -grin_config = { git = "https://github.com/mimblewimble/grin.git" } -grin_core = { git = "https://github.com/mimblewimble/grin.git" } -#grin_keychain = "5.1.2" -#grin_p2p = "5.1.2" -grin_servers = { git = "https://github.com/mimblewimble/grin.git" } +grin_api = { path = "../grin/node/api" } +grin_chain = { path = "../grin/node/chain" } +grin_config = { path = "../grin/node/config" } +grin_core = { path = "../grin/node/core" } +grin_keychain = { path = "../grin/node/keychain" } +grin_p2p = { path = "../grin/node/p2p" } +grin_servers = { path = "../grin/node/servers" } #grin_store = "5.1.2" -grin_util = { git = "https://github.com/mimblewimble/grin.git" } +grin_util = { path = "../grin/node/util" } openssl-sys = { version = "0.9.82", features = ["vendored"] } #grin_wallet_api = "5.1.0" #grin_wallet_libwallet = "5.1.0" @@ -43,12 +43,20 @@ dirs = "2.0" ## other once_cell = "1.10.0" -rust-i18n = "1.1.4" +rust-i18n = "2.0.0" sys-locale = "0.3.0" chrono = "0.4.23" lazy_static = "1.4.0" toml = "0.7.4" serde = "1.0.164" +pnet = "0.33.0" + +# stratum server +serde_derive = "1" +serde_json = "1" +tokio = {version = "0.2", features = ["full"] } +tokio-util = { version = "0.2", features = ["codec"] } +rand = "0.6" [patch.crates-io] winit = { git = "https://github.com/rib/winit", branch = "android-activity" } diff --git a/locales/en.yml b/locales/en.yml index 3187f2a..557cdf3 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -58,8 +58,8 @@ network_mining: loading: Mining will be available after the synchronization server_setup: Stratum server setup enable_server: Enable server - server_setting: 'Enable stratum server or change more settings by selecting %{settings} at the bottom of the screen. App restart is required to change settings of the running server.' info: 'Mining server is enabled, you can change its settings by selecting %{settings} at the bottom of the screen. Data is updating when devices are connected.' + info_settings: To change the settings of enabled server, you will need to restart the node. rewards_wallet: Wallet for rewards server: Stratum server address: Address @@ -72,11 +72,9 @@ network_mining: network_settings: ip: IP Address port: Port - change_port: Change port change_value: Change value stratum_port: Stratum server port port_unavailable: Specified port is unavailable - restart_app_required: App restart is required to apply changes. restart_node_required: Node restart is required to apply changes. enable: Enable disable: Disable @@ -97,6 +95,11 @@ network_settings: full_validation_description: Whether to run a full chain validation when processing each block (except during synchronization). archive_mode: Archive mode archive_mode_desc: Run the node in full archive mode (more disk space and time will be required for synchronization). + attempt_time: Attempt time + attempt_time_desc: The amount of time in seconds to attempt to mine on a particular header before stopping and re-collecting transactions from the pool + min_share_diff: The minimum acceptable share difficulty + reset_settings_desc: Reset integrated node settings to default values + reset_settings: Reset settings modal: cancel: Cancel save: Save diff --git a/locales/ru.yml b/locales/ru.yml index aa4b000..71b0bc7 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -58,8 +58,8 @@ network_mining: loading: Майнинг будет доступен после синхронизации server_setup: Настройка stratum-сервера enable_server: Включить сервер - server_setting: 'Включите stratum-сервер или измените больше настроек, выбрав %{settings} внизу экрана. Для изменения настроек запущенного сервера потребуется перезапуск приложения.' info: 'Сервер майнинга запущен, вы можете изменить его настройки, выбрав %{settings} внизу экрана. Данные обновляются, когда устройства подключены.' + info_settings: Для изменения настроек запущенного сервера потребуется перезапуск узла. rewards_wallet: Кошелёк для наград server: Stratum-сервер address: Адрес @@ -72,11 +72,9 @@ network_mining: network_settings: ip: IP Адрес port: Порт - change_port: Изменить порт change_value: Изменить значение stratum_port: Порт Stratum сервера port_unavailable: Указанный порт недоступен - restart_app_required: Для применения изменений требуется перезапуск приложения. restart_node_required: Для применения изменений требуется перезапуск узла. enable: Включить disable: Выключить @@ -97,6 +95,11 @@ network_settings: full_validation_description: Запускать ли полную проверку цепи при обработке каждого блока (за исключением синхронизации). archive_mode: Архивный режим archive_mode_desc: Запустить узел в режиме полного архива (потребуется больше места и времени для синхронизации). + attempt_time: Время попытки + attempt_time_desc: Количество времени в секундах для попытки майнинга на определённом заголовке перед остановкой и повторным сбором транзакций из пула + min_share_diff: Минимальная допустимая сложность шары + reset_settings_desc: Сбросить настройки встроенного узла до стандартных значений + reset_settings: Сброс настроек modal: cancel: Отмена save: Сохранить diff --git a/src/grim.rs b/src/grim.rs index 2a9eca3..5988e37 100644 --- a/src/grim.rs +++ b/src/grim.rs @@ -89,7 +89,7 @@ fn setup_i18n() { } else { DEFAULT_LOCALE }; - if crate::available_locales().contains(&locale_str) { + if crate::_rust_i18n_available_locales().contains(&locale_str) { rust_i18n::set_locale(locale_str); } } diff --git a/src/gui/app.rs b/src/gui/app.rs index 3deb326..bc982ae 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -38,7 +38,7 @@ impl App { egui::CentralPanel::default() .frame(egui::Frame { fill: Colors::FILL, - .. Default::default() + ..Default::default() }) .show(ctx, |ui| { self.root.ui(ui, frame, cb); diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 6bb7aca..383517b 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -59,7 +59,7 @@ impl PlatformCallbacks for Android { } fn get_string_from_buffer(&self) -> String { - use jni::objects::{JObject, JValue, JString}; + use jni::objects::{JObject, JString}; let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let mut env = vm.attach_current_thread().unwrap(); diff --git a/src/gui/screens/root.rs b/src/gui/screens/root.rs index 918cdbe..8f0b644 100644 --- a/src/gui/screens/root.rs +++ b/src/gui/screens/root.rs @@ -17,12 +17,12 @@ use egui::RichText; use crate::gui::{App, Colors, Navigator}; use crate::gui::platform::PlatformCallbacks; use crate::gui::screens::{Account, Accounts, Screen, ScreenId}; -use crate::gui::views::{ModalContainer, Network, View}; +use crate::gui::views::{ModalContainer, NetworkContainer, View}; use crate::node::Node; pub struct Root { screens: Vec>, - network: Network, + network_panel: NetworkContainer, show_exit_progress: bool, allowed_modal_ids: Vec<&'static str> } @@ -36,7 +36,7 @@ impl Default for Root { Box::new(Accounts::default()), Box::new(Account::default()) ], - network: Network::default(), + network_panel: NetworkContainer::default(), show_exit_progress: false, allowed_modal_ids: vec![ Navigator::EXIT_MODAL @@ -65,7 +65,7 @@ impl Root { .exact_width(panel_width) .frame(egui::Frame::default()) .show_animated_inside(ui, is_panel_open, |ui| { - self.network.ui(ui, frame, cb); + self.network_panel.ui(ui, frame, cb); }); egui::CentralPanel::default() diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index ab5fc41..823ad8f 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -22,11 +22,4 @@ mod modal; pub use modal::*; mod network; -pub use network::*; - -mod network_node; -mod network_settings; -mod network_metrics; -mod network_mining; -mod settings_stratum; -mod settings_node; \ No newline at end of file +pub use network::*; \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index b6ebb5f..eff7abc 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -126,7 +126,7 @@ impl Modal { // Show main content Window at given position. let (content_align, content_offset) = self.modal_position(); - let layer_id = egui::Window::new("modal_window") + let layer_id = egui::Window::new(format!("modal_window_{}", self.id)) .title_bar(false) .resizable(false) .collapsible(false) @@ -152,12 +152,12 @@ impl Modal { /// Get [`egui::Window`] position based on [`ModalPosition`]. fn modal_position(&self) -> (Align2, Vec2) { let align = match self.position { - ModalPosition::CenterTop => { Align2::CENTER_TOP } - ModalPosition::Center => { Align2::CENTER_CENTER } + ModalPosition::CenterTop => Align2::CENTER_TOP, + ModalPosition::Center => Align2::CENTER_CENTER }; let offset = match self.position { - ModalPosition::CenterTop => { Vec2::new(0.0, 20.0) } - ModalPosition::Center => { Vec2::new(0.0, 0.0) } + ModalPosition::CenterTop => Vec2::new(0.0, 20.0), + ModalPosition::Center => Vec2::new(0.0, 0.0) }; (align, offset) } diff --git a/src/gui/views/network.rs b/src/gui/views/network.rs index 044a593..ebad78c 100644 --- a/src/gui/views/network.rs +++ b/src/gui/views/network.rs @@ -12,262 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Duration; +mod container; +mod metrics; +mod mining; +mod node_settings; +mod node; +mod settings; -use egui::{Color32, lerp, Rgba, RichText}; -use egui::style::Margin; -use egui_extras::{Size, StripBuilder}; -use grin_chain::SyncStatus; -use crate::AppConfig; - -use crate::gui::{Colors, Navigator}; -use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalContainer, View}; -use crate::gui::views::network_metrics::NetworkMetrics; -use crate::gui::views::network_mining::NetworkMining; -use crate::gui::views::network_node::NetworkNode; -use crate::gui::views::network_settings::NetworkSettings; -use crate::gui::views::settings_node::NodeSetup; -use crate::gui::views::settings_stratum::StratumServerSetup; -use crate::node::Node; - -pub trait NetworkTab { - fn get_type(&self) -> NetworkTabType; - fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); - fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); -} - -#[derive(PartialEq)] -pub enum NetworkTabType { - Node, - Metrics, - Mining, - Settings -} - -impl NetworkTabType { - pub fn name(&self) -> String { - match *self { - NetworkTabType::Node => { t!("network.node") } - NetworkTabType::Metrics => { t!("network.metrics") } - NetworkTabType::Mining => { t!("network.mining") } - NetworkTabType::Settings => { t!("network.settings") } - } - } -} - -pub struct Network { - current_tab: Box, - modal_ids: Vec<&'static str>, -} - -impl Default for Network { - fn default() -> Self { - Self { - current_tab: Box::new(NetworkNode::default()), - modal_ids: vec![ - NetworkSettings::NODE_RESTART_REQUIRED_MODAL, - StratumServerSetup::STRATUM_PORT_MODAL, - NodeSetup::API_PORT_MODAL, - NodeSetup::API_SECRET_MODAL, - NodeSetup::FOREIGN_API_SECRET_MODAL, - NodeSetup::FTL_MODAL - ] - } - } -} - -impl ModalContainer for Network { - fn modal_ids(&self) -> &Vec<&'static str> { - self.modal_ids.as_ref() - } -} - -impl Network { - pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - // Show modal content if it's opened. - let modal_id = Navigator::is_modal_open(); - if modal_id.is_some() && self.can_show_modal(modal_id.unwrap()) { - Navigator::modal_ui(ui, |ui, modal| { - self.current_tab.as_mut().on_modal_ui(ui, modal, cb); - }); - } - - egui::TopBottomPanel::top("network_title") - .resizable(false) - .frame(egui::Frame { - fill: Colors::YELLOW, - inner_margin: Margin::same(0.0), - outer_margin: Margin::same(0.0), - ..Default::default() - }) - .show_inside(ui, |ui| { - self.title_ui(ui, frame); - }); - - egui::TopBottomPanel::bottom("network_tabs") - .frame(egui::Frame { - outer_margin: Margin::same(5.0), - ..Default::default() - }) - .show_inside(ui, |ui| { - self.tabs_ui(ui); - }); - - egui::CentralPanel::default() - .frame(egui::Frame { - stroke: View::DEFAULT_STROKE, - inner_margin: Margin::same(4.0), - fill: Colors::WHITE, - ..Default::default() - }) - .show_inside(ui, |ui| { - self.current_tab.ui(ui, cb); - }); - } - - /// Draw tab buttons in the bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui) { - ui.scope(|ui| { - // Setup spacing between tabs. - ui.style_mut().spacing.item_spacing = egui::vec2(5.0, 0.0); - // Setup vertical padding inside tab button. - ui.style_mut().spacing.button_padding = egui::vec2(0.0, 3.0); - - ui.columns(4, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::tab_button(ui, DATABASE, self.is_current_tab(NetworkTabType::Node), || { - self.current_tab = Box::new(NetworkNode::default()); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::tab_button(ui, GAUGE, self.is_current_tab(NetworkTabType::Metrics), || { - self.current_tab = Box::new(NetworkMetrics::default()); - }); - }); - columns[2].vertical_centered_justified(|ui| { - View::tab_button(ui, FACTORY, self.is_current_tab(NetworkTabType::Mining), || { - self.current_tab = Box::new(NetworkMining::default()); - }); - }); - columns[3].vertical_centered_justified(|ui| { - View::tab_button(ui, FADERS, self.is_current_tab(NetworkTabType::Settings), || { - self.current_tab = Box::new(NetworkSettings::default()); - }); - }); - }); - }); - } - - /// Check if current tab equals providing [`NetworkTabType`]. - fn is_current_tab(&self, tab_type: NetworkTabType) -> bool { - self.current_tab.get_type() == tab_type - } - - /// Draw title content. - fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - StripBuilder::new(ui) - .size(Size::exact(52.0)) - .vertical(|mut strip| { - strip.strip(|builder| { - builder - .size(Size::exact(52.0)) - .size(Size::remainder()) - .size(Size::exact(52.0)) - .horizontal(|mut strip| { - strip.cell(|ui| { - ui.centered_and_justified(|ui| { - View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { - //TODO: Actions for node - }); - }); - }); - strip.strip(|builder| { - self.title_text_ui(builder); - }); - strip.cell(|ui| { - if !View::is_dual_panel_mode(frame) { - ui.centered_and_justified(|ui| { - View::title_button(ui, CARDHOLDER, || { - Navigator::toggle_side_panel(); - }); - }); - } - }); - }); - }); - }); - } - - /// Draw title text. - fn title_text_ui(&self, builder: StripBuilder) { - builder - .size(Size::remainder()) - .size(Size::exact(32.0)) - .vertical(|mut strip| { - strip.cell(|ui| { - ui.add_space(2.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(self.current_tab.get_type().name().to_uppercase()) - .size(18.0) - .color(Colors::TITLE)); - }); - }); - strip.cell(|ui| { - ui.centered_and_justified(|ui| { - let sync_status = Node::get_sync_status(); - - // Setup text color animation based on sync status - let idle = match sync_status { - None => !Node::is_starting(), - Some(ss) => ss == SyncStatus::NoSync - }; - let (dark, bright) = (0.3, 1.0); - let color_factor = if !idle { - lerp(dark..=bright, ui.input().time.cos().abs()) as f32 - } else { - bright as f32 - }; - - // Draw sync text - let status_color_rgba = Rgba::from(Colors::TEXT) * color_factor; - let status_color = Color32::from(status_color_rgba); - View::ellipsize_text(ui, Node::get_sync_status_text(), 15.0, status_color); - - // Repaint based on sync status - if idle { - ui.ctx().request_repaint_after(Duration::from_millis(600)); - } else { - ui.ctx().request_repaint(); - } - }); - }); - }); - } - - /// Content to draw when node is disabled. - pub fn disabled_node_ui(ui: &mut egui::Ui) { - View::center_content(ui, 162.0, |ui| { - let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); - ui.label(RichText::new(text) - .size(16.0) - .color(Colors::INACTIVE_TEXT) - ); - ui.add_space(10.0); - View::button(ui, t!("network.enable_node"), Colors::GOLD, || { - Node::start(); - }); - ui.add_space(2.0); - Self::autorun_node_ui(ui); - }); - } - - /// Draw checkbox with setting to run node on app launch. - pub fn autorun_node_ui(ui: &mut egui::Ui) { - let autostart = AppConfig::autostart_node(); - View::checkbox(ui, autostart, t!("network.autorun"), || { - AppConfig::toggle_node_autostart(); - }); - } -} \ No newline at end of file +pub use container::*; \ No newline at end of file diff --git a/src/gui/views/network/container.rs b/src/gui/views/network/container.rs new file mode 100644 index 0000000..1016c36 --- /dev/null +++ b/src/gui/views/network/container.rs @@ -0,0 +1,275 @@ +// 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::time::Duration; + +use egui::{Color32, lerp, Rgba, RichText}; +use egui::style::Margin; +use egui_extras::{Size, StripBuilder}; +use grin_chain::SyncStatus; + +use crate::AppConfig; +use crate::gui::{Colors, Navigator}; +use crate::gui::icons::{CARDHOLDER, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, ModalContainer, View}; +use crate::gui::views::network::metrics::NetworkMetrics; +use crate::gui::views::network::mining::NetworkMining; +use crate::gui::views::network::node::NetworkNode; +use crate::gui::views::network::node_settings::NetworkNodeSettings; +use crate::gui::views::network::settings::server::ServerSetup; +use crate::gui::views::network::settings::stratum::StratumServerSetup; +use crate::node::Node; + +pub trait NetworkTab { + fn get_type(&self) -> NetworkTabType; + fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); + fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks); +} + +#[derive(PartialEq)] +pub enum NetworkTabType { + Node, + Metrics, + Mining, + Settings +} + +impl NetworkTabType { + pub fn name(&self) -> String { + match *self { + NetworkTabType::Node => { t!("network.node") } + NetworkTabType::Metrics => { t!("network.metrics") } + NetworkTabType::Mining => { t!("network.mining") } + NetworkTabType::Settings => { t!("network.settings") } + } + } +} + +pub struct NetworkContainer { + current_tab: Box, + modal_ids: Vec<&'static str>, +} + +impl Default for NetworkContainer { + fn default() -> Self { + Self { + current_tab: Box::new(NetworkNode::default()), + modal_ids: vec![ + NetworkNodeSettings::NODE_RESTART_REQUIRED_MODAL, + StratumServerSetup::STRATUM_PORT_MODAL, + StratumServerSetup::STRATUM_ATTEMPT_TIME_MODAL, + StratumServerSetup::STRATUM_MIN_SHARE_MODAL, + ServerSetup::API_PORT_MODAL, + ServerSetup::API_SECRET_MODAL, + ServerSetup::FOREIGN_API_SECRET_MODAL, + ServerSetup::FTL_MODAL + ] + } + } +} + +impl ModalContainer for NetworkContainer { + fn modal_ids(&self) -> &Vec<&'static str> { + self.modal_ids.as_ref() + } +} + +impl NetworkContainer { + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { + // Show modal content if it's opened. + let modal_id = Navigator::is_modal_open(); + if modal_id.is_some() && self.can_show_modal(modal_id.unwrap()) { + Navigator::modal_ui(ui, |ui, modal| { + self.current_tab.as_mut().on_modal_ui(ui, modal, cb); + }); + } + + egui::TopBottomPanel::top("network_title") + .resizable(false) + .frame(egui::Frame { + fill: Colors::YELLOW, + inner_margin: Margin::same(0.0), + outer_margin: Margin::same(0.0), + ..Default::default() + }) + .show_inside(ui, |ui| { + self.title_ui(ui, frame); + }); + + egui::TopBottomPanel::bottom("network_tabs") + .frame(egui::Frame { + outer_margin: Margin::same(5.0), + ..Default::default() + }) + .show_inside(ui, |ui| { + self.tabs_ui(ui); + }); + + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::DEFAULT_STROKE, + inner_margin: Margin::same(4.0), + fill: Colors::WHITE, + ..Default::default() + }) + .show_inside(ui, |ui| { + self.current_tab.ui(ui, cb); + }); + } + + /// Draw tab buttons in the bottom of the screen. + fn tabs_ui(&mut self, ui: &mut egui::Ui) { + ui.scope(|ui| { + // Setup spacing between tabs. + ui.style_mut().spacing.item_spacing = egui::vec2(5.0, 0.0); + // Setup vertical padding inside tab button. + ui.style_mut().spacing.button_padding = egui::vec2(0.0, 3.0); + + ui.columns(4, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::tab_button(ui, DATABASE, self.is_current_tab(NetworkTabType::Node), || { + self.current_tab = Box::new(NetworkNode::default()); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::tab_button(ui, GAUGE, self.is_current_tab(NetworkTabType::Metrics), || { + self.current_tab = Box::new(NetworkMetrics::default()); + }); + }); + columns[2].vertical_centered_justified(|ui| { + View::tab_button(ui, FACTORY, self.is_current_tab(NetworkTabType::Mining), || { + self.current_tab = Box::new(NetworkMining::default()); + }); + }); + columns[3].vertical_centered_justified(|ui| { + View::tab_button(ui, FADERS, self.is_current_tab(NetworkTabType::Settings), || { + self.current_tab = Box::new(NetworkNodeSettings::default()); + }); + }); + }); + }); + } + + /// Check if current tab equals providing [`NetworkTabType`]. + fn is_current_tab(&self, tab_type: NetworkTabType) -> bool { + self.current_tab.get_type() == tab_type + } + + /// Draw title content. + fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + StripBuilder::new(ui) + .size(Size::exact(52.0)) + .vertical(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::exact(52.0)) + .size(Size::remainder()) + .size(Size::exact(52.0)) + .horizontal(|mut strip| { + strip.cell(|ui| { + ui.centered_and_justified(|ui| { + View::title_button(ui, DOTS_THREE_OUTLINE_VERTICAL, || { + //TODO: Actions for node + }); + }); + }); + strip.strip(|builder| { + self.title_text_ui(builder); + }); + strip.cell(|ui| { + if !View::is_dual_panel_mode(frame) { + ui.centered_and_justified(|ui| { + View::title_button(ui, CARDHOLDER, || { + Navigator::toggle_side_panel(); + }); + }); + } + }); + }); + }); + }); + } + + /// Draw title text. + fn title_text_ui(&self, builder: StripBuilder) { + builder + .size(Size::remainder()) + .size(Size::exact(32.0)) + .vertical(|mut strip| { + strip.cell(|ui| { + ui.add_space(2.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(self.current_tab.get_type().name().to_uppercase()) + .size(18.0) + .color(Colors::TITLE)); + }); + }); + strip.cell(|ui| { + ui.centered_and_justified(|ui| { + let sync_status = Node::get_sync_status(); + + // Setup text color animation based on sync status + let idle = match sync_status { + None => !Node::is_starting(), + Some(ss) => ss == SyncStatus::NoSync + }; + let (dark, bright) = (0.3, 1.0); + let color_factor = if !idle { + lerp(dark..=bright, ui.input().time.cos().abs()) as f32 + } else { + bright as f32 + }; + + // Draw sync text + let status_color_rgba = Rgba::from(Colors::TEXT) * color_factor; + let status_color = Color32::from(status_color_rgba); + View::ellipsize_text(ui, Node::get_sync_status_text(), 15.0, status_color); + + // Repaint based on sync status + if idle { + ui.ctx().request_repaint_after(Duration::from_millis(600)); + } else { + ui.ctx().request_repaint(); + } + }); + }); + }); + } + + /// Content to draw when node is disabled. + pub fn disabled_node_ui(ui: &mut egui::Ui) { + View::center_content(ui, 162.0, |ui| { + let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL); + ui.label(RichText::new(text) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + ui.add_space(10.0); + View::button(ui, t!("network.enable_node"), Colors::GOLD, || { + Node::start(); + }); + ui.add_space(2.0); + Self::autorun_node_ui(ui); + }); + } + + /// Draw checkbox with setting to run node on app launch. + pub fn autorun_node_ui(ui: &mut egui::Ui) { + let autostart = AppConfig::autostart_node(); + View::checkbox(ui, autostart, t!("network.autorun"), || { + AppConfig::toggle_node_autostart(); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/network_metrics.rs b/src/gui/views/network/metrics.rs similarity index 90% rename from src/gui/views/network_metrics.rs rename to src/gui/views/network/metrics.rs index 807af8b..72453ef 100644 --- a/src/gui/views/network_metrics.rs +++ b/src/gui/views/network/metrics.rs @@ -19,7 +19,8 @@ use grin_servers::DiffBlock; use crate::gui::Colors; use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HASH, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Network, NetworkTab, NetworkTabType, View}; +use crate::gui::views::network::{NetworkTab, NetworkTabType}; +use crate::gui::views::{Modal, NetworkContainer, View}; use crate::node::Node; #[derive(Default)] @@ -36,23 +37,31 @@ impl NetworkTab for NetworkMetrics { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let server_stats = Node::get_stats(); - // Show message when node is not running or loading spinner when metrics are not available. + // Show message to enable node when it's not running. + if !Node::is_running() { + NetworkContainer::disabled_node_ui(ui); + return; + } + + // Show loading spinner when node is stopping. + if Node::is_stopping() { + ui.centered_and_justified(|ui| { + View::big_loading_spinner(ui); + }); + return; + } + + // Show message when metrics are not available. if server_stats.is_none() || Node::is_restarting() || server_stats.as_ref().unwrap().diff_stats.height == 0 { - if !Node::is_running() { - Network::disabled_node_ui(ui); - } else { - View::center_content(ui, 162.0, |ui| { - View::big_loading_spinner(ui); - if !Node::is_stopping() { - ui.add_space(18.0); - ui.label(RichText::new(t!("network_metrics.loading")) - .size(16.0) - .color(Colors::INACTIVE_TEXT) - ); - } - }); - } + View::center_content(ui, 162.0, |ui| { + View::big_loading_spinner(ui); + ui.add_space(18.0); + ui.label(RichText::new(t!("network_metrics.loading")) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + }); return; } diff --git a/src/gui/views/network_mining.rs b/src/gui/views/network/mining.rs similarity index 82% rename from src/gui/views/network_mining.rs rename to src/gui/views/network/mining.rs index 513da02..942c4f8 100644 --- a/src/gui/views/network_mining.rs +++ b/src/gui/views/network/mining.rs @@ -20,8 +20,9 @@ use grin_servers::WorkerStats; use crate::gui::Colors; use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, COMPUTER_TOWER, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_NOTCH_MINUS, FOLDER_NOTCH_PLUS, PLUGS, PLUGS_CONNECTED, POLYGON}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Network, NetworkTab, NetworkTabType, View}; -use crate::gui::views::settings_stratum::StratumServerSetup; +use crate::gui::views::{Modal, NetworkContainer, View}; +use crate::gui::views::network::{NetworkTab, NetworkTabType}; +use crate::gui::views::network::settings::stratum::StratumServerSetup; use crate::node::{Node, NodeConfig}; #[derive(Default)] @@ -37,69 +38,47 @@ impl NetworkTab for NetworkMining { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let server_stats = Node::get_stats(); - // Show message when node is not running or loading spinner when mining is not available. - if !server_stats.is_some() || Node::get_sync_status().unwrap() != SyncStatus::NoSync { - if !Node::is_running() { - Network::disabled_node_ui(ui); - } else { - View::center_content(ui, 162.0, |ui| { - View::big_loading_spinner(ui); - if !Node::is_stopping() { - ui.add_space(18.0); - ui.label(RichText::new(t!("network_mining.loading")) - .size(16.0) - .color(Colors::INACTIVE_TEXT) - ); - } - }); - } + // Show message to enable node when it's not running. + if !Node::is_running() { + NetworkContainer::disabled_node_ui(ui); return; } - let stratum_stats = &server_stats.as_ref().unwrap().stratum_stats; - - // Show stratum server setup when mining server is not running. - if !stratum_stats.is_running && !Node::is_stratum_server_starting() { - ScrollArea::vertical() - .id_source("stratum_server_setup") - .auto_shrink([false; 2]) - .show(ui, |ui| { - self.stratum_server_setup.ui(ui, cb); - - ui.vertical_centered(|ui| { - // Show message about stratum server config. - let text = t!("network_mining.server_setting", "settings" => FADERS); - ui.label(RichText::new(text) - .size(16.0) - .color(Colors::INACTIVE_TEXT) - ); - ui.add_space(4.0); - - // Show button to enable stratum server if port is available. - if self.stratum_server_setup.is_port_available { - ui.add_space(6.0); - View::button(ui, t!("network_mining.enable_server"), Colors::GOLD, || { - Node::start_stratum_server(); - }); - ui.add_space(2.0); - - // Show stratum server autorun checkbox. - let stratum_enabled = NodeConfig::is_stratum_autorun_enabled(); - View::checkbox(ui, stratum_enabled, t!("network.autorun"), || { - NodeConfig::toggle_stratum_autorun(); - }); - ui.add_space(6.0); - } - }); - }); - return; - } else if Node::is_stratum_server_starting() { + // Show loading spinner when node is stopping or stratum server is starting. + if Node::is_stopping() || Node::is_stratum_server_starting() { ui.centered_and_justified(|ui| { View::big_loading_spinner(ui); }); return; } + // Show message when mining is not available. + if server_stats.is_none() || Node::is_restarting() + || Node::get_sync_status().unwrap() != SyncStatus::NoSync { + View::center_content(ui, 162.0, |ui| { + View::big_loading_spinner(ui); + ui.add_space(18.0); + ui.label(RichText::new(t!("network_mining.loading")) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + }); + return; + } + + let stratum_stats = Node::get_stratum_stats(); + + // Show stratum server setup when mining server is not running. + if !stratum_stats.is_running { + ScrollArea::vertical() + .id_source("stratum_setup_scroll") + .auto_shrink([false; 2]) + .show(ui, |ui| { + self.stratum_server_setup.ui(ui, cb); + }); + return; + } + // Show stratum mining server info. View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_mining.server"))); ui.columns(2, |columns| { @@ -221,7 +200,7 @@ impl NetworkTab for NetworkMining { fn on_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { StratumServerSetup::STRATUM_PORT_MODAL => { - self.stratum_server_setup.stratum_port_modal_ui(ui, modal, cb); + self.stratum_server_setup.port_modal(ui, modal, cb); }, _ => {} } diff --git a/src/gui/views/network_node.rs b/src/gui/views/network/node.rs similarity index 94% rename from src/gui/views/network_node.rs rename to src/gui/views/network/node.rs index 496a904..4551115 100644 --- a/src/gui/views/network_node.rs +++ b/src/gui/views/network/node.rs @@ -19,7 +19,8 @@ use grin_servers::PeerStats; use crate::gui::Colors; use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, PLUGS_CONNECTED, SHARE_NETWORK}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Network, NetworkTab, NetworkTabType, View}; +use crate::gui::views::{Modal, View}; +use crate::gui::views::network::{NetworkContainer, NetworkTab, NetworkTabType}; use crate::node::Node; #[derive(Default)] @@ -32,15 +33,17 @@ impl NetworkTab for NetworkNode { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let server_stats = Node::get_stats(); - // Show message when node is not running or loading spinner when stats are not available. - if !server_stats.is_some() || Node::is_restarting() { - if !Node::is_running() { - Network::disabled_node_ui(ui); - } else { - ui.centered_and_justified(|ui| { - View::big_loading_spinner(ui); - }); - } + // Show message to enable node when it's not running. + if !Node::is_running() { + NetworkContainer::disabled_node_ui(ui); + return; + } + + // Show loading spinner when stats are not available. + if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() { + ui.centered_and_justified(|ui| { + View::big_loading_spinner(ui); + }); return; } diff --git a/src/gui/views/network_settings.rs b/src/gui/views/network/node_settings.rs similarity index 79% rename from src/gui/views/network_settings.rs rename to src/gui/views/network/node_settings.rs index 67d630d..afc9862 100644 --- a/src/gui/views/network_settings.rs +++ b/src/gui/views/network/node_settings.rs @@ -19,16 +19,19 @@ use egui::{RichText, ScrollArea}; use crate::gui::{Colors, Navigator}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalPosition, NetworkTab, NetworkTabType, View}; -use crate::gui::views::settings_node::NodeSetup; +use crate::gui::views::{Modal, ModalPosition, View}; +use crate::gui::views::network::{NetworkTab, NetworkTabType}; +use crate::gui::views::network::settings::server::ServerSetup; +use crate::gui::views::network::settings::stratum::StratumServerSetup; use crate::node::Node; #[derive(Default)] -pub struct NetworkSettings { - node_setup: NodeSetup +pub struct NetworkNodeSettings { + server_setup: ServerSetup, + stratum_server_setup: StratumServerSetup } -impl NetworkTab for NetworkSettings { +impl NetworkTab for NetworkNodeSettings { fn get_type(&self) -> NetworkTabType { NetworkTabType::Settings } @@ -38,7 +41,8 @@ impl NetworkTab for NetworkSettings { .id_source("network_settings") .auto_shrink([false; 2]) .show(ui, |ui| { - self.node_setup.ui(ui, cb); + self.server_setup.ui(ui, cb); + self.stratum_server_setup.ui(ui, cb); }); } @@ -47,24 +51,35 @@ impl NetworkTab for NetworkSettings { Self::NODE_RESTART_REQUIRED_MODAL => { self.node_restart_required_modal(ui, modal); } - NodeSetup::API_PORT_MODAL => { - self.node_setup.api_port_modal(ui, modal, cb); + + ServerSetup::API_PORT_MODAL => { + self.server_setup.api_port_modal(ui, modal, cb); }, - NodeSetup::API_SECRET_MODAL => { - self.node_setup.secret_modal(ui, modal, cb); + ServerSetup::API_SECRET_MODAL => { + self.server_setup.secret_modal(ui, modal, cb); }, - NodeSetup::FOREIGN_API_SECRET_MODAL => { - self.node_setup.secret_modal(ui, modal, cb); + ServerSetup::FOREIGN_API_SECRET_MODAL => { + self.server_setup.secret_modal(ui, modal, cb); }, - NodeSetup::FTL_MODAL => { - self.node_setup.ftl_modal(ui, modal, cb); + ServerSetup::FTL_MODAL => { + self.server_setup.ftl_modal(ui, modal, cb); + }, + + StratumServerSetup::STRATUM_PORT_MODAL => { + self.stratum_server_setup.port_modal(ui, modal, cb); + } + StratumServerSetup::STRATUM_ATTEMPT_TIME_MODAL => { + self.stratum_server_setup.attempt_modal(ui, modal, cb); + } + StratumServerSetup::STRATUM_MIN_SHARE_MODAL => { + self.stratum_server_setup.min_diff_modal(ui, modal, cb); } _ => {} } } } -impl NetworkSettings { +impl NetworkNodeSettings { pub const NODE_RESTART_REQUIRED_MODAL: &'static str = "node_restart_required"; /// Reminder to restart enabled node to show on edit setting at [`Modal`]. @@ -82,7 +97,7 @@ impl NetworkSettings { pub fn show_node_restart_required_modal() { if Node::is_running() { // Show modal to apply changes by node restart. - let port_modal = Modal::new(NetworkSettings::NODE_RESTART_REQUIRED_MODAL) + let port_modal = Modal::new(NetworkNodeSettings::NODE_RESTART_REQUIRED_MODAL) .position(ModalPosition::Center) .title(t!("network.settings")); Navigator::show_modal(port_modal); @@ -121,19 +136,6 @@ impl NetworkSettings { }); } - /// List of available IP addresses. - pub fn get_ip_addrs() -> Vec { - let mut ip_addrs = Vec::new(); - for net_if in pnet::datalink::interfaces() { - for ip in net_if.ips { - if ip.is_ipv4() { - ip_addrs.push(ip.ip()); - } - } - } - ip_addrs - } - /// Draw IP addresses as radio buttons. pub fn ip_addrs_ui(ui: &mut egui::Ui, saved_ip: &String, @@ -147,6 +149,7 @@ impl NetworkSettings { selected_ip_addr = ip_addrs.get(0).unwrap(); } + ui.add_space(2.0); // Show available IP addresses on the system. let _ = ip_addrs.chunks(2).map(|x| { if x.len() == 2 { diff --git a/src/gui/views/network/settings.rs b/src/gui/views/network/settings.rs new file mode 100644 index 0000000..b3666e1 --- /dev/null +++ b/src/gui/views/network/settings.rs @@ -0,0 +1,19 @@ +// 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. + +pub mod stratum; +pub mod server; +pub mod p2p; +pub mod pool; +pub mod dandelion; \ No newline at end of file diff --git a/src/gui/views/network/settings/dandelion.rs b/src/gui/views/network/settings/dandelion.rs new file mode 100644 index 0000000..6ddbed9 --- /dev/null +++ b/src/gui/views/network/settings/dandelion.rs @@ -0,0 +1,14 @@ +// 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. + diff --git a/src/gui/views/network/settings/p2p.rs b/src/gui/views/network/settings/p2p.rs new file mode 100644 index 0000000..6ddbed9 --- /dev/null +++ b/src/gui/views/network/settings/p2p.rs @@ -0,0 +1,14 @@ +// 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. + diff --git a/src/gui/views/network/settings/pool.rs b/src/gui/views/network/settings/pool.rs new file mode 100644 index 0000000..6ddbed9 --- /dev/null +++ b/src/gui/views/network/settings/pool.rs @@ -0,0 +1,14 @@ +// 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. + diff --git a/src/gui/views/settings_node.rs b/src/gui/views/network/settings/server.rs similarity index 92% rename from src/gui/views/settings_node.rs rename to src/gui/views/network/settings/server.rs index ff5247c..6e55e9c 100644 --- a/src/gui/views/settings_node.rs +++ b/src/gui/views/network/settings/server.rs @@ -15,16 +15,17 @@ use eframe::emath::Align; use egui::{Id, Layout, RichText, TextStyle, Widget}; use grin_core::global::ChainTypes; + use crate::AppConfig; use crate::gui::{Colors, Navigator}; -use crate::gui::icons::{CLIPBOARD_TEXT, COMPUTER_TOWER, COPY, POWER, SHIELD, SHIELD_SLASH}; +use crate::gui::icons::{CLIPBOARD_TEXT, CLOCK_CLOCKWISE, COMPUTER_TOWER, COPY, PLUG, POWER, SHIELD, SHIELD_SLASH}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalPosition, Network, View}; -use crate::gui::views::network_settings::NetworkSettings; +use crate::gui::views::{Modal, ModalPosition, NetworkContainer, View}; +use crate::gui::views::network::node_settings::NetworkNodeSettings; use crate::node::{Node, NodeConfig}; /// Integrated node server setup ui section. -pub struct NodeSetup { +pub struct ServerSetup { /// API port to be used inside edit modal. api_port_edit: String, /// Flag to check if API port is available inside edit modal. @@ -43,7 +44,7 @@ pub struct NodeSetup { ftl_edit: String, } -impl Default for NodeSetup { +impl Default for ServerSetup { fn default() -> Self { let (api_ip, api_port) = NodeConfig::get_api_address(); let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port); @@ -58,7 +59,7 @@ impl Default for NodeSetup { } } -impl NodeSetup { +impl ServerSetup { pub const API_PORT_MODAL: &'static str = "api_port"; pub const API_SECRET_MODAL: &'static str = "api_secret"; pub const FOREIGN_API_SECRET_MODAL: &'static str = "foreign_api_secret"; @@ -67,7 +68,7 @@ impl NodeSetup { pub fn 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::ITEM_STROKE); - ui.add_space(8.0); + ui.add_space(4.0); // Show chain type setup. self.chain_type_ui(ui); @@ -75,8 +76,9 @@ impl NodeSetup { // Show loading indicator or controls to stop/start/restart node. if Node::is_stopping() || Node::is_restarting() || Node::is_starting() { ui.vertical_centered(|ui| { - ui.add_space(6.0); + ui.add_space(8.0); View::small_loading_spinner(ui); + ui.add_space(2.0); }); } else { if Node::is_running() { @@ -110,21 +112,29 @@ impl NodeSetup { } // Autorun node setup. - ui.add_space(4.0); ui.vertical_centered(|ui| { - Network::autorun_node_ui(ui); + ui.add_space(6.0); + NetworkContainer::autorun_node_ui(ui); + if Node::is_running() { + ui.add_space(2.0); + ui.label(RichText::new(t!("network_settings.restart_node_required")) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + ui.add_space(4.0); + } }); - ui.add_space(4.0); + ui.add_space(6.0); - let addrs = NetworkSettings::get_ip_addrs(); + let addrs = NodeConfig::get_ip_addrs(); if addrs.is_empty() { // Show message when IP addresses are not available on the system. - NetworkSettings::no_ip_address_ui(ui); + NetworkNodeSettings::no_ip_address_ui(ui); ui.add_space(4.0); } else { View::horizontal_line(ui, Colors::ITEM_STROKE); - ui.add_space(4.0); + ui.add_space(6.0); ui.vertical_centered(|ui| { ui.label(RichText::new(t!("network_settings.api_ip")) @@ -132,15 +142,13 @@ impl NodeSetup { .color(Colors::GRAY) ); ui.add_space(6.0); + // Show API IP addresses to select. let (api_ip, api_port) = NodeConfig::get_api_address(); - NetworkSettings::ip_addrs_ui(ui, &api_ip, &addrs, |selected_ip| { + NetworkNodeSettings::ip_addrs_ui(ui, &api_ip, &addrs, |selected_ip| { let api_available = NodeConfig::is_api_port_available(selected_ip, &api_port); self.is_api_port_available = api_available; NodeConfig::save_api_address(selected_ip, &api_port); - if api_available { - NetworkSettings::show_node_restart_required_modal(); - } }); ui.label(RichText::new(t!("network_settings.api_port")) @@ -209,6 +217,7 @@ impl NodeSetup { let saved_chain_type = AppConfig::chain_type(); let mut selected_chain_type = saved_chain_type; + ui.add_space(8.0); ui.columns(2, |columns| { columns[0].vertical_centered(|ui| { let main_type = ChainTypes::Mainnet; @@ -219,19 +228,18 @@ impl NodeSetup { View::radio_value(ui, &mut selected_chain_type, test_type, "Testnet".to_string()); }) }); - ui.add_space(4.0); + ui.add_space(8.0); if saved_chain_type != selected_chain_type { AppConfig::change_chain_type(&selected_chain_type); - NetworkSettings::show_node_restart_required_modal(); + NetworkNodeSettings::show_node_restart_required_modal(); } } /// Draw API port setup ui. fn api_port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let (_, port) = NodeConfig::get_api_address(); - // Show button to enter API server port. - View::button(ui, port.clone(), Colors::BUTTON, || { + View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::BUTTON, || { // Setup values for modal. self.api_port_edit = port; self.api_port_available_edit = self.is_api_port_available; @@ -285,7 +293,7 @@ impl NodeSetup { .size(16.0) .color(Colors::RED)); } else { - NetworkSettings::node_restart_required_ui(ui); + NetworkNodeSettings::node_restart_required_ui(ui); } ui.add_space(12.0); }); @@ -329,7 +337,6 @@ impl NodeSetup { /// Draw API secret token setup ui. fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Setup values for modal let secret_value = match modal_id { Self::API_SECRET_MODAL => NodeConfig::get_api_secret(), _ => NodeConfig::get_foreign_api_secret() @@ -341,7 +348,6 @@ impl NodeSetup { format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled")) }; - // Show button to open secret modal. View::button(ui, secret_text, Colors::BUTTON, || { // Setup values for modal. match modal_id { @@ -428,7 +434,7 @@ impl NodeSetup { }); // Show reminder to restart enabled node. - NetworkSettings::node_restart_required_ui(ui); + NetworkNodeSettings::node_restart_required_ui(ui); ui.add_space(12.0); }); @@ -472,8 +478,7 @@ impl NodeSetup { /// Draw FTL setup ui. fn ftl_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let ftl = NodeConfig::get_ftl(); - // Show button to enter FTL value. - View::button(ui, ftl.clone(), Colors::BUTTON, || { + View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::BUTTON, || { // Setup values for modal. self.ftl_edit = ftl; // Show stratum port modal. @@ -519,7 +524,7 @@ impl NodeSetup { .size(18.0) .color(Colors::RED)); } else { - NetworkSettings::node_restart_required_ui(ui); + NetworkNodeSettings::node_restart_required_ui(ui); } ui.add_space(12.0); }); @@ -559,9 +564,9 @@ impl NodeSetup { let validate = NodeConfig::is_full_chain_validation(); View::checkbox(ui, validate, t!("network_settings.full_validation"), || { NodeConfig::toggle_full_chain_validation(); - NetworkSettings::show_node_restart_required_modal(); + NetworkNodeSettings::show_node_restart_required_modal(); }); - ui.add_space(6.0); + ui.add_space(4.0); ui.label(RichText::new(t!("network_settings.full_validation_description")) .size(16.0) .color(Colors::INACTIVE_TEXT) @@ -573,9 +578,9 @@ impl NodeSetup { let archive_mode = NodeConfig::is_archive_mode(); View::checkbox(ui, archive_mode, t!("network_settings.archive_mode"), || { NodeConfig::toggle_archive_mode(); - NetworkSettings::show_node_restart_required_modal(); + NetworkNodeSettings::show_node_restart_required_modal(); }); - ui.add_space(6.0); + ui.add_space(4.0); ui.label(RichText::new(t!("network_settings.archive_mode_desc")) .size(16.0) .color(Colors::INACTIVE_TEXT) diff --git a/src/gui/views/network/settings/stratum.rs b/src/gui/views/network/settings/stratum.rs new file mode 100644 index 0000000..7600341 --- /dev/null +++ b/src/gui/views/network/settings/stratum.rs @@ -0,0 +1,413 @@ +// 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, TextStyle, Widget}; + +use crate::gui::{Colors, Navigator}; +use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, TIMER}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, ModalPosition, View}; +use crate::gui::views::network::node_settings::NetworkNodeSettings; +use crate::node::{Node, NodeConfig}; + +/// Stratum server setup ui section. +pub struct StratumServerSetup { + /// Stratum port value to be used inside edit modal. + stratum_port_edit: String, + /// Flag to check if stratum port is available inside edit modal. + stratum_port_available_edit: bool, + + /// Flag to check if stratum port is available from saved config value. + pub(crate) is_port_available: bool, + + /// Attempt time value to be used inside edit modal. + attempt_time_edit: String, + + /// Minimum share difficulty value to be used inside edit modal. + min_share_diff_edit: String +} + +impl Default for StratumServerSetup { + fn default() -> Self { + let (ip, port) = NodeConfig::get_stratum_address(); + let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); + let attempt_time = NodeConfig::get_stratum_attempt_time(); + let min_share_diff = NodeConfig::get_stratum_min_share_diff(); + Self { + stratum_port_edit: port, + stratum_port_available_edit: is_port_available, + is_port_available, + attempt_time_edit: attempt_time, + min_share_diff_edit: min_share_diff + + } + } +} + +impl StratumServerSetup { + /// Identifier for stratum port [`Modal`]. + pub const STRATUM_PORT_MODAL: &'static str = "stratum_port"; + /// Identifier for attempt time [`Modal`]. + pub const STRATUM_ATTEMPT_TIME_MODAL: &'static str = "stratum_attempt_time"; + /// Identifier for minimum share difficulty [`Modal`]. + pub const STRATUM_MIN_SHARE_MODAL: &'static str = "stratum_min_share"; + + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server_setup"))); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + + ui.vertical_centered(|ui| { + // Show button to enable stratum server if port is available and server is not running. + if self.is_port_available && !Node::is_stratum_server_starting() && Node::is_running() + && !Node::get_stratum_stats().is_running { + ui.add_space(6.0); + View::button(ui, t!("network_mining.enable_server"), Colors::GOLD, || { + Node::start_stratum_server(); + }); + ui.add_space(6.0); + } + + // Show stratum server autorun checkbox. + let stratum_enabled = NodeConfig::is_stratum_autorun_enabled(); + View::checkbox(ui, stratum_enabled, t!("network.autorun"), || { + NodeConfig::toggle_stratum_autorun(); + }); + ui.add_space(4.0); + + // Show message to restart node after changing of stratum settings + ui.label(RichText::new(t!("network_mining.info_settings")) + .size(16.0) + .color(Colors::INACTIVE_TEXT) + ); + ui.add_space(8.0); + }); + + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + + // Show message when IP addresses are not available on the system. + let all_ips = NodeConfig::get_ip_addrs(); + if all_ips.is_empty() { + NetworkNodeSettings::no_ip_address_ui(ui); + return; + } + + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("network_settings.ip")) + .size(16.0) + .color(Colors::GRAY) + ); + ui.add_space(6.0); + // Show stratum IP addresses to select. + let (ip, port) = NodeConfig::get_stratum_address(); + NetworkNodeSettings::ip_addrs_ui(ui, &ip, &all_ips, |selected_ip| { + NodeConfig::save_stratum_address(selected_ip, &port); + self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port); + + }); + // Show stratum port setup. + self.port_setup_ui(ui, cb); + + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + + // Show attempt time setup. + self.attempt_time_ui(ui, cb); + + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + + // Show minimum acceptable share difficulty setup. + self.min_diff_ui(ui, cb); + }); + } + + /// Draw stratum port value setup ui. + fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(t!("network_settings.port")) + .size(16.0) + .color(Colors::GRAY) + ); + ui.add_space(6.0); + + let (_, port) = NodeConfig::get_stratum_address(); + View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::BUTTON, || { + // Setup values for modal. + self.stratum_port_edit = port; + self.stratum_port_available_edit = self.is_port_available; + // Show stratum port modal. + let port_modal = Modal::new(Self::STRATUM_PORT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")); + Navigator::show_modal(port_modal); + cb.show_keyboard(); + }); + ui.add_space(12.0); + + // Show error when stratum server port is unavailable. + if !self.is_port_available { + ui.add_space(6.0); + ui.label(RichText::new(t!("network_settings.port_unavailable")) + .size(16.0) + .color(Colors::RED)); + ui.add_space(12.0); + } + } + + /// Draw stratum port [`Modal`] content ui. + pub fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("network_settings.stratum_port")) + .size(18.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw stratum port text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.stratum_port_edit) + .id(Id::from(modal.id)) + .font(TextStyle::Heading) + .desired_width(58.0) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified port is unavailable. + if !self.stratum_port_available_edit { + ui.add_space(12.0); + ui.label(RichText::new(t!("network_settings.port_unavailable")) + .size(18.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); + + // Save button callback + let on_save = || { + // Check if port is available. + let (stratum_ip, _) = NodeConfig::get_stratum_address(); + let available = NodeConfig::is_stratum_port_available( + &stratum_ip, + &self.stratum_port_edit + ); + self.stratum_port_available_edit = available; + + // Save port at config if it's available. + if available { + NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit); + + self.is_port_available = true; + 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.save"), Colors::WHITE, on_save); + }); + }); + ui.add_space(6.0); + }); + }); + } + + /// Draw attempt time value setup ui. + fn attempt_time_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(t!("network_settings.attempt_time_desc")) + .size(16.0) + .color(Colors::GRAY) + ); + ui.add_space(6.0); + + let time = NodeConfig::get_stratum_attempt_time(); + View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::BUTTON, || { + // Setup values for modal. + self.attempt_time_edit = time; + + // Show attempt time modal. + let time_modal = Modal::new(Self::STRATUM_ATTEMPT_TIME_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")); + Navigator::show_modal(time_modal); + cb.show_keyboard(); + }); + ui.add_space(12.0); + } + + /// Draw attempt time [`Modal`] content ui. + pub fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("network_settings.attempt_time")) + .size(18.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw stratum port text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.attempt_time_edit) + .id(Id::from(modal.id)) + .font(TextStyle::Heading) + .desired_width(34.0) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified value is not valid or reminder to restart enabled node. + if self.attempt_time_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label(RichText::new(t!("network_settings.not_valid_value")) + .size(18.0) + .color(Colors::RED)); + } else { + NetworkNodeSettings::node_restart_required_ui(ui); + } + 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); + + // Save button callback + let on_save = || { + if let Ok(time) = self.attempt_time_edit.parse::() { + NodeConfig::save_stratum_attempt_time(time); + 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.save"), Colors::WHITE, on_save); + }); + }); + ui.add_space(6.0); + }); + } + + /// Draw minimum share difficulty value setup ui. + fn min_diff_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(t!("network_settings.min_share_diff")) + .size(16.0) + .color(Colors::GRAY) + ); + ui.add_space(6.0); + + let diff = NodeConfig::get_stratum_min_share_diff(); + View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::BUTTON, || { + // Setup values for modal. + self.min_share_diff_edit = diff; + + // Show attempt time modal. + let diff_modal = Modal::new(Self::STRATUM_MIN_SHARE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")); + Navigator::show_modal(diff_modal); + cb.show_keyboard(); + }); + ui.add_space(12.0); + } + + /// Draw minimum acceptable share difficulty [`Modal`] content ui. + pub fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("network_settings.min_share_diff")) + .size(18.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw stratum port text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.min_share_diff_edit) + .id(Id::from(modal.id)) + .font(TextStyle::Heading) + .desired_width(34.0) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified value is not valid or reminder to restart enabled node. + if self.min_share_diff_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label(RichText::new(t!("network_settings.not_valid_value")) + .size(18.0) + .color(Colors::RED)); + } else { + NetworkNodeSettings::node_restart_required_ui(ui); + } + 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); + + // Save button callback + let on_save = || { + if let Ok(diff) = self.min_share_diff_edit.parse::() { + NodeConfig::save_stratum_min_share_diff(diff); + 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.save"), Colors::WHITE, on_save); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/settings_stratum.rs b/src/gui/views/settings_stratum.rs deleted file mode 100644 index 19d67c9..0000000 --- a/src/gui/views/settings_stratum.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2023 The Grim Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use egui::{RichText, TextStyle, Widget}; - -use crate::gui::{Colors, Navigator}; -use crate::gui::icons::WRENCH; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, ModalPosition, View}; -use crate::gui::views::network_settings::NetworkSettings; -use crate::node::NodeConfig; - -/// Stratum server setup ui section. -pub struct StratumServerSetup { - /// Stratum port to be used inside edit modal. - stratum_port_edit: String, - /// Flag to check if stratum port is available inside edit modal. - stratum_port_available_edit: bool, - - /// Flag to check if stratum port is available from saved config value. - pub(crate) is_port_available: bool -} - -impl Default for StratumServerSetup { - fn default() -> Self { - let (ip, port) = NodeConfig::get_stratum_address(); - let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port); - Self { - stratum_port_edit: port, - stratum_port_available_edit: is_port_available, - is_port_available - } - } -} - -impl StratumServerSetup { - /// Identifier for stratum port [`Modal`]. - pub const STRATUM_PORT_MODAL: &'static str = "stratum_port"; - - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - View::sub_title(ui, format!("{} {}", WRENCH, t!("network_mining.server_setup"))); - View::horizontal_line(ui, Colors::ITEM_STROKE); - ui.add_space(4.0); - - // Show message when IP addresses are not available on the system. - let all_ips = NetworkSettings::get_ip_addrs(); - if all_ips.is_empty() { - NetworkSettings::no_ip_address_ui(ui); - return; - } - - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.ip")) - .size(16.0) - .color(Colors::GRAY) - ); - ui.add_space(6.0); - // Show stratum IP addresses to select. - let (ip, port) = NodeConfig::get_stratum_address(); - NetworkSettings::ip_addrs_ui(ui, &ip, &all_ips, |selected_ip| { - self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port); - NodeConfig::save_stratum_address(selected_ip, &port); - }); - - ui.label(RichText::new(t!("network_settings.port")) - .size(16.0) - .color(Colors::GRAY) - ); - ui.add_space(6.0); - // Show stratum port setup. - self.port_setup_ui(ui, cb); - - View::horizontal_line(ui, Colors::ITEM_STROKE); - ui.add_space(6.0); - }); - } - - /// Draw stratum port setup ui. - fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let (_, port) = NodeConfig::get_stratum_address(); - // Show button to enter stratum server port. - View::button(ui, port.clone(), Colors::BUTTON, || { - // Setup values for modal. - self.stratum_port_edit = port; - self.stratum_port_available_edit = self.is_port_available; - // Show stratum port modal. - let port_modal = Modal::new(Self::STRATUM_PORT_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("network_settings.change_value")); - Navigator::show_modal(port_modal); - cb.show_keyboard(); - }); - ui.add_space(14.0); - - // Show error when stratum server port is unavailable. - if !self.is_port_available { - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(16.0) - .color(Colors::RED)); - ui.add_space(12.0); - } - } - - /// Draw stratum port [`Modal`] content ui. - pub fn stratum_port_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("network_settings.stratum_port")) - .size(18.0) - .color(Colors::GRAY)); - ui.add_space(8.0); - - // Draw stratum port text edit. - let text_edit_resp = egui::TextEdit::singleline(&mut self.stratum_port_edit) - .font(TextStyle::Heading) - .desired_width(58.0) - .cursor_at_end(true) - .ui(ui); - text_edit_resp.request_focus(); - if text_edit_resp.clicked() { - cb.show_keyboard(); - } - - // Show error when specified port is unavailable. - if !self.stratum_port_available_edit { - ui.add_space(12.0); - ui.label(RichText::new(t!("network_settings.port_unavailable")) - .size(18.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); - - // Save button callback - let on_save = || { - // Check if port is available. - let (stratum_ip, _) = NodeConfig::get_stratum_address(); - let available = NodeConfig::is_stratum_port_available( - &stratum_ip, - &self.stratum_port_edit - ); - self.stratum_port_available_edit = available; - - // Save port at config if it's available. - if available { - NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit); - - self.is_port_available = true; - 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.save"), Colors::WHITE, on_save); - }); - }); - ui.add_space(6.0); - }); - }); - } -} \ No newline at end of file diff --git a/src/gui/views/title_panel.rs b/src/gui/views/title_panel.rs index 567c1bb..11bc7c6 100644 --- a/src/gui/views/title_panel.rs +++ b/src/gui/views/title_panel.rs @@ -18,13 +18,13 @@ use egui_extras::{Size, StripBuilder}; use crate::gui::Colors; use crate::gui::views::View; -pub struct TitlePanelAction<'action> { - pub(crate) icon: Box<&'action str>, +pub struct TitlePanelAction { + pub(crate) icon: Box<&'static str>, pub(crate) on_click: Box, } -impl<'action> TitlePanelAction<'action> { - pub fn new(icon: &'action str, on_click: fn()) -> Option { +impl TitlePanelAction { + pub fn new(icon: &'static str, on_click: fn()) -> Option { Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) }) } } diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 51c88a5..2e6353c 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -17,7 +17,7 @@ use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke}; use egui::epaint::text::TextWrapping; use egui::text::{LayoutJob, TextFormat}; -use crate::gui::{Colors, Navigator}; +use crate::gui::Colors; use crate::gui::icons::{CHECK_SQUARE, SQUARE}; pub struct View; @@ -93,16 +93,16 @@ impl View { /// Tab button with white background fill color, contains only icon. pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) { let text_color = match active { - true => { Colors::TITLE } - false => { Colors::TEXT } + true => Colors::TITLE, + false => Colors::TEXT }; let stroke = match active { - true => { Stroke::NONE } - false => { Self::DEFAULT_STROKE } + true => Stroke::NONE, + false => Self::DEFAULT_STROKE }; let color = match active { - true => { Colors::FILL } - false => { Colors::WHITE } + true => Colors::FILL, + false => Colors::WHITE }; let br = Button::new(RichText::new(icon.to_string()).size(24.0).color(text_color)) .stroke(stroke) @@ -115,7 +115,8 @@ impl View { /// Draw [`Button`] with specified background fill color. pub fn button(ui: &mut egui::Ui, text: String, fill_color: Color32, action: impl FnOnce()) { - let br = Button::new(RichText::new(text.to_uppercase()).size(18.0).color(Colors::TEXT_BUTTON)) + let button_text = RichText::new(text.to_uppercase()).size(18.0).color(Colors::TEXT_BUTTON); + let br = Button::new(button_text) .stroke(Self::DEFAULT_STROKE) .fill(fill_color) .ui(ui); @@ -201,14 +202,14 @@ impl View { /// Draw small gold loading spinner. pub fn small_loading_spinner(ui: &mut egui::Ui) { - Spinner::new().size(42.0).color(Colors::GOLD).ui(ui); + Spinner::new().size(38.0).color(Colors::GOLD).ui(ui); } /// Draw the button that looks like checkbox with callback on check. pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: String, callback: impl FnOnce()) { let (text_value, color) = match checked { - true => { (format!("{} {}", CHECK_SQUARE, text), Colors::TEXT_BUTTON) } - false => { (format!("{} {}", SQUARE, text), Colors::TEXT) } + true => (format!("{} {}", CHECK_SQUARE, text), Colors::TEXT_BUTTON), + false => (format!("{} {}", SQUARE, text), Colors::TEXT) }; let br = Button::new(RichText::new(text_value).size(18.0).color(color)) diff --git a/src/node/config.rs b/src/node/config.rs index 6322ecd..e99d100 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -100,6 +100,19 @@ impl NodeConfig { api_secret_path } + /// List of available IP addresses. + pub fn get_ip_addrs() -> Vec { + let mut ip_addrs = Vec::new(); + for net_if in pnet::datalink::interfaces() { + for ip in net_if.ips { + if ip.is_ipv4() { + ip_addrs.push(ip.ip()); + } + } + } + ip_addrs + } + /// Check whether a port is available on the provided host. fn is_port_available(host: &String, port: &String) -> bool { if let Ok(p) = port.parse::() { @@ -142,6 +155,17 @@ impl NodeConfig { /// Check if stratum server port is available across the system and config. pub fn is_stratum_port_available(ip: &String, port: &String) -> bool { + if Node::get_stratum_stats().is_running { + // Check if Stratum server with same address is running. + let (cur_ip, cur_port) = Self::get_stratum_address(); + let same_running = ip == &cur_ip && port == &cur_port; + return same_running || Self::is_not_running_stratum_port_available(ip, port); + } + Self::is_not_running_stratum_port_available(&ip, &port) + } + + /// Check if stratum port is available when server is not running. + fn is_not_running_stratum_port_available(ip: &String, port: &String) -> bool { if Self::is_port_available(&ip, &port) { if &Self::get_p2p_port().to_string() != port { let (api_ip, api_port) = Self::get_api_address(); @@ -161,6 +185,54 @@ impl NodeConfig { r_config.members.clone().server.stratum_mining_config.unwrap().wallet_listener_url } + /// Get the amount of time in seconds to attempt to mine on a particular header. + pub fn get_stratum_attempt_time() -> String { + let r_config = Settings::node_config_to_read(); + r_config.members + .clone() + .server + .stratum_mining_config + .unwrap() + .attempt_time_per_block + .to_string() + } + + /// Save stratum attempt time value in seconds. + pub fn save_stratum_attempt_time(time: u32) { + let mut w_node_config = Settings::node_config_to_update(); + w_node_config.members + .clone() + .server + .stratum_mining_config + .unwrap() + .attempt_time_per_block = time; + w_node_config.save(); + } + + /// Get minimum acceptable share difficulty to request from miners. + pub fn get_stratum_min_share_diff() -> String { + let r_config = Settings::node_config_to_read(); + r_config.members + .clone() + .server + .stratum_mining_config + .unwrap() + .minimum_share_difficulty + .to_string() + } + + /// Save minimum acceptable share difficulty. + pub fn save_stratum_min_share_diff(diff: u64) { + let mut w_node_config = Settings::node_config_to_update(); + w_node_config.members + .clone() + .server + .stratum_mining_config + .unwrap() + .minimum_share_difficulty = diff; + w_node_config.save(); + } + /// Check if stratum mining server autorun is enabled. pub fn is_stratum_autorun_enabled() -> bool { let stratum_config = Settings::node_config_to_read() @@ -212,17 +284,17 @@ impl NodeConfig { pub fn is_api_port_available(ip: &String, port: &String) -> bool { if Node::is_running() { // Check if API server with same address is running. - let same_running = if let Some(running_addr) = Node::get_api_addr() { + let same_running = if let Some(running_addr) = Node::get_api_addr() { running_addr == format!("{}:{}", ip, port) } else { false }; - if same_running || NodeConfig::is_port_available(&ip, &port) { - return &NodeConfig::get_p2p_port().to_string() != port; + if same_running || Self::is_port_available(&ip, &port) { + return &Self::get_p2p_port().to_string() != port; } return false; - } else if NodeConfig::is_port_available(&ip, &port) { - return &NodeConfig::get_p2p_port().to_string() != port; + } else if Self::is_port_available(&ip, &port) { + return &Self::get_p2p_port().to_string() != port; } false } @@ -472,7 +544,7 @@ impl NodeConfig { } /// Set how long a banned peer should stay banned in ms. - pub fn set_ban_window(time: i64) { + pub fn save_ban_window(time: i64) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.p2p_config.ban_window = Some(time); w_node_config.save(); @@ -484,7 +556,7 @@ impl NodeConfig { } /// Set maximum number of inbound peer connections. - pub fn set_max_inbound_count(count: u32) { + pub fn save_max_inbound_count(count: u32) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.p2p_config.peer_max_inbound_count = Some(count); w_node_config.save(); @@ -496,7 +568,7 @@ impl NodeConfig { } /// Set maximum number of outbound peer connections. - pub fn set_max_outbound_count(count: u32) { + pub fn save_max_outbound_count(count: u32) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.p2p_config.peer_max_outbound_count = Some(count); w_node_config.save(); @@ -512,7 +584,7 @@ impl NodeConfig { } /// Set minimum number of outbound peer connections. - pub fn set_min_outbound_count(count: u32) { + pub fn save_min_outbound_count(count: u32) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.p2p_config.peer_min_preferred_outbound_count = Some(count); w_node_config.save(); @@ -526,7 +598,7 @@ impl NodeConfig { } /// Set base fee that's accepted into the pool. - pub fn set_base_fee(fee: u64) { + pub fn save_base_fee(fee: u64) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.pool_config.accept_fee_base = fee; w_node_config.save(); @@ -538,7 +610,7 @@ impl NodeConfig { } /// Set reorg cache retention period in minute. - pub fn set_reorg_cache_period(period: u32) { + pub fn save_reorg_cache_period(period: u32) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.pool_config.reorg_cache_period = period; w_node_config.save(); @@ -550,7 +622,7 @@ impl NodeConfig { } /// Set max amount of transactions at pool. - pub fn set_max_pool_size(amount: usize) { + pub fn save_max_pool_size(amount: usize) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.pool_config.max_pool_size = amount; w_node_config.save(); @@ -562,7 +634,7 @@ impl NodeConfig { } /// Set max amount of transactions at stem pool. - pub fn set_max_stempool_size(amount: usize) { + pub fn save_max_stempool_size(amount: usize) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.pool_config.max_stempool_size = amount; w_node_config.save(); @@ -574,7 +646,7 @@ impl NodeConfig { } /// Set max total weight of transactions that can get selected to build a block. - pub fn set_mineable_max_weight(weight: u64) { + pub fn save_mineable_max_weight(weight: u64) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.pool_config.mineable_max_weight = weight; w_node_config.save(); @@ -588,7 +660,7 @@ impl NodeConfig { } /// Set Dandelion epoch duration in secs. - pub fn set_epoch(secs: u16) { + pub fn save_epoch(secs: u16) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.dandelion_config.epoch_secs = secs; w_node_config.save(); @@ -601,7 +673,7 @@ impl NodeConfig { } /// Set Dandelion embargo timer. - pub fn set_embargo(secs: u16) { + pub fn save_embargo(secs: u16) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.dandelion_config.embargo_secs = secs; w_node_config.save(); @@ -613,7 +685,7 @@ impl NodeConfig { } /// Set Dandelion stem probability. - pub fn set_stem_probability(percent: u8) { + pub fn save_stem_probability(percent: u8) { let mut w_node_config = Settings::node_config_to_update(); w_node_config.members.server.dandelion_config.stem_probability = percent; w_node_config.save(); diff --git a/src/node/mine_block.rs b/src/node/mine_block.rs new file mode 100644 index 0000000..01ed940 --- /dev/null +++ b/src/node/mine_block.rs @@ -0,0 +1,298 @@ +// 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. + +//! Build a block to mine: gathers transactions from the pool, assembles +//! them into a block and returns it. + +use chrono::prelude::{DateTime, NaiveDateTime, Utc}; +use rand::{thread_rng, Rng}; +use serde_json::{json, Value}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use grin_api; +use grin_chain; +use grin_servers::common::types::Error; +use grin_core::core::{Output, TxKernel}; +use grin_core::libtx::secp_ser; +use grin_core::libtx::ProofBuilder; +use grin_core::{consensus, core, global}; +use grin_keychain::{ExtKeychain, Identifier, Keychain}; +use grin_servers::ServerTxPool; +use log::{debug, error, trace, warn}; +use serde_derive::{Deserialize, Serialize}; + +/// Fees in block to use for coinbase amount calculation +/// (Duplicated from Grin wallet project) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BlockFees { + /// fees + #[serde(with = "secp_ser::string_or_u64")] + pub fees: u64, + /// height + #[serde(with = "secp_ser::string_or_u64")] + pub height: u64, + /// key id + pub key_id: Option, +} + +impl BlockFees { + /// return key id + pub fn key_id(&self) -> Option { + self.key_id.clone() + } +} + +/// Response to build a coinbase output. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CbData { + /// Output + pub output: Output, + /// Kernel + pub kernel: TxKernel, + /// Key Id + pub key_id: Option, +} + +// Ensure a block suitable for mining is built and returned +// If a wallet listener URL is not provided the reward will be "burnt" +// Warning: This call does not return until/unless a new block can be built +pub fn get_block( + chain: &Arc, + tx_pool: &ServerTxPool, + key_id: Option, + wallet_listener_url: Option, +) -> (core::Block, BlockFees) { + let wallet_retry_interval = 5; + // get the latest chain state and build a block on top of it + let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone()); + while let Err(e) = result { + let mut new_key_id = key_id.to_owned(); + match e { + self::Error::Chain(c) => match c { + grin_chain::Error::DuplicateCommitment(_) => { + debug!( + "Duplicate commit for potential coinbase detected. Trying next derivation." + ); + // use the next available key to generate a different coinbase commitment + new_key_id = None; + } + _ => { + error!("Chain Error: {}", c); + } + }, + self::Error::WalletComm(_) => { + error!( + "Error building new block: Can't connect to wallet listener at {:?}; will retry", + wallet_listener_url.as_ref().unwrap() + ); + thread::sleep(Duration::from_secs(wallet_retry_interval)); + } + ae => { + warn!("Error building new block: {:?}. Retrying.", ae); + } + } + + // only wait if we are still using the same key: a different coinbase commitment is unlikely + // to have duplication + if new_key_id.is_some() { + thread::sleep(Duration::from_millis(100)); + } + + result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone()); + } + return result.unwrap(); +} + +/// Builds a new block with the chain head as previous and eligible +/// transactions from the pool. +fn build_block( + chain: &Arc, + tx_pool: &ServerTxPool, + key_id: Option, + wallet_listener_url: Option, +) -> Result<(core::Block, BlockFees), Error> { + let head = chain.head_header()?; + + // prepare the block header timestamp + let mut now_sec = Utc::now().timestamp(); + let head_sec = head.timestamp.timestamp(); + if now_sec <= head_sec { + now_sec = head_sec + 1; + } + + // Determine the difficulty our block should be at. + // Note: do not keep the difficulty_iter in scope (it has an active batch). + let difficulty = consensus::next_difficulty(head.height + 1, chain.difficulty_iter()?); + + // Extract current "mineable" transactions from the pool. + // If this fails for *any* reason then fallback to an empty vec of txs. + // This will allow us to mine an "empty" block if the txpool is in an + // invalid (and unexpected) state. + let txs = match tx_pool.read().prepare_mineable_transactions() { + Ok(txs) => txs, + Err(e) => { + error!( + "build_block: Failed to prepare mineable txs from txpool: {:?}", + e + ); + warn!("build_block: Falling back to mining empty block."); + vec![] + } + }; + + // build the coinbase and the block itself + let fees = txs.iter().map(|tx| tx.fee()).sum(); + let height = head.height + 1; + let block_fees = BlockFees { + fees, + key_id, + height, + }; + + let (output, kernel, block_fees) = get_coinbase(wallet_listener_url, block_fees)?; + let mut b = core::Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?; + + // making sure we're not spending time mining a useless block + b.validate(&head.total_kernel_offset)?; + + b.header.pow.nonce = thread_rng().gen(); + b.header.pow.secondary_scaling = difficulty.secondary_scaling; + b.header.timestamp = DateTime::::from_utc(NaiveDateTime::from_timestamp(now_sec, 0), Utc); + + debug!( + "Built new block with {} inputs and {} outputs, block difficulty: {}, cumulative difficulty {}", + b.inputs().len(), + b.outputs().len(), + difficulty.difficulty, + b.header.total_difficulty().to_num(), + ); + + // Now set txhashset roots and sizes on the header of the block being built. + match chain.set_txhashset_roots(&mut b) { + Ok(_) => Ok((b, block_fees)), + Err(e) => { + match e { + // If this is a duplicate commitment then likely trying to use + // a key that hass already been derived but not in the wallet + // for some reason, allow caller to retry. + grin_chain::Error::DuplicateCommitment(e) => { + Err(Error::Chain(grin_chain::Error::DuplicateCommitment(e))) + } + + // Some other issue, possibly duplicate kernel + _ => { + error!("Error setting txhashset root to build a block: {:?}", e); + Err(Error::Chain(grin_chain::Error::Other(format!("{:?}", e)))) + } + } + } + } +} + +/// +/// Probably only want to do this when testing. +/// +fn burn_reward(block_fees: BlockFees) -> Result<(Output, TxKernel, BlockFees), Error> { + warn!("Burning block fees: {:?}", block_fees); + let keychain = ExtKeychain::from_random_seed(global::is_testnet())?; + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let (out, kernel) = grin_core::libtx::reward::output( + &keychain, + &ProofBuilder::new(&keychain), + &key_id, + block_fees.fees, + false, + ) + .unwrap(); + Ok((out, kernel, block_fees)) +} + +// Connect to the wallet listener and get coinbase. +// Warning: If a wallet listener URL is not provided the reward will be "burnt" +fn get_coinbase( + wallet_listener_url: Option, + block_fees: BlockFees, +) -> Result<(core::Output, core::TxKernel, BlockFees), Error> { + match wallet_listener_url { + None => { + // Burn it + return burn_reward(block_fees); + } + Some(wallet_listener_url) => { + let res = create_coinbase(&wallet_listener_url, &block_fees)?; + let output = res.output; + let kernel = res.kernel; + let key_id = res.key_id; + let block_fees = BlockFees { + key_id: key_id, + ..block_fees + }; + + debug!("get_coinbase: {:?}", block_fees); + return Ok((output, kernel, block_fees)); + } + } +} + +/// Call the wallet API to create a coinbase output for the given block_fees. +/// Will retry based on default "retry forever with backoff" behavior. +fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result { + let url = format!("{}/v2/foreign", dest); + let req_body = json!({ + "jsonrpc": "2.0", + "method": "build_coinbase", + "id": 1, + "params": { + "block_fees": block_fees + } + }); + + trace!("Sending build_coinbase request: {}", req_body); + let req = grin_api::client::create_post_request(url.as_str(), None, &req_body)?; + let timeout = grin_api::client::TimeOut::default(); + let res: String = grin_api::client::send_request(req, timeout).map_err(|e| { + let report = format!( + "Failed to get coinbase from {}. Is the wallet listening? {}", + dest, e + ); + error!("{}", report); + Error::WalletComm(report) + })?; + + let res: Value = serde_json::from_str(&res).unwrap(); + trace!("Response: {}", res); + if res["error"] != json!(null) { + let report = format!( + "Failed to get coinbase from {}: Error: {}, Message: {}", + dest, res["error"]["code"], res["error"]["message"] + ); + error!("{}", report); + return Err(Error::WalletComm(report)); + } + + let cb_data = res["result"]["Ok"].clone(); + trace!("cb_data: {}", cb_data); + let ret_val = match serde_json::from_value::(cb_data) { + Ok(r) => r, + Err(e) => { + let report = format!("Couldn't deserialize CbData: {}", e); + error!("{}", report); + return Err(Error::WalletComm(report)); + } + }; + + Ok(ret_val) +} diff --git a/src/node/mod.rs b/src/node/mod.rs index d0bea2f..3118bae 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -16,4 +16,8 @@ mod node; pub use node::Node; mod config; + +mod stratum; +mod mine_block; + pub use config::NodeConfig; \ No newline at end of file diff --git a/src/node/node.rs b/src/node/node.rs index ae72b30..34f5ad7 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -22,11 +22,12 @@ use futures::channel::oneshot; use grin_chain::SyncStatus; use grin_core::global; use grin_core::global::ChainTypes; -use grin_servers::{Server, ServerStats}; +use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats}; use grin_servers::common::types::Error; use jni::sys::{jboolean, jstring}; use lazy_static::lazy_static; use crate::node::NodeConfig; +use crate::node::stratum::StratumServer; lazy_static! { /// Static thread-aware state of [`Node`] to be updated from another thread. @@ -35,8 +36,10 @@ lazy_static! { /// Provides [`Server`] control, holds current status and statistics. pub struct Node { - /// Statistics data for UI. + /// The node [`Server`] statistics for UI. stats: Arc>>, + /// Stratum server statistics. + stratum_stats: Arc>, /// Running API server address. api_addr: Arc>>, /// Running P2P server port. @@ -49,8 +52,8 @@ pub struct Node { stop_needed: AtomicBool, /// Flag to check if app exit is needed after server stop. exit_after_stop: AtomicBool, - /// Thread flag to start stratum server at separate. - start_stratum_server: AtomicBool, + /// Thread flag to start stratum server. + start_stratum_needed: AtomicBool, /// Error on [`Server`] start. init_error: Option } @@ -59,13 +62,14 @@ impl Default for Node { fn default() -> Self { Self { stats: Arc::new(RwLock::new(None)), + stratum_stats: Arc::new(grin_util::RwLock::new(StratumStats::default())), api_addr: Arc::new(RwLock::new(None)), p2p_port: Arc::new(RwLock::new(None)), starting: AtomicBool::new(false), restart_needed: AtomicBool::new(false), stop_needed: AtomicBool::new(false), exit_after_stop: AtomicBool::new(false), - start_stratum_server: AtomicBool::new(false), + start_stratum_needed: AtomicBool::new(false), init_error: None } } @@ -114,14 +118,14 @@ impl Node { } } - /// Start stratum server. + /// Request to start stratum server. pub fn start_stratum_server() { - NODE_STATE.start_stratum_server.store(true, Ordering::Relaxed); + NODE_STATE.start_stratum_needed.store(true, Ordering::Relaxed); } /// Check if stratum server is starting. pub fn is_stratum_server_starting() -> bool { - NODE_STATE.start_stratum_server.load(Ordering::Relaxed) + NODE_STATE.start_stratum_needed.load(Ordering::Relaxed) } /// Check if node is starting. @@ -149,6 +153,11 @@ impl Node { NODE_STATE.stats.read().unwrap() } + /// Get stratum server [`Server`] statistics. + pub fn get_stratum_stats() -> grin_util::RwLockReadGuard<'static, StratumStats> { + NODE_STATE.stratum_stats.read() + } + /// Get synchronization status, empty when [`Server`] is not running. pub fn get_sync_status() -> Option { // Return Shutdown status when node is stopping. @@ -169,13 +178,13 @@ impl Node { None } - /// Start node [`Server`] at separate thread to update [`NODE_STATE`] with [`ServerStats`]. + /// Start the [`Server`] at separate thread to update state with stats and handle statuses. fn start_server_thread() { thread::spawn(move || { NODE_STATE.starting.store(true, Ordering::Relaxed); // Start the server. - match start_server() { + match start_node_server() { Ok(mut server) => { let mut first_start = true; loop { @@ -183,84 +192,85 @@ impl Node { // Stop the server. server.stop(); + // Reset stratum stats + { + let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); + *w_stratum_stats = StratumStats::default(); + } + // Create new server. - match start_server() { + match start_node_server() { Ok(s) => { server = s; NODE_STATE.restart_needed.store(false, Ordering::Relaxed); } Err(e) => { - NODE_STATE.restart_needed.store(false, Ordering::Relaxed); Self::on_start_error(&e); break; } } } else if Self::is_stopping() { - // Clean server stats. - { - let mut w_stats = NODE_STATE.stats.write().unwrap(); - *w_stats = None; - } - // Stop the server. server.stop(); - - NODE_STATE.starting.store(false, Ordering::Relaxed); - NODE_STATE.stop_needed.store(false, Ordering::Relaxed); - NODE_STATE.start_stratum_server.store(false, Ordering::Relaxed); - - // Clean launched API server address. - { - let mut w_api_addr = NODE_STATE.api_addr.write().unwrap(); - *w_api_addr = None; - } - // Clean launched P2P server port. - { - let mut w_p2p_port = NODE_STATE.p2p_port.write().unwrap(); - *w_p2p_port = None; - } + // Clean stats and statuses. + Self::on_thread_stop(); + // Exit thread loop. break; - } else { - // Start stratum mining server. - if Self::is_stratum_server_starting() { + } + + // Start stratum mining server if requested. + let stratum_start_requested = Self::is_stratum_server_starting(); + if stratum_start_requested { + let (s_ip, s_port) = NodeConfig::get_stratum_address(); + if NodeConfig::is_stratum_port_available(&s_ip, &s_port) { let stratum_config = server .config .stratum_mining_config .clone() .unwrap(); - server.start_stratum_server(stratum_config); - - // Wait for mining server to start and update status. - thread::sleep(Duration::from_millis(100)); - NODE_STATE.start_stratum_server.store(false, Ordering::Relaxed); - } - - // Update server stats. - if let Ok(stats) = server.get_server_stats() { - { - let mut w_stats = NODE_STATE.stats.write().unwrap(); - *w_stats = Some(stats); - } - - if first_start { - NODE_STATE.starting.store(false, Ordering::Relaxed); - first_start = false; - } + start_stratum_mining_server(&server, stratum_config); } } + + // Update server stats. + if let Ok(stats) = server.get_server_stats() { + { + let mut w_stats = NODE_STATE.stats.write().unwrap(); + *w_stats = Some(stats.clone()); + } + + if first_start { + NODE_STATE.starting.store(false, Ordering::Relaxed); + first_start = false; + } + } + + if stratum_start_requested { + NODE_STATE.start_stratum_needed.store(false, Ordering::Relaxed); + } + thread::sleep(Duration::from_millis(250)); } } Err(e) => { - NODE_STATE.starting.store(false, Ordering::Relaxed); Self::on_start_error(&e); } } }); } - /// Handle node [`Server`] error on start. - fn on_start_error(e: &Error) { + /// Reset stats and statuses on [`Server`] thread stop. + fn on_thread_stop() { + NODE_STATE.starting.store(false, Ordering::Relaxed); + NODE_STATE.restart_needed.store(false, Ordering::Relaxed); + NODE_STATE.start_stratum_needed.store(false, Ordering::Relaxed); + NODE_STATE.stop_needed.store(false, Ordering::Relaxed); + + // Reset stratum stats. + { + let mut w_stratum_stats = NODE_STATE.stratum_stats.write(); + *w_stratum_stats = StratumStats::default(); + } // Clean server stats. { let mut w_stats = NODE_STATE.stats.write().unwrap(); @@ -276,6 +286,12 @@ impl Node { let mut w_p2p_port = NODE_STATE.p2p_port.write().unwrap(); *w_p2p_port = None; } + } + + /// Handle node [`Server`] error on start. + fn on_start_error(e: &Error) { + Self::on_thread_stop(); + //TODO: Create error // NODE_STATE.init_error = Some(e); @@ -464,8 +480,8 @@ impl Node { } } -/// Start the [`Server`] for node. -fn start_server() -> Result { +/// Start the node [`Server`]. +fn start_node_server() -> Result { // Get current global config let config = NodeConfig::get_members(); let server_config = config.server.clone(); @@ -545,10 +561,34 @@ fn start_server() -> Result { *w_p2p_port = Some(config.server.p2p_config.port); } + // Put flag to start stratum server if autorun is available. + if NodeConfig::is_stratum_autorun_enabled() { + NODE_STATE.start_stratum_needed.store(true, Ordering::Relaxed); + } + let server_result = Server::new(server_config.clone(), None, api_chan); server_result } +/// Start stratum mining server on a separate thread. +pub fn start_stratum_mining_server(server: &Server, config: StratumServerConfig) { + let proof_size = global::proofsize(); + let sync_state = server.sync_state.clone(); + + let mut stratum_server = StratumServer::new( + config, + server.chain.clone(), + server.tx_pool.clone(), + NODE_STATE.stratum_stats.clone(), + ); + let stop_state = server.stop_state.clone(); + let _ = thread::Builder::new() + .name("stratum_server".to_string()) + .spawn(move || { + stratum_server.run_loop(proof_size, sync_state, stop_state); + }); +} + #[allow(dead_code)] #[cfg(target_os = "android")] #[allow(non_snake_case)] diff --git a/src/node/stratum.rs b/src/node/stratum.rs new file mode 100644 index 0000000..9ff0ccc --- /dev/null +++ b/src/node/stratum.rs @@ -0,0 +1,934 @@ +// 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. + +//! Mining Stratum Server + + + +use futures::channel::mpsc; +use futures::pin_mut; +use futures::{SinkExt, StreamExt, TryStreamExt}; +use tokio::net::TcpListener; +use tokio::runtime::Runtime; +use tokio_util::codec::{Framed, LinesCodec}; + +use grin_util::{RwLock, StopState}; +use chrono::prelude::Utc; +use serde_json::Value; +use std::collections::HashMap; +use std::net::{SocketAddr, TcpStream}; +use std::panic::panic_any; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, SystemTime}; + +use grin_chain::{self, SyncState}; +use grin_servers::common::stats::{StratumStats, WorkerStats}; +use grin_servers::common::types::StratumServerConfig; +use grin_core::consensus::graph_weight; +use grin_core::core::hash::Hashed; +use grin_core::core::Block; +use grin_core::global; +use grin_core::{pow, ser}; +use crate::node::mine_block; +use grin_util::ToHex; +use grin_servers::ServerTxPool; +use log::{debug, error, info, warn}; +use serde_derive::{Deserialize, Serialize}; + +type Tx = mpsc::UnboundedSender; + + +// ---------------------------------------- +// http://www.jsonrpc.org/specification +// RPC Methods + +/// Represents a compliant JSON RPC 2.0 id. +/// Valid id: Integer, String. +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(untagged)] +enum JsonId { + IntId(u32), + StrId(String), +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct RpcRequest { + id: JsonId, + jsonrpc: String, + method: String, + params: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct RpcResponse { + id: JsonId, + jsonrpc: String, + method: String, + result: Option, + error: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +struct RpcError { + code: i32, + message: String, +} + +impl RpcError { + pub fn internal_error() -> Self { + RpcError { + code: 32603, + message: "Internal error".to_owned(), + } + } + pub fn node_is_syncing() -> Self { + RpcError { + code: -32000, + message: "Node is syncing - Please wait".to_owned(), + } + } + pub fn method_not_found() -> Self { + RpcError { + code: -32601, + message: "Method not found".to_owned(), + } + } + pub fn too_late() -> Self { + RpcError { + code: -32503, + message: "Solution submitted too late".to_string(), + } + } + pub fn cannot_validate() -> Self { + RpcError { + code: -32502, + message: "Failed to validate solution".to_string(), + } + } + pub fn too_low_difficulty() -> Self { + RpcError { + code: -32501, + message: "Share rejected due to low difficulty".to_string(), + } + } + pub fn invalid_request() -> Self { + RpcError { + code: -32600, + message: "Invalid Request".to_string(), + } + } +} + +impl From for Value { + fn from(e: RpcError) -> Self { + serde_json::to_value(e).unwrap() + } +} + +impl From for RpcError + where + T: std::error::Error, +{ + fn from(e: T) -> Self { + error!("Received unhandled error: {}", e); + RpcError::internal_error() + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoginParams { + login: String, + pass: String, + agent: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct SubmitParams { + height: u64, + job_id: u64, + nonce: u64, + edge_bits: u32, + pow: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JobTemplate { + height: u64, + job_id: u64, + difficulty: u64, + pre_pow: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WorkerStatus { + id: String, + height: u64, + difficulty: u64, + accepted: u64, + rejected: u64, + stale: u64, +} + +struct State { + current_block_versions: Vec, + // to prevent the wallet from generating a new HD key derivation for each + // iteration, we keep the returned derivation to provide it back when + // nothing has changed. We only want to create a key_id for each new block, + // and reuse it when we rebuild the current block to add new tx. + current_key_id: Option, + current_difficulty: u64, // scaled + minimum_share_difficulty: u64, // unscaled +} + +impl State { + pub fn new(minimum_share_difficulty: u64) -> Self { + let blocks = vec![Block::default()]; + State { + current_block_versions: blocks, + current_key_id: None, + current_difficulty: ::max_value(), + minimum_share_difficulty: minimum_share_difficulty, + } + } +} + +struct Handler { + id: String, + workers: Arc, + sync_state: Arc, + chain: Arc, + current_state: Arc>, +} + +impl Handler { + pub fn new( + id: String, + stratum_stats: Arc>, + sync_state: Arc, + minimum_share_difficulty: u64, + chain: Arc, + ) -> Self { + Handler { + id: id, + workers: Arc::new(WorkersList::new(stratum_stats)), + sync_state: sync_state, + chain: chain, + current_state: Arc::new(RwLock::new(State::new(minimum_share_difficulty))), + } + } + pub fn from_stratum(stratum: &StratumServer) -> Self { + Handler::new( + stratum.id.clone(), + stratum.stratum_stats.clone(), + stratum.sync_state.clone(), + stratum.config.minimum_share_difficulty, + stratum.chain.clone(), + ) + } + fn handle_rpc_requests(&self, request: RpcRequest, worker_id: usize) -> String { + self.workers.last_seen(worker_id); + + // Call the handler function for requested method + let response = match request.method.as_str() { + "login" => self.handle_login(request.params, worker_id), + "submit" => { + let res = self.handle_submit(request.params, worker_id); + // this key_id has been used now, reset + if let Ok((_, true)) = res { + self.current_state.write().current_key_id = None; + } + res.map(|(v, _)| v) + } + "keepalive" => self.handle_keepalive(), + "getjobtemplate" => { + if self.sync_state.is_syncing() { + Err(RpcError::node_is_syncing()) + } else { + self.handle_getjobtemplate() + } + } + "status" => self.handle_status(worker_id), + _ => { + // Called undefined method + Err(RpcError::method_not_found()) + } + }; + + // Package the reply as RpcResponse json + let resp = match response { + Err(rpc_error) => RpcResponse { + id: request.id, + jsonrpc: String::from("2.0"), + method: request.method, + result: None, + error: Some(rpc_error.into()), + }, + Ok(response) => RpcResponse { + id: request.id, + jsonrpc: String::from("2.0"), + method: request.method, + result: Some(response), + error: None, + }, + }; + serde_json::to_string(&resp).unwrap() + } + fn handle_login(&self, params: Option, worker_id: usize) -> Result { + let params: LoginParams = parse_params(params)?; + self.workers.login(worker_id, params.login, params.agent)?; + return Ok("ok".into()); + } + + // Handle KEEPALIVE message + fn handle_keepalive(&self) -> Result { + return Ok("ok".into()); + } + + fn handle_status(&self, worker_id: usize) -> Result { + // Return worker status in json for use by a dashboard or healthcheck. + let stats = self.workers.get_stats(worker_id)?; + let status = WorkerStatus { + id: stats.id.clone(), + height: self + .current_state + .read() + .current_block_versions + .last() + .unwrap() + .header + .height, + difficulty: stats.pow_difficulty, + accepted: stats.num_accepted, + rejected: stats.num_rejected, + stale: stats.num_stale, + }; + let response = serde_json::to_value(&status).unwrap(); + return Ok(response); + } + // Handle GETJOBTEMPLATE message + fn handle_getjobtemplate(&self) -> Result { + // Build a JobTemplate from a BlockHeader and return JSON + let job_template = self.build_block_template(); + let response = serde_json::to_value(&job_template).unwrap(); + debug!( + "(Server ID: {}) sending block {} with id {} to single worker", + self.id, job_template.height, job_template.job_id, + ); + return Ok(response); + } + + // Build and return a JobTemplate for mining the current block + fn build_block_template(&self) -> JobTemplate { + let bh = self + .current_state + .read() + .current_block_versions + .last() + .unwrap() + .header + .clone(); + // Serialize the block header into pre and post nonce strings + let mut header_buf = vec![]; + { + let mut writer = ser::BinWriter::default(&mut header_buf); + bh.write_pre_pow(&mut writer).unwrap(); + bh.pow.write_pre_pow(&mut writer).unwrap(); + } + let pre_pow = header_buf.to_hex(); + let current_state = self.current_state.read(); + let job_template = JobTemplate { + height: bh.height, + job_id: (current_state.current_block_versions.len() - 1) as u64, + difficulty: current_state.minimum_share_difficulty, + pre_pow, + }; + return job_template; + } + // Handle SUBMIT message + // params contains a solved block header + // We accept and log valid shares of all difficulty above configured minimum + // Accepted shares that are full solutions will also be submitted to the + // network + fn handle_submit( + &self, + params: Option, + worker_id: usize, + ) -> Result<(Value, bool), RpcError> { + // Validate parameters + let params: SubmitParams = parse_params(params)?; + + let state = self.current_state.read(); + // Find the correct version of the block to match this header + let b: Option<&Block> = state.current_block_versions.get(params.job_id as usize); + if params.height != state.current_block_versions.last().unwrap().header.height + || b.is_none() + { + // Return error status + error!( + "(Server ID: {}) Share at height {}, edge_bits {}, nonce {}, job_id {} submitted too late", + self.id, params.height, params.edge_bits, params.nonce, params.job_id, + ); + self.workers.update_stats(worker_id, |ws| ws.num_stale += 1); + return Err(RpcError::too_late()); + } + + let scaled_share_difficulty: u64; + let unscaled_share_difficulty: u64; + let mut share_is_block = false; + + let mut b: Block = b.unwrap().clone(); + // Reconstruct the blocks header with this nonce and pow added + b.header.pow.proof.edge_bits = params.edge_bits as u8; + b.header.pow.nonce = params.nonce; + b.header.pow.proof.nonces = params.pow; + + if !b.header.pow.is_primary() && !b.header.pow.is_secondary() { + // Return error status + error!( + "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}: cuckoo size too small", + self.id, params.height, b.hash(), params.edge_bits, params.nonce, params.job_id, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + + // Get share difficulty values + scaled_share_difficulty = b.header.pow.to_difficulty(b.header.height).to_num(); + unscaled_share_difficulty = b.header.pow.to_unscaled_difficulty().to_num(); + // Note: state.minimum_share_difficulty is unscaled + // state.current_difficulty is scaled + // If the difficulty is too low its an error + if unscaled_share_difficulty < state.minimum_share_difficulty { + // Return error status + error!( + "(Server ID: {}) Share at height {}, hash {}, edge_bits {}, nonce {}, job_id {} rejected due to low difficulty: {}/{}", + self.id, params.height, b.hash(), params.edge_bits, params.nonce, params.job_id, unscaled_share_difficulty, state.minimum_share_difficulty, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::too_low_difficulty()); + } + + // If the difficulty is high enough, submit it (which also validates it) + if scaled_share_difficulty >= state.current_difficulty { + // This is a full solution, submit it to the network + let res = self.chain.process_block(b.clone(), grin_chain::Options::MINE); + if let Err(e) = res { + // Return error status + error!( + "(Server ID: {}) Failed to validate solution at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, {}", + self.id, + params.height, + b.hash(), + params.edge_bits, + params.nonce, + params.job_id, + e, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + share_is_block = true; + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_blocks_found += 1); + self.workers.stratum_stats.write().blocks_found += 1; + // Log message to make it obvious we found a block + let stats = self.workers.get_stats(worker_id)?; + warn!( + "(Server ID: {}) Solution Found for block {}, hash {} - Yay!!! Worker ID: {}, blocks found: {}, shares: {}", + self.id, params.height, + b.hash(), + stats.id, + stats.num_blocks_found, + stats.num_accepted, + ); + } else { + // Do some validation but dont submit + let res = pow::verify_size(&b.header); + if res.is_err() { + // Return error status + error!( + "(Server ID: {}) Failed to validate share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}. {:?}", + self.id, + params.height, + b.hash(), + params.edge_bits, + b.header.pow.nonce, + params.job_id, + res, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_rejected += 1); + return Err(RpcError::cannot_validate()); + } + } + // Log this as a valid share + self.workers.update_edge_bits(params.edge_bits as u16); + let worker = self.workers.get_worker(worker_id)?; + let submitted_by = match worker.login { + None => worker.id.to_string(), + Some(login) => login, + }; + + info!( + "(Server ID: {}) Got share at height {}, hash {}, edge_bits {}, nonce {}, job_id {}, difficulty {}/{}, submitted by {}", + self.id, + b.header.height, + b.hash(), + b.header.pow.proof.edge_bits, + b.header.pow.nonce, + params.job_id, + scaled_share_difficulty, + state.current_difficulty, + submitted_by, + ); + self.workers + .update_stats(worker_id, |worker_stats| worker_stats.num_accepted += 1); + let submit_response = if share_is_block { + format!("blockfound - {}", b.hash().to_hex()) + } else { + "ok".to_string() + }; + return Ok(( + serde_json::to_value(submit_response).unwrap(), + share_is_block, + )); + } // handle submit a solution + + fn broadcast_job(&self) { + debug!("broadcast job"); + // Package new block into RpcRequest + let job_template = self.build_block_template(); + let job_template_json = serde_json::to_string(&job_template).unwrap(); + // Issue #1159 - use a serde_json Value type to avoid extra quoting + let job_template_value: Value = serde_json::from_str(&job_template_json).unwrap(); + let job_request = RpcRequest { + id: JsonId::StrId(String::from("Stratum")), + jsonrpc: String::from("2.0"), + method: String::from("job"), + params: Some(job_template_value), + }; + let job_request_json = serde_json::to_string(&job_request).unwrap(); + debug!( + "(Server ID: {}) sending block {} with id {} to stratum clients", + self.id, job_template.height, job_template.job_id, + ); + self.workers.broadcast(job_request_json); + } + + pub fn run(&self, config: &StratumServerConfig, tx_pool: &ServerTxPool, stop_state: Arc) { + debug!("Run main loop"); + let mut deadline: i64 = 0; + let mut head = self.chain.head().unwrap(); + let mut current_hash = head.prev_block_h; + loop { + // Ping stratum socket on stop to handle TcpListener unbind. + if stop_state.is_stopped() { + let listen_addr: SocketAddr = config + .stratum_server_addr + .clone() + .unwrap() + .parse() + .expect("Stratum: Incorrect address "); + thread::spawn(move || { + let _ = TcpStream::connect(listen_addr).unwrap(); + }); + break; + } + + // get the latest chain state + head = self.chain.head().unwrap(); + let latest_hash = head.last_block_h; + + // Build a new block if there is at least one worker and + // There is a new block on the chain or its time to rebuild + // the current one to include new transactions + if (current_hash != latest_hash || Utc::now().timestamp() >= deadline) + && self.workers.count() > 0 + { + { + debug!("resend updated block"); + let mut state = self.current_state.write(); + let wallet_listener_url = if !config.burn_reward { + Some(config.wallet_listener_url.clone()) + } else { + None + }; + // If this is a new block we will clear the current_block version history + let clear_blocks = current_hash != latest_hash; + + // Build the new block (version) + let (new_block, block_fees) = mine_block::get_block( + &self.chain, + tx_pool, + state.current_key_id.clone(), + wallet_listener_url, + ); + + // scaled difficulty + state.current_difficulty = + (new_block.header.total_difficulty() - head.total_difficulty).to_num(); + + state.current_key_id = block_fees.key_id(); + + current_hash = latest_hash; + // set the minimum acceptable share unscaled difficulty for this block + state.minimum_share_difficulty = config.minimum_share_difficulty; + + // set a new deadline for rebuilding with fresh transactions + deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64; + + // If this is a new block we will clear the current_block version history + if clear_blocks { + state.current_block_versions.clear(); + } + + // Update the mining stats + self.workers.update_block_height(new_block.header.height); + let difficulty = new_block.header.total_difficulty() - head.total_difficulty; + self.workers.update_network_difficulty(difficulty.to_num()); + self.workers.update_network_hashrate(); + + // Add this new block candidate onto our list of block versions for this height + state.current_block_versions.push(new_block); + } + // Send this job to all connected workers + self.broadcast_job(); + } + + // sleep before restarting loop + thread::sleep(Duration::from_millis(5)); + } // Main Loop + } +} + +// ---------------------------------------- +// Worker Factory Thread Function +fn accept_connections(listen_addr: SocketAddr, handler: Arc, stop_state: Arc) { + info!("Start tokio stratum server"); + + let task = async move { + let mut listener = TcpListener::bind(&listen_addr).await.unwrap_or_else(|_| { + panic!("Stratum: Failed to bind to listen address {}", listen_addr) + }); + let state_socket = &stop_state.clone(); + let server = listener + .incoming() + .filter_map(|s| async { s.map_err(|e| error!("accept error = {:?}", e)).ok() }) + .for_each(move |socket| { + let handler = handler.clone(); + async move { + // Stop listener on node server stop. + if state_socket.is_stopped() { + panic_any("Stopped"); + } + // Spawn a task to process the connection + let (tx, mut rx) = mpsc::unbounded(); + + let worker_id = handler.workers.add_worker(tx); + info!("Worker {} connected", worker_id); + + let framed = Framed::new(socket, LinesCodec::new()); + let (mut writer, mut reader) = framed.split(); + + let h = handler.clone(); + let read = async move { + while let Some(line) = reader + .try_next() + .await + .map_err(|e| error!("error reading line: {}", e))? + { + let request = serde_json::from_str(&line) + .map_err(|e| error!("error serializing line: {}", e))?; + let resp = h.handle_rpc_requests(request, worker_id); + h.workers.send_to(worker_id, resp); + } + + Result::<_, ()>::Ok(()) + }; + + let write = async move { + while let Some(line) = rx.next().await { + writer + .send(line) + .await + .map_err(|e| error!("error writing line: {}", e))?; + } + + Result::<_, ()>::Ok(()) + }; + + let task = async move { + pin_mut!(read, write); + futures::future::select(read, write).await; + handler.workers.remove_worker(worker_id); + info!("Worker {} disconnected", worker_id); + }; + tokio::spawn(task); + } + }); + server.await + }; + let mut rt = Runtime::new().unwrap(); + rt.block_on(task); +} + +// ---------------------------------------- +// Worker Object - a connected stratum client - a miner, pool, proxy, etc... + +#[derive(Clone)] +pub struct Worker { + id: usize, + agent: String, + login: Option, + authenticated: bool, + tx: Tx, +} + +impl Worker { + /// Creates a new Stratum Worker. + pub fn new(id: usize, tx: Tx) -> Worker { + Worker { + id: id, + agent: String::from(""), + login: None, + authenticated: false, + tx: tx, + } + } +} // impl Worker + +struct WorkersList { + workers_list: Arc>>, + stratum_stats: Arc>, +} + +impl WorkersList { + pub fn new(stratum_stats: Arc>) -> Self { + WorkersList { + workers_list: Arc::new(RwLock::new(HashMap::new())), + stratum_stats: stratum_stats, + } + } + + pub fn add_worker(&self, tx: Tx) -> usize { + let mut stratum_stats = self.stratum_stats.write(); + let worker_id = stratum_stats.worker_stats.len(); + let worker = Worker::new(worker_id, tx); + let mut workers_list = self.workers_list.write(); + workers_list.insert(worker_id, worker); + + let mut worker_stats = WorkerStats::default(); + worker_stats.is_connected = true; + worker_stats.id = worker_id.to_string(); + worker_stats.pow_difficulty = stratum_stats.minimum_share_difficulty; + stratum_stats.worker_stats.push(worker_stats); + stratum_stats.num_workers = workers_list.len(); + worker_id + } + pub fn remove_worker(&self, worker_id: usize) { + self.update_stats(worker_id, |ws| ws.is_connected = false); + let mut stratum_stats = self.stratum_stats.write(); + let mut workers_list = self.workers_list.write(); + workers_list + .remove(&worker_id) + .expect("Stratum: no such addr in map"); + + stratum_stats.num_workers = workers_list.len(); + } + + pub fn login(&self, worker_id: usize, login: String, agent: String) -> Result<(), RpcError> { + let mut wl = self.workers_list.write(); + let mut worker = wl + .get_mut(&worker_id) + .ok_or_else(RpcError::internal_error)?; + worker.login = Some(login); + // XXX TODO Future - Validate password? + worker.agent = agent; + worker.authenticated = true; + Ok(()) + } + + pub fn get_worker(&self, worker_id: usize) -> Result { + self.workers_list + .read() + .get(&worker_id) + .ok_or_else(|| { + error!("Worker {} not found", worker_id); + RpcError::internal_error() + }) + .map(|w| w.clone()) + } + + pub fn get_stats(&self, worker_id: usize) -> Result { + self.stratum_stats + .read() + .worker_stats + .get(worker_id) + .ok_or_else(RpcError::internal_error) + .map(|ws| ws.clone()) + } + + pub fn last_seen(&self, worker_id: usize) { + //self.stratum_stats.write().worker_stats[worker_id].last_seen = SystemTime::now(); + self.update_stats(worker_id, |ws| ws.last_seen = SystemTime::now()); + } + + pub fn update_stats(&self, worker_id: usize, f: impl FnOnce(&mut WorkerStats) -> ()) { + let mut stratum_stats = self.stratum_stats.write(); + f(&mut stratum_stats.worker_stats[worker_id]); + } + + pub fn send_to(&self, worker_id: usize, msg: String) { + let _ = self + .workers_list + .read() + .get(&worker_id) + .unwrap() + .tx + .unbounded_send(msg); + } + + pub fn broadcast(&self, msg: String) { + for worker in self.workers_list.read().values() { + let _ = worker.tx.unbounded_send(msg.clone()); + } + } + + pub fn count(&self) -> usize { + self.workers_list.read().len() + } + + pub fn update_edge_bits(&self, edge_bits: u16) { + { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.edge_bits = edge_bits; + } + self.update_network_hashrate(); + } + + pub fn update_block_height(&self, height: u64) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.block_height = height; + } + + pub fn update_network_difficulty(&self, difficulty: u64) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.network_difficulty = difficulty; + } + + pub fn update_network_hashrate(&self) { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.network_hashrate = 42.0 + * (stratum_stats.network_difficulty as f64 + / graph_weight(stratum_stats.block_height, stratum_stats.edge_bits as u8) as f64) + / 60.0; + } +} + +// ---------------------------------------- +// Grin Stratum Server + +pub struct StratumServer { + id: String, + config: StratumServerConfig, + chain: Arc, + pub tx_pool: ServerTxPool, + sync_state: Arc, + stratum_stats: Arc>, +} + +impl StratumServer { + /// Creates a new Stratum Server. + pub fn new( + config: StratumServerConfig, + chain: Arc, + tx_pool: ServerTxPool, + stratum_stats: Arc>, + ) -> StratumServer { + StratumServer { + id: String::from("0"), + config, + chain, + tx_pool, + sync_state: Arc::new(SyncState::new()), + stratum_stats: stratum_stats, + } + } + + /// "main()" - Starts the stratum-server. Creates a thread to Listens for + /// a connection, then enters a loop, building a new block on top of the + /// existing chain anytime required and sending that to the connected + /// stratum miner, proxy, or pool, and accepts full solutions to + /// be submitted. + pub fn run_loop(&mut self, proof_size: usize, sync_state: Arc, stop_state: Arc) { + info!( + "(Server ID: {}) Starting stratum server with proof_size = {}", + self.id, proof_size + ); + + self.sync_state = sync_state; + + let listen_addr = self + .config + .stratum_server_addr + .clone() + .unwrap() + .parse() + .expect("Stratum: Incorrect address "); + + let handler = Arc::new(Handler::from_stratum(&self)); + let h = handler.clone(); + + let s_state = stop_state.clone(); + + let _listener_th = thread::spawn(move || { + accept_connections(listen_addr, h, s_state); + }); + + // We have started + { + let mut stratum_stats = self.stratum_stats.write(); + stratum_stats.is_running = true; + stratum_stats.edge_bits = (global::min_edge_bits() + 1) as u16; + stratum_stats.minimum_share_difficulty = self.config.minimum_share_difficulty; + } + + warn!( + "Stratum server started on {}", + self.config.stratum_server_addr.clone().unwrap() + ); + + // Initial Loop. Waiting node complete syncing + while self.sync_state.is_syncing() { + thread::sleep(Duration::from_millis(50)); + } + + handler.run(&self.config, &self.tx_pool, stop_state.clone()); + } // fn run_loop() +} // StratumServer + +// Utility function to parse a JSON RPC parameter object, returning a proper +// error if things go wrong. +fn parse_params(params: Option) -> Result + where + for<'de> T: serde::Deserialize<'de>, +{ + params + .and_then(|v| serde_json::from_value(v).ok()) + .ok_or_else(RpcError::invalid_request) +} \ No newline at end of file