node + config + ui: fork stratum server to handle state on node start/stop, complete stratum configuration, complete node server config, settings ui refactoring, update i18n lib

This commit is contained in:
ardocrat 2023-07-03 21:17:49 +03:00
parent ffbe772c27
commit 3b2d3ab202
30 changed files with 2534 additions and 753 deletions

187
Cargo.lock generated
View file

@ -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",
]

View file

@ -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" }

View file

@ -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

View file

@ -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: Сохранить

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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();

View file

@ -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<Box<dyn Screen>>,
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()

View file

@ -23,10 +23,3 @@ 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;

View file

@ -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)
}

View file

@ -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<dyn NetworkTab>,
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();
});
}
}
pub use container::*;

View file

@ -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<dyn NetworkTab>,
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();
});
}
}

View file

@ -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)
);
}
});
}
return;
}

View file

@ -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,65 +38,43 @@ 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 {
// Show message to enable node when it's not running.
if !Node::is_running() {
Network::disabled_node_ui(ui);
} else {
NetworkContainer::disabled_node_ui(ui);
return;
}
// 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);
if !Node::is_stopping() {
ui.add_space(18.0);
ui.label(RichText::new(t!("network_mining.loading"))
.size(16.0)
.color(Colors::INACTIVE_TEXT)
);
}
});
}
return;
}
let stratum_stats = &server_stats.as_ref().unwrap().stratum_stats;
let stratum_stats = Node::get_stratum_stats();
// Show stratum server setup when mining server is not running.
if !stratum_stats.is_running && !Node::is_stratum_server_starting() {
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_source("stratum_server_setup")
.id_source("stratum_setup_scroll")
.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() {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
return;
}
@ -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);
},
_ => {}
}

View file

@ -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() {
// Show message to enable node when it's not running.
if !Node::is_running() {
Network::disabled_node_ui(ui);
} else {
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;
}

View file

@ -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<IpAddr> {
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 {

View file

@ -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;

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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(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)

View file

@ -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::<u32>().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::<u32>() {
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::<u64>().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::<u64>() {
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);
});
}
}

View file

@ -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);
});
});
}
}

View file

@ -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<dyn Fn()>,
}
impl<'action> TitlePanelAction<'action> {
pub fn new(icon: &'action str, on_click: fn()) -> Option<Self> {
impl TitlePanelAction {
pub fn new(icon: &'static str, on_click: fn()) -> Option<Self> {
Option::from(Self { icon: Box::new(icon), on_click: Box::new(on_click) })
}
}

View file

@ -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))

View file

@ -100,6 +100,19 @@ impl NodeConfig {
api_secret_path
}
/// List of available IP addresses.
pub fn get_ip_addrs() -> Vec<IpAddr> {
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::<u16>() {
@ -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()
@ -217,12 +289,12 @@ impl NodeConfig {
} 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();

298
src/node/mine_block.rs Normal file
View file

@ -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<Identifier>,
}
impl BlockFees {
/// return key id
pub fn key_id(&self) -> Option<Identifier> {
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<Identifier>,
}
// 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<grin_chain::Chain>,
tx_pool: &ServerTxPool,
key_id: Option<Identifier>,
wallet_listener_url: Option<String>,
) -> (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<grin_chain::Chain>,
tx_pool: &ServerTxPool,
key_id: Option<Identifier>,
wallet_listener_url: Option<String>,
) -> 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::<Utc>::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<String>,
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<CbData, Error> {
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::<CbData>(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)
}

View file

@ -16,4 +16,8 @@ mod node;
pub use node::Node;
mod config;
mod stratum;
mod mine_block;
pub use config::NodeConfig;

View file

@ -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<RwLock<Option<ServerStats>>>,
/// Stratum server statistics.
stratum_stats: Arc<grin_util::RwLock<StratumStats>>,
/// Running API server address.
api_addr: Arc<RwLock<Option<String>>>,
/// 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<Error>
}
@ -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<SyncStatus> {
// 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,63 +192,51 @@ 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);
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);
*w_stats = Some(stats.clone());
}
if first_start {
@ -247,20 +244,33 @@ impl Node {
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<Server, Error> {
/// Start the node [`Server`].
fn start_node_server() -> Result<Server, Error> {
// Get current global config
let config = NodeConfig::get_members();
let server_config = config.server.clone();
@ -545,10 +561,34 @@ fn start_server() -> Result<Server, Error> {
*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)]

934
src/node/stratum.rs Normal file
View file

@ -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<String>;
// ----------------------------------------
// 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<Value>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct RpcResponse {
id: JsonId,
jsonrpc: String,
method: String,
result: Option<Value>,
error: Option<Value>,
}
#[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<RpcError> for Value {
fn from(e: RpcError) -> Self {
serde_json::to_value(e).unwrap()
}
}
impl<T> From<T> 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<u64>,
}
#[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<Block>,
// 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<grin_keychain::Identifier>,
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: <u64>::max_value(),
minimum_share_difficulty: minimum_share_difficulty,
}
}
}
struct Handler {
id: String,
workers: Arc<WorkersList>,
sync_state: Arc<SyncState>,
chain: Arc<grin_chain::Chain>,
current_state: Arc<RwLock<State>>,
}
impl Handler {
pub fn new(
id: String,
stratum_stats: Arc<RwLock<StratumStats>>,
sync_state: Arc<SyncState>,
minimum_share_difficulty: u64,
chain: Arc<grin_chain::Chain>,
) -> 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<Value>, worker_id: usize) -> Result<Value, RpcError> {
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<Value, RpcError> {
return Ok("ok".into());
}
fn handle_status(&self, worker_id: usize) -> Result<Value, RpcError> {
// 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<Value, RpcError> {
// 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<Value>,
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<StopState>) {
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<Handler>, stop_state: Arc<StopState>) {
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<String>,
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<RwLock<HashMap<usize, Worker>>>,
stratum_stats: Arc<RwLock<StratumStats>>,
}
impl WorkersList {
pub fn new(stratum_stats: Arc<RwLock<StratumStats>>) -> 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<Worker, RpcError> {
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<WorkerStats, RpcError> {
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<grin_chain::Chain>,
pub tx_pool: ServerTxPool,
sync_state: Arc<SyncState>,
stratum_stats: Arc<RwLock<StratumStats>>,
}
impl StratumServer {
/// Creates a new Stratum Server.
pub fn new(
config: StratumServerConfig,
chain: Arc<grin_chain::Chain>,
tx_pool: ServerTxPool,
stratum_stats: Arc<RwLock<StratumStats>>,
) -> 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<SyncState>, stop_state: Arc<StopState>) {
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<T>(params: Option<Value>) -> Result<T, RpcError>
where
for<'de> T: serde::Deserialize<'de>,
{
params
.and_then(|v| serde_json::from_value(v).ok())
.ok_or_else(RpcError::invalid_request)
}