This commit is contained in:
victor 2025-08-27 01:57:30 +02:00
parent e680744944
commit a882d6684e
21 changed files with 1445 additions and 494 deletions

401
Cargo.lock generated
View File

@ -17,6 +17,12 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -136,7 +142,9 @@ version = "0.1.0"
dependencies = [
"chrono",
"clap",
"crossterm 0.29.0",
"hex",
"ratatui",
"serde",
"serde_json",
"sha2",
@ -144,6 +152,7 @@ dependencies = [
"tokio",
"tokio-tungstenite",
"uuid",
"vlogger",
"warp",
"wasm-bindgen",
"web-sys",
@ -161,6 +170,21 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.34"
@ -236,6 +260,29 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -251,6 +298,49 @@ dependencies = [
"libc",
]
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags",
"crossterm_winapi",
"derive_more",
"document-features",
"mio",
"parking_lot",
"rustix 1.0.8",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -261,12 +351,68 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -277,18 +423,49 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -391,6 +568,11 @@ name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "headers"
@ -536,6 +718,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.11.0"
@ -546,6 +734,25 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "instability"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "io-uring"
version = "0.7.10"
@ -563,6 +770,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -585,6 +801,24 @@ version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litrs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
version = "0.4.13"
@ -601,6 +835,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.5"
@ -639,6 +882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@ -696,6 +940,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@ -796,6 +1046,27 @@ dependencies = [
"getrandom",
]
[[package]]
name = "ratatui"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags",
"cassowary",
"compact_str",
"crossterm 0.28.1",
"indoc",
"instability",
"itertools",
"lru",
"paste",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
[[package]]
name = "redox_syscall"
version = "0.5.17"
@ -811,6 +1082,32 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.60.2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@ -907,6 +1204,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.6"
@ -938,12 +1256,40 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.106"
@ -1092,6 +1438,35 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf-8"
version = "0.7.6"
@ -1122,6 +1497,10 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vlogger"
version = "0.1.0"
[[package]]
name = "warp"
version = "0.4.2"
@ -1235,6 +1614,28 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"

View File

@ -17,3 +17,6 @@ uuid = { version = "1.18.0", features = ["v4", "serde"] }
warp = { version = "0.4.2", features = ["server", "websocket"] }
wasm-bindgen = "0.2.100"
web-sys = { version = "0.3.77", features = ["WebSocket"] }
vlogger = { path = "./lib/logger-rs" }
ratatui = "0.29.0"
crossterm = "0.29.0"

1
lib/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
lib/logger-rs/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "vlogger"
version = "0.1.0"

6
lib/logger-rs/Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[package]
name = "vlogger"
version = "0.1.0"
edition = "2024"
[dependencies]

11
lib/logger-rs/README.md Normal file
View File

@ -0,0 +1,11 @@
# Simple Logger
A lightweight logging utility for Rust with both printing and string formatting capabilities.
## Features
- **Two macros**: `log!` for immediate printing, `msg!` for string formatting
- **Color support**: ANSI color codes for terminal output
- **Multiple log levels**: INFO, DEBUG, WARNING, ERROR, FATAL
- **No external dependencies**: Uses only standard library
- **File/line info**: DEBUG, WARNING, ERROR, and FATAL levels include source location

173
lib/logger-rs/src/lib.rs Normal file
View File

@ -0,0 +1,173 @@
use std::time::{SystemTime, UNIX_EPOCH};
pub const INFO: usize = 0;
pub const DEBUG: usize = 1;
pub const WARNING: usize = 2;
pub const ERROR: usize = 3;
pub const FATAL: usize = 4;
pub const LOG_LEVEL: [&str; 5] = [
"INFO",
"DEBUG",
"WARNING",
"ERROR",
"FATAL",
];
pub fn colored(text: &str, color: &str) -> String {
// For terminals: use ANSI escape codes
let ansi_code = match color {
"black" => "30",
"red" => "31",
"green" => "32",
"yellow" => "33",
"blue" => "34",
"purple" => "35",
"cyan" => "36",
"white" => "37",
_ => "0", // default no color
};
format!("\x1b[{ansi_code}m{text}\x1b[0m")
}
pub fn current_timestamp() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let total_seconds = now.as_secs();
let seconds_in_day = total_seconds % 86400; // Seconds within current day
let hours = seconds_in_day / 3600;
let minutes = (seconds_in_day % 3600) / 60;
let seconds = seconds_in_day % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
/// Creates a formatted log message string and returns it
///
/// log_levels are:
/// - INFO
/// - DEBUG
/// - WARNING
/// - ERROR
/// - FATAL
///
/// Returns a formatted string ready for display or further processing.
#[macro_export]
macro_rules! msg {
($level:expr, $($arg:tt)*) => {{
let formatted_msg = format!($($arg)*);
match $level {
$crate::INFO => {
format!(
"[{}] {} | {}\n",
$crate::colored($crate::LOG_LEVEL[$level], "green"),
$crate::current_timestamp(),
formatted_msg
)
},
$crate::DEBUG => {
format!(
"[{}] [{}:{}] {} | {}\n",
$crate::LOG_LEVEL[$level],
file!(),
line!(),
$crate::current_timestamp(),
formatted_msg
)
},
$crate::WARNING => {
format!(
"[{}] {} | {}\n",
$crate::colored($crate::LOG_LEVEL[$level], "yellow"),
$crate::current_timestamp(),
formatted_msg
)
},
$crate::ERROR => {
format!(
"[{}] {} | {}\n",
$crate::colored($crate::LOG_LEVEL[$level], "red"),
$crate::current_timestamp(),
formatted_msg
)
},
$crate::FATAL => {
format!(
"[{}] [{}:{}] {} | {}\n",
$crate::colored($crate::LOG_LEVEL[$level], "red"),
file!(),
line!(),
$crate::current_timestamp(),
formatted_msg
)
},
_ => {
format!(
"[{}] {} {}",
$crate::LOG_LEVEL[$crate::ERROR],
"logging error: log_level value invalid:",
$level
)
}
}
}};
}
/// Creates a formatted log message and prints it to stdout
///
/// log_levels are:
/// - INFO
/// - DEBUG
/// - WARNING
/// - ERROR
/// - FATAL
///
/// FATAL will cause the thread to panic and stop execution.
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
let log_message = $crate::msg!($level, $($arg)*);
println!("{}", log_message);
// Handle FATAL level panic
if $level == $crate::FATAL {
panic!("Fatal error encountered: {}", format!($($arg)*));
}
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_msg_macro() {
let info_msg = msg!(INFO, "Test info message");
assert!(info_msg.contains("INFO"));
assert!(info_msg.contains("Test info message"));
let error_msg = msg!(ERROR, "Test error: {}", 42);
assert!(error_msg.contains("ERROR"));
assert!(error_msg.contains("Test error: 42"));
}
#[test]
fn test_colored() {
let red_text = colored("error", "red");
assert!(red_text.contains("\x1b[31m"));
assert!(red_text.contains("error"));
assert!(red_text.contains("\x1b[0m"));
}
#[test]
fn test_timestamp() {
let ts = current_timestamp();
assert!(ts.contains(":"));
// Should be in HH:MM:SS format
assert_eq!(ts.len(), 8);
}
}

View File

@ -4,51 +4,26 @@ use crate::core;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
#[command(subcommand)]
command: Commands
/// Provide address on which node will listen
#[arg(short = 'a', long)]
pub addr: Option<String>,
/// Provide File with current chain
#[arg(short = 'f', long)]
pub seed_file: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Transaction Options (add, ...)
#[command(short_flag = 't')]
Tx {
#[command(subcommand)]
tx_command: TxCmd
},
/// Show accounts and balances
#[command(short_flag = 'l')]
List,
/// Start as seed node
#[command(short_flag = 's')]
Seed {
#[arg(short = 'a')]
addr: String
},
/// listen on addr
#[command(short_flag = 'r')]
Run {
#[arg(short = 'a')]
addr: String
},
}
#[derive(Subcommand, Debug)]
pub enum TxCmd {
/// Add a new transaction to the DB
#[command(short_flag = 'a')]
Add(core::Tx)
}
impl Args {
pub fn get_commands(&self) -> &Commands {
&self.command
}
/// Add a new transaction to the DB
#[command(short_flag = 'a')]
Add(core::Tx)
}
pub fn get_args() -> Args {
Args::parse()
Args::parse()
}

View File

@ -1,6 +1,6 @@
use crate::core;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Default)]
pub struct BlockHeader {
pub previous_hash: String,
pub timestamp: u64,
@ -9,7 +9,7 @@ pub struct BlockHeader {
pub nonce: u32
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Default)]
pub struct Block {
pub head: BlockHeader,
pub tx: Vec<core::Tx>

View File

@ -1,8 +1,7 @@
use sha2::Digest;
use sha2::Sha256;
use crate::core::block;
use crate::log::*;
use vlogger::*;
use crate::core;
use crate::error::{ BlockchainError, TxError };
@ -18,31 +17,13 @@ pub enum ValidationError {
InvalidPreviousBlockHash
}
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct Genesis {
pub genesis_time: String,
pub chain_id: String,
pub balances: std::collections::HashMap<Account, u32>
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Blockchain {
genesis: Genesis,
balances: std::collections::HashMap<Account, u32>,
blocks: Vec<core::Block>,
tx_mempool: Vec<core::Tx>,
}
impl Genesis {
pub fn new() -> Self {
Self {
genesis_time: String::new(),
chain_id: String::new(),
balances: std::collections::HashMap::new()
}
}
}
#[allow(dead_code)]
impl Blockchain {
pub fn open_account(&mut self, tx: core::Tx) -> Result<(), BlockchainError> {
@ -185,9 +166,8 @@ impl Blockchain {
Ok(())
}
pub fn new(balances: HashMap<Account, u32>, blocks: Vec<core::Block>, tx_mempool: Vec<core::Tx>, genesis: Genesis) -> Blockchain {
pub fn new(balances: HashMap<Account, u32>, blocks: Vec<core::Block>, tx_mempool: Vec<core::Tx>) -> Blockchain {
return Self {
genesis,
balances,
blocks,
tx_mempool
@ -224,10 +204,6 @@ impl Blockchain {
&self.blocks
}
pub fn genesis(&self) -> &Genesis {
&self.genesis
}
pub fn add_block(&mut self, block: core::Block) {
match self.validate_block(&block) {
Ok(()) => self.blocks.push(block),
@ -266,10 +242,9 @@ impl Blockchain {
Ok(())
}
pub fn from_genesis(genesis: Genesis, blocks: Vec<core::Block>) -> Result<Blockchain, ValidationError> {
pub fn build(blocks: Vec<core::Block>) -> Result<Blockchain, ValidationError> {
log!(INFO, "Starting Chain Build from Genesis");
let chain = Blockchain {
genesis,
blocks,
balances: HashMap::new(),
tx_mempool: vec![]

View File

@ -1,115 +0,0 @@
pub const INFO: usize = 0;
pub const DEBUG: usize = 1;
pub const WARNING: usize = 2;
pub const ERROR: usize = 3;
pub const FATAL: usize = 4;
pub const LOG_LEVEL: [&str; 5] = [
"INFO",
"DEBUG",
"WARNING",
"ERROR",
"FATAL",
];
use chrono::Utc;
pub fn colored(text: &str, color: &str) -> String {
// For terminals: use ANSI escape codes
let ansi_code = match color {
"black" => "30",
"red" => "31",
"green" => "32",
"yellow" => "33",
"blue" => "34",
"purple" => "35",
"cyan" => "36",
"white" => "37",
_ => "0", // default no color
};
format!("\x1b[{ansi_code}m{text}\x1b[0m")
}
pub fn current_timestamp() -> String {
Utc::now().format("%Y-%m-%d@%H:%M:%S").to_string()
}
/// You can use this macro to create error messages that will show in the console
/// if run on the client or in the terminal if run on the server
///
/// log_levels are :
/// - INFO
/// - DEBUG
/// - WARNING
/// - ERROR
/// - FATAL
///
/// important: FATAL will cause the thread to panic and stop execution
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
let formatted_msg = format!($($arg)*);
match $level {
$crate::log::INFO => {
println!(
"[{}] {} | {}",
$crate::log::colored($crate::log::LOG_LEVEL[$level], "green"),
$crate::log::current_timestamp(),
formatted_msg
);
},
$crate::log::DEBUG => {
println!(
"[{}] [{}:{}] {} | {}",
$crate::log::LOG_LEVEL[$level],
file!(),
line!(),
$crate::log::current_timestamp(),
formatted_msg
);
},
$crate::log::WARNING => {
println!(
"[{}] [{}:{}] {} | {}",
$crate::log::colored($crate::log::LOG_LEVEL[$level], "yellow"),
file!(),
line!(),
$crate::log::current_timestamp(),
formatted_msg
);
},
$crate::log::ERROR => {
println!(
"[{}] [{}:{}] {} | {}",
$crate::log::colored($crate::log::LOG_LEVEL[$level], "red"),
file!(),
line!(),
$crate::log::current_timestamp(),
formatted_msg
);
},
$crate::log::FATAL => {
println!(
"[{}] [{}:{}] {} | {}",
$crate::log::colored($crate::log::LOG_LEVEL[$level], "red"),
file!(),
line!(),
$crate::log::current_timestamp(),
formatted_msg
);
if $level == $crate::log::FATAL {
panic!("Fatal error encountered: {}", formatted_msg);
}
},
_ => {
println!(
"[{}] {} {}",
$crate::log::LOG_LEVEL[$crate::log::ERROR],
"loggin error: log_level value invalid:",
$level
);
}
}
}};
}

View File

@ -1,15 +1,13 @@
use error::{ BlockchainError, handle_error };
#[macro_use]
pub mod log;
pub mod error;
pub mod args;
pub mod core;
pub mod native_node;
pub mod seeds_constants;
pub mod watcher;
use crate::native_node::node::NativeNode;
use crate::args::{get_args, TxCmd, Commands};
use crate::{args::get_args, watcher::watcher::Watcher};
const SEED_ADDR: &str = "127.0.0.1:8333";
@ -26,24 +24,13 @@ async fn main() {
let args = get_args();
match args.get_commands() {
Commands::Tx{ tx_command } => {
match tx_command {
TxCmd::Add(tx) => {
add_transaction(tx.clone()).unwrap_or_else(|e| handle_error(e))
}
}
},
Commands::List => {
list_accounts()
}
Commands::Seed { addr: _ } => {
NativeNode::seed(SEED_ADDR.to_string()).run_native().await;
}
Commands::Run{ addr } => {
dbg!(&addr);
NativeNode::bootstrap(addr).await.unwrap().run_native().await;
}
let mut watcher = Watcher::build().file(args.seed_file).addr(args.addr).start();
loop {
if !watcher.poll().await.is_ok() {
break ;
}
}
println!("Hello, world!");
println!("Hello, world!");
}

View File

@ -1,5 +1,5 @@
use crate::native_node::{message, node};
use crate::log::*;
use crate::native_node::{node};
use vlogger::*;
use tokio::sync::mpsc;
use crate::core;
use std::io::{self, Write};
@ -22,51 +22,6 @@ impl node::NativeNode {
let args = &parts[1..];
match command {
"id" => {
command_sender.send(node::NodeCommand::DebugShowId).await.unwrap();
},
"tx" => {
if args.len() != 4 {
log!(ERROR, "Invalid arg count! Expected {}, got {}", 4, args.len());
continue;
}
let from = args[0];
let to = args[1];
let value = args[2].parse::<u32>().unwrap();
let data = args[3];
let tx = core::Tx::new(
from.to_string(),
to.to_string(),
value,
data.to_string()
);
let cmd = node::NodeCommand::Transaction { tx };
command_sender.send(cmd).await.unwrap();
},
"block" => {
let cmd = node::NodeCommand::CreateBlock;
command_sender.send(cmd).await.unwrap();
},
"list" => {
if args.len() != 1 {
log!(ERROR, "{command}: Invalid arg! (blocks, peers)");
continue;
}
match args[0] {
"blocks" => command_sender.send(node::NodeCommand::DebugListBlocks).await.unwrap(),
"peers" => command_sender.send(node::NodeCommand::DebugListPeers).await.unwrap(),
_ => log!(ERROR, "Unkown arg: {}", args[0]),
}
},
"dump_blocks" => {
command_sender.send(node::NodeCommand::DebugDumpBlocks).await.unwrap();
},
"connect" => {
command_sender.send(node::NodeCommand::ConnectToSeeds).await.unwrap();
}
_ => {
log!(ERROR, "Unkown command {command}");
continue;

View File

@ -4,7 +4,7 @@ use crate::native_node::node;
use crate::native_node::error;
use crate::core;
use crate::log::*;
use vlogger::*;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub enum ProtocolMessage {
@ -13,7 +13,6 @@ pub enum ProtocolMessage {
version: String
},
BootstrapResponse {
genesis: core::Genesis,
blocks: Vec<core::Block>
},
GetPeersRequest {
@ -78,7 +77,6 @@ impl node::NativeNode {
log!(INFO, "Received BootstrapRequest from {peer_id}");
let peer = &self.tcp_peers[&peer_id];
let resp = ProtocolMessage::BootstrapResponse {
genesis: self.chain.genesis().clone(),
blocks: self.chain.blocks().to_vec()
};
peer.sender.send(resp).await.unwrap();

View File

@ -2,7 +2,7 @@ use crate::native_node::{message, node};
use crate::seeds_constants::SEED_NODES;
use crate::log::*;
use vlogger::*;
use tokio::select;
use tokio::sync::mpsc;

View File

@ -1,275 +1,237 @@
use crate::core::{self, ValidationError};
use crate::core::{self, Blockchain, ValidationError};
use crate::native_node::message::{self, ProtocolMessage};
use crate::seeds_constants::SEED_NODES;
use crate::watcher::executor::ExecutorCommand;
use std::io::{Read, Write};
use std::collections::HashMap;
use crate::log::*;
use vlogger::*;
use tokio::sync::mpsc;
use uuid::Uuid;
pub struct TcpPeer {
pub id: Uuid,
pub addr: String,
pub sender: tokio::sync::mpsc::Sender<ProtocolMessage>
pub id: Uuid,
pub addr: String,
pub sender: tokio::sync::mpsc::Sender<ProtocolMessage>
}
pub struct NativeNode {
pub id: Uuid,
pub addr: String,
pub tcp_peers: HashMap<Uuid, TcpPeer>,
pub ws: Vec<web_sys::WebSocket>,
pub chain: core::Blockchain,
pub db_file: std::fs::File
pub id: Uuid,
pub addr: String,
pub tcp_peers: HashMap<Uuid, TcpPeer>,
pub chain: core::Blockchain,
exec_tx: mpsc::Sender<ExecutorCommand>,
rx: mpsc::Receiver<NodeCommand>,
}
#[derive(Debug)]
pub enum NodeCommand {
AddPeer { peer_id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender<ProtocolMessage> },
RemovePeer { peer_id: Uuid },
ProcessMessage { peer_id: Uuid, message: ProtocolMessage },
Transaction { tx: core::Tx },
CreateBlock,
DebugListBlocks,
DebugListPeers,
DebugShowId,
DebugDumpBlocks,
ConnectToSeeds
AddPeer { peer_id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender<ProtocolMessage> },
RemovePeer { peer_id: Uuid },
ProcessMessage { peer_id: Uuid, message: ProtocolMessage },
Transaction { tx: core::Tx },
CreateBlock,
DebugListBlocks,
DebugListPeers,
DebugShowId,
DebugDumpBlocks,
ConnectToSeeds
}
impl NativeNode {
pub fn peer_addresses(&self) -> Vec<String> {
let mut addr: Vec<String> = self.tcp_peers.iter().map(|p| p.1.addr.to_string()).collect();
addr.push(self.addr.clone());
addr
pub fn peer_addresses(&self) -> Vec<String> {
let mut addr: Vec<String> = self.tcp_peers.iter().map(|p| p.1.addr.to_string()).collect();
addr.push(self.addr.clone());
addr
}
pub fn list_peers(&self) {
println!("Peer List\n-----------");
for (i, p) in self.tcp_peers.iter().enumerate() {
println!("Peer #{i}: {}", p.1.id)
}
}
pub fn list_peers(&self) {
println!("Peer List\n-----------");
for (i, p) in self.tcp_peers.iter().enumerate() {
println!("Peer #{i}: {}", p.1.id)
}
pub fn show_id(&self) {
println!("Node Id: {}", self.id)
}
fn remove_tcp_peer(&mut self, peer_id: Uuid) {
log!(INFO, "Removing Peer {peer_id}");
self.tcp_peers.remove_entry(&peer_id);
}
fn add_tcp_peer(&mut self, id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender<ProtocolMessage>) {
let peer = TcpPeer {
id: id,
addr,
sender
};
log!(INFO, "Adding Peer {}", peer.id);
self.tcp_peers.insert(id, peer);
}
pub fn new_with_id(
id: uuid::Uuid,
exec_tx: mpsc::Sender<ExecutorCommand>,
rx: mpsc::Receiver<NodeCommand>
) -> Self {
Self {
id,
tcp_peers: HashMap::new(),
chain: Default::default(),
addr: String::new(),
exec_tx,
rx,
}
}
pub fn show_id(&self) {
println!("Node Id: {}", self.id)
}
fn remove_tcp_peer(&mut self, peer_id: Uuid) {
log!(INFO, "Removing Peer {peer_id}");
self.tcp_peers.remove_entry(&peer_id);
}
fn add_tcp_peer(&mut self, id: Uuid, addr: String, sender: tokio::sync::mpsc::Sender<ProtocolMessage>) {
let peer = TcpPeer {
id: id,
addr,
sender
};
log!(INFO, "Adding Peer {}", peer.id);
self.tcp_peers.insert(id, peer);
}
fn persist(&mut self) {
for t in self.chain.blocks() {
let json = serde_json::to_string(&t).unwrap();
self.db_file.write(json.as_bytes()).unwrap();
}
}
pub fn new_with_id(id: uuid::Uuid, chain: core::Blockchain, db_file: std::fs::File, addr: String) -> Self {
Self {
id,
tcp_peers: HashMap::new(),
ws: Vec::new(),
chain,
addr,
db_file
}
}
pub fn new(chain: core::Blockchain, db_file: std::fs::File, addr: String) -> Self {
Self {
id: Uuid::new_v4(),
tcp_peers: HashMap::new(),
ws: Vec::new(),
chain,
addr,
db_file
}
}
pub async fn send_handshake(id: uuid::Uuid, stream: &mut tokio::net::TcpStream) -> Result<uuid::Uuid, ValidationError> {
let handshake = ProtocolMessage::Handshake { node_id: id.clone(), version: "".to_string() };
NativeNode::send_message(stream, &handshake).await.unwrap();
if let Ok(response) = NativeNode::receive_message(stream).await {
match response {
message::ProtocolMessage::Handshake { node_id, version: _ } => {
Ok(node_id)
},
_ => {
log!(ERROR, "Invalid response on Handshake");
Err(ValidationError::InvalidBlockHash)
}
}
pub fn new(
addr: Option<String>,
blocks: Option<Vec<core::Block>>,
exec_tx: mpsc::Sender<ExecutorCommand>,
rx: mpsc::Receiver<NodeCommand>,
) -> Self {
Self {
id: Uuid::new_v4(),
tcp_peers: HashMap::new(),
chain: {
if blocks.is_some() {
Blockchain::build(blocks.unwrap()).unwrap_or(Default::default())
} else {
Default::default()
}
},
addr: if addr.is_some() { addr.unwrap() } else { String::new() },
exec_tx,
rx
}
}
pub async fn send_handshake(id: uuid::Uuid, stream: &mut tokio::net::TcpStream) -> Result<uuid::Uuid, ValidationError> {
let handshake = ProtocolMessage::Handshake { node_id: id.clone(), version: "".to_string() };
NativeNode::send_message(stream, &handshake).await.unwrap();
if let Ok(response) = NativeNode::receive_message(stream).await {
match response {
message::ProtocolMessage::Handshake { node_id, version: _ } => {
Ok(node_id)
},
_ => {
log!(ERROR, "Invalid response on Handshake");
Err(ValidationError::InvalidBlockHash)
}
}
} else {
Err(ValidationError::InvalidBlockHash)
}
}
pub async fn bootstrap(&mut self) -> Result<(), ValidationError> {
log!(INFO, "Running As Native Node");
let mut stream = tokio::net::TcpStream::connect(SEED_NODES[0]).await.unwrap();
let id = uuid::Uuid::new_v4();
if let Ok(_) = NativeNode::send_handshake(id, &mut stream).await {
let message = message::ProtocolMessage::BootstrapRequest { node_id: id.clone(), version: "".to_string() };
NativeNode::send_message(&mut stream, &message).await.unwrap();
log!(INFO, "Sent BootstrapRequest to seed");
if let Ok(response) = NativeNode::receive_message(&mut stream).await {
match response {
ProtocolMessage::BootstrapResponse { blocks } => {
log!(INFO, "Received BootstrapResponse from seed");
self.chain = core::Blockchain::build(blocks).unwrap();
Ok(())
},
_ => {
log!(ERROR, "Invalid Response from BootstrapRequest: {:?}", &response);
Err(ValidationError::InvalidBlockHash)
}
}
} else {
Err(ValidationError::InvalidBlockHash)
}
} else {
Err(ValidationError::InvalidBlockHash)
}
}
pub async fn bootstrap(addr: &str) -> Result<Self, ValidationError> {
log!(INFO, "Running As Native Node");
pub async fn broadcast_transaction(&self, tx: &core::Tx) {
for (id, peer) in &self.tcp_peers {
let message = ProtocolMessage::Transaction{peer_id: self.id, tx: tx.clone()};
peer.sender.send(message).await.unwrap();
log!(DEBUG, "Send Transaction message to {id}");
}
}
let mut stream = tokio::net::TcpStream::connect(SEED_NODES[0]).await.unwrap();
pub async fn broadcast_block(&self, block: &core::Block) {
for (id, peer) in &self.tcp_peers {
let message = ProtocolMessage::Block{
peer_id: self.id,
height: self.chain.blocks().len() as u64,
block: block.clone()
};
peer.sender.send(message).await.unwrap();
log!(DEBUG, "Send Block message to {id}");
}
}
let id = uuid::Uuid::new_v4();
pub async fn run_native(&mut self) {
let tcp_listner = tokio::net::TcpListener::bind(&self.addr).await.unwrap();
if let Ok(_) = NativeNode::send_handshake(id, &mut stream).await {
let message = message::ProtocolMessage::BootstrapRequest { node_id: id.clone(), version: "".to_string() };
NativeNode::send_message(&mut stream, &message).await.unwrap();
log!(INFO, "Sent BootstrapRequest to seed");
if let Ok(response) = NativeNode::receive_message(&mut stream).await {
match response {
ProtocolMessage::BootstrapResponse { genesis, blocks } => {
log!(INFO, "Received BootstrapResponse from seed");
let chain = core::Blockchain::from_genesis(genesis, blocks)?;
let node = Self::new_with_id(id, chain, std::fs::File::open("./database/tx.db").unwrap(), addr.to_string());
Ok(node)
},
_ => {
log!(ERROR, "Invalid Response from BootstrapRequest: {:?}", &response);
Err(ValidationError::InvalidBlockHash)
}
}
} else {
Err(ValidationError::InvalidBlockHash)
}
} else {
Err(ValidationError::InvalidBlockHash)
let (channel_write, mut channel_read) = mpsc::channel::<NodeCommand>(100);
let id = self.id.clone();
tokio::spawn({
let c = channel_write.clone();
async move {
NativeNode::accept_connections(tcp_listner, c, id).await;
}});
while let Some(command) = channel_read.recv().await {
match command {
NodeCommand::ConnectToSeeds => {
self.connect_to_seeds(channel_write.clone()).await;
},
NodeCommand::AddPeer { peer_id, addr, sender } => {
self.add_tcp_peer(peer_id, addr, sender);
},
NodeCommand::RemovePeer { peer_id } => {
self.remove_tcp_peer(peer_id);
}
}
pub fn seed(addr: String) -> Self {
log!(INFO, "Running As Seed Node");
let cwd = std::env::current_dir().unwrap();
let mut genpath = std::path::PathBuf::from(&cwd);
genpath.push("database");
genpath.push("genesis.json");
let mut gen_file = std::fs::File::open(genpath).unwrap();
let mut buf = String::new();
gen_file.read_to_string(&mut buf).unwrap();
let mut db_file: std::fs::File = {
let mut db_path = std::path::PathBuf::from(&cwd);
db_path.push("database");
db_path.push("tx.db");
std::fs::OpenOptions::new().read(true).write(true).create(true).open(&db_path).unwrap()
};
let genesis = serde_json::from_str::<core::Genesis>(&buf).unwrap();
buf.clear();
db_file.read_to_string(&mut buf).unwrap();
let buf = buf.trim();
log!(DEBUG, "Buf content: {:#?}", buf);
let blocks = if !buf.is_empty() {
serde_json::from_str::<Vec<core::Block>>(&buf).unwrap()
} else {
vec![]
};
let chain = core::Blockchain::from_genesis(genesis, blocks).unwrap();
Self::new(chain, db_file, addr)
}
pub async fn broadcast_transaction(&self, tx: &core::Tx) {
for (id, peer) in &self.tcp_peers {
let message = ProtocolMessage::Transaction{peer_id: self.id, tx: tx.clone()};
peer.sender.send(message).await.unwrap();
log!(DEBUG, "Send Transaction message to {id}");
}
}
pub async fn broadcast_block(&self, block: &core::Block) {
for (id, peer) in &self.tcp_peers {
let message = ProtocolMessage::Block{
peer_id: self.id,
height: self.chain.blocks().len() as u64,
block: block.clone()
};
peer.sender.send(message).await.unwrap();
log!(DEBUG, "Send Block message to {id}");
}
}
pub async fn run_native(&mut self) {
let tcp_listner = tokio::net::TcpListener::bind(&self.addr).await.unwrap();
let (channel_write, mut channel_read) = mpsc::channel::<NodeCommand>(100);
let id = self.id.clone();
tokio::spawn({
let c = channel_write.clone();
async move {
NativeNode::accept_connections(tcp_listner, c, id).await;
}});
tokio::spawn({
let c = channel_write.clone();
async move {
NativeNode::cli(c).await;
}
});
while let Some(command) = channel_read.recv().await {
match command {
NodeCommand::ConnectToSeeds => {
self.connect_to_seeds(channel_write.clone()).await;
},
NodeCommand::AddPeer { peer_id, addr, sender } => {
self.add_tcp_peer(peer_id, addr, sender);
},
NodeCommand::RemovePeer { peer_id } => {
self.remove_tcp_peer(peer_id);
}
NodeCommand::ProcessMessage { peer_id, message } => {
self.process_message(peer_id, &message).await;
},
NodeCommand::Transaction { tx } => {
self.chain.apply(&tx).unwrap();
self.broadcast_transaction(&tx).await;
},
NodeCommand::CreateBlock => {
log!(INFO, "Received CreateBlock Command");
let block = self.chain.create_block();
self.broadcast_block(&block).await;
},
NodeCommand::DebugListBlocks => {
log!(INFO, "Received DebugListBlocks command");
self.chain.print_blocks();
},
NodeCommand::DebugListPeers => {
log!(INFO, "Received DebugListPeers command");
self.list_peers();
},
NodeCommand::DebugShowId => {
log!(INFO, "Received DebugListBlocks command");
self.show_id();
},
NodeCommand::DebugDumpBlocks => {
self.chain.dump_blocks(&mut self.db_file);
}
}
NodeCommand::ProcessMessage { peer_id, message } => {
self.process_message(peer_id, &message).await;
},
NodeCommand::Transaction { tx } => {
self.chain.apply(&tx).unwrap();
self.broadcast_transaction(&tx).await;
},
NodeCommand::CreateBlock => {
log!(INFO, "Received CreateBlock Command");
let block = self.chain.create_block();
self.broadcast_block(&block).await;
},
NodeCommand::DebugListBlocks => {
log!(INFO, "Received DebugListBlocks command");
self.chain.print_blocks();
},
NodeCommand::DebugListPeers => {
log!(INFO, "Received DebugListPeers command");
self.list_peers();
},
NodeCommand::DebugShowId => {
log!(INFO, "Received DebugListBlocks command");
self.show_id();
},
NodeCommand::DebugDumpBlocks => {
// self.chain.dump_blocks(&mut self.db_file);
}
}
}
}
}

8
src/watcher.rs Normal file
View File

@ -0,0 +1,8 @@
pub mod executor;
pub mod parser;
pub mod renderer;
pub mod watcher;
pub use executor::*;
pub use parser::*;
pub use renderer::*;

109
src/watcher/executor.rs Normal file
View File

@ -0,0 +1,109 @@
use crate::{native_node::node::NodeCommand, watcher::renderer::*};
use tokio::sync::mpsc;
pub enum ExecutorCommand {
NodeResponse(String),
Echo(Vec<String>),
InvalidCommand(String),
Node(NodeCommand),
Clear(RenderPane),
Exit
}
pub struct Executor {
render_tx: mpsc::Sender<RenderCommand>,
node_tx: mpsc::Sender<NodeCommand>,
rx: mpsc::Receiver<ExecutorCommand>,
exit: bool
}
impl Executor {
pub fn new(render_tx: mpsc::Sender<RenderCommand>, node_tx: mpsc::Sender<NodeCommand>, rx: mpsc::Receiver<ExecutorCommand>) -> Self {
Self {
render_tx,
node_tx,
rx,
exit: false
}
}
fn exit(&mut self) {
self.exit = true
}
async fn listen(&mut self) {
if let Some(cmd) = self.rx.recv().await {
let _ = self.execute(cmd);
}
}
async fn send_node_cmd(&self, cmd: NodeCommand) {
self.node_tx.send(cmd).await.unwrap()
}
async fn handle_node_cmd(&self, cmd: NodeCommand) {
self.send_node_cmd(cmd).await;
// match cmd {
// NodeCommand::AddPeer { peer_id, addr, sender } => {},
// NodeCommand::RemovePeer { peer_id } => {},
// NodeCommand::ProcessMessage { peer_id, message } => {},
// NodeCommand::Transaction { tx } => {},
// NodeCommand::CreateBlock => {},
// NodeCommand::DebugListBlocks => {},
// NodeCommand::DebugListPeers => {},
// NodeCommand::DebugShowId => {},
// NodeCommand::DebugDumpBlocks => {},
// NodeCommand::ConnectToSeeds => {}
// }
}
fn render_string(&self, str: String) {
let rd_cmd = RenderCommand::RenderStringToPane{
str,
pane: RenderPane::CliOutput
};
let _ = self.render_tx.send(rd_cmd);
}
fn echo(&self, s: Vec<String>) {
let mut str = s.join(" ");
str.push_str("\n");
let rd_cmd = RenderCommand::RenderStringToPane{
str,
pane: RenderPane::CliOutput
};
let _ = self.render_tx.send(rd_cmd);
}
fn clear(&self, p: RenderPane) {
let rd_cmd = RenderCommand::ClearPane(p);
let _ = self.render_tx.send(rd_cmd);
}
fn invalid_command(&self, str: String) {
let rd_cmd = RenderCommand::RenderStringToPane{
str,
pane: RenderPane::CliOutput
};
let _ = self.render_tx.send(rd_cmd);
}
async fn execute(&mut self, cmd: ExecutorCommand) {
match cmd {
ExecutorCommand::NodeResponse(resp) => self.render_string(resp),
ExecutorCommand::Node(n) => self.handle_node_cmd(n).await,
ExecutorCommand::Clear(p) => self.clear(p),
ExecutorCommand::Echo(s) => self.echo(s),
ExecutorCommand::InvalidCommand(str) => self.invalid_command(str),
ExecutorCommand::Exit => self.exit(),
}
}
pub async fn run(&mut self) {
while !self.exit {
self.listen().await;
}
}
}

144
src/watcher/parser.rs Normal file
View File

@ -0,0 +1,144 @@
use crate::native_node::node::NodeCommand;
use crate::watcher::executor::{ExecutorCommand};
use vlogger::*;
use crate::core;
use tokio::time::{timeout, Duration};
use tokio::sync::mpsc;
use crate::watcher::renderer::RenderPane;
#[derive(Debug)]
pub struct Parser {
rx: mpsc::Receiver<ParserCommand>,
exec_tx: mpsc::Sender<ExecutorCommand>,
exit: bool
}
pub enum ParserCommand {
ParseCmdString(String),
Exit
}
const CMD_ECHO: &str = "echo";
const CMD_CLEAR: &str = "clear";
const CMD_NODE: &str = "node";
impl Parser {
pub fn new(
rx: mpsc::Receiver<ParserCommand>,
exec_tx: mpsc::Sender<ExecutorCommand>
) -> Self {
Self {
rx,
exec_tx,
exit: false
}
}
fn exit(&mut self) {
self.exit = true;
}
pub async fn run(&mut self) {
while !self.exit {
self.listen().await;
}
}
async fn listen(&mut self) {
if let Ok(Some(mes)) = timeout(Duration::from_millis(400), self.rx.recv()).await {
match mes {
ParserCommand::ParseCmdString(s) => {
let s_split: Vec<&str> = s.split(" ").collect();
if s_split.len() != 0 {
let cmd = &s_split[0];
let args = &s_split[1..];
let exec_cmd = match *cmd {
CMD_NODE => {
if !args.is_empty() {
match args[0] {
"id" => {
ExecutorCommand::Node(NodeCommand::DebugShowId)
},
"tx" => {
if args.len() != 4 {
log!(ERROR, "Invalid arg count! Expected {}, got {}", 4, args.len());
}
let from = args[0];
let to = args[1];
let value = args[2].parse::<u32>().unwrap();
let data = args[3];
let tx = core::Tx::new(
from.to_string(),
to.to_string(),
value,
data.to_string()
);
ExecutorCommand::Node(NodeCommand::Transaction { tx })
},
"block" => {
ExecutorCommand::Node(NodeCommand::CreateBlock)
},
"list" => {
if args.len() != 1 {
log!(ERROR, "{cmd}: Invalid arg! (blocks, peers)");
}
match args[0] {
"blocks" => ExecutorCommand::Node(NodeCommand::DebugListBlocks),
"peers" => ExecutorCommand::Node(NodeCommand::DebugListPeers),
_ => ExecutorCommand::InvalidCommand(msg!(ERROR, "Unkown arg: {}", args[0])),
}
},
"dump_blocks" => {
ExecutorCommand::Node(NodeCommand::DebugDumpBlocks)
},
"connect" => {
ExecutorCommand::Node(NodeCommand::ConnectToSeeds)
}
_ => {
ExecutorCommand::InvalidCommand(msg!(ERROR, "node: unknown argument {}", args[0]))
}
}
} else {
ExecutorCommand::InvalidCommand(msg!(ERROR, "node: expected arguments"))
}
}
CMD_ECHO => {
if args.is_empty() {
ExecutorCommand::InvalidCommand(msg!(ERROR, "print expects args"))
} else {
ExecutorCommand::Echo(args.iter().map(|a| a.to_string()).collect())
}
}
CMD_CLEAR => {
if args.is_empty() {
ExecutorCommand::Clear(RenderPane::All)
} else if args[0] == "in" {
ExecutorCommand::Clear(RenderPane::CliInput)
} else if args[0] == "out" {
ExecutorCommand::Clear(RenderPane::CliOutput)
} else {
ExecutorCommand::InvalidCommand(msg!(ERROR, "clear: Unknown arg {}", args[0]))
}
}
_ => {
ExecutorCommand::InvalidCommand(msg!(ERROR, "Unknown Command {cmd}"))
}
};
let _ = self.exec_tx.send(exec_cmd);
}
}
ParserCommand::Exit => {
self.exit();
}
}
}
}
}

210
src/watcher/renderer.rs Normal file
View File

@ -0,0 +1,210 @@
use crossterm::event::KeyCode;
use ratatui::prelude::*;
use ratatui::widgets::Wrap;
use ratatui::{
buffer::Buffer,
layout::Rect,
symbols::border,
widgets::{Block, Paragraph, Widget},
DefaultTerminal, Frame,
};
use tokio::sync::mpsc;
use tokio::time::{timeout, Duration};
use std::io;
#[derive(Debug)]
pub struct Renderer {
buffer: String,
exit: bool,
rx: mpsc::Receiver<RenderCommand>,
layout: RenderLayout
}
#[derive(Debug, PartialEq)]
pub struct Pane {
title: Option<String>,
target: RenderPane,
layout_index: u8,
buffer: String,
}
#[derive(Debug, PartialEq)]
pub enum RenderPane {
All,
CliInput,
CliOutput
}
pub enum RenderCommand {
RenderStringToPane{
str: String,
pane: RenderPane
},
RenderInput(KeyCode),
ChangeLayout(RenderLayoutKind),
ClearPane(RenderPane),
Exit,
}
#[derive(Debug, Clone)]
pub enum RenderLayoutKind {
Cli,
}
#[derive(Debug)]
pub struct RenderLayout {
kind: RenderLayoutKind,
panes: Vec<Pane>,
}
impl RenderLayoutKind {
pub fn rects(&self, area: Rect) -> std::rc::Rc<[Rect]> {
match self {
Self::Cli => {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Percentage(30),
Constraint::Percentage(70)
])
.split(area)
}
}
}
pub fn generate(&self) -> RenderLayout {
RenderLayout {
kind: self.clone(),
panes: vec![
Pane {
title: Some(" Input Pane ".to_string()),
target: RenderPane::CliInput,
layout_index: 0,
buffer: String::with_capacity(CLI_INPUT_BUFFE_SIZE) + "> ",
},
Pane {
title: Some(" Output Pane ".to_string()),
target: RenderPane::CliOutput,
layout_index: 1,
buffer: String::with_capacity(CLI_OUTPUT_BUFFE_SIZE),
}
]
}
}
}
const CLI_INPUT_BUFFE_SIZE: usize = 4096;
const CLI_OUTPUT_BUFFE_SIZE: usize = 4096;
#[allow(dead_code)]
impl Renderer {
pub fn new(rx: mpsc::Receiver<RenderCommand>, layout: RenderLayoutKind) -> Self {
Self {
buffer: String::new(),
rx,
exit: false,
layout: layout.generate()
}
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.listen().await?
}
Ok(())
}
pub fn draw(&self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
}
fn exit(&mut self) {
self.exit = true;
}
fn buffer_extend<S: AsRef<str>>(&mut self, input: S) {
self.buffer.push_str(input.as_ref());
}
fn input_pane(&mut self) -> Option<&mut Pane> {
self.layout.panes.iter_mut().find(|p| p.target == RenderPane::CliInput)
}
async fn listen(&mut self) -> io::Result<()> {
if let Ok(Some(mes)) = timeout(Duration::from_millis(400), self.rx.recv()).await {
match mes {
RenderCommand::RenderInput(k) => {
if let Some(p) = self.input_pane() {
match k {
KeyCode::Char(c) => {
p.buffer.push(c);
}
KeyCode::Backspace => {
if !p.buffer.ends_with("> ") {
p.buffer.pop();
}
}
KeyCode::Enter => {
p.buffer.push_str("\n> ");
}
_ => {}
}
}
},
RenderCommand::RenderStringToPane{ str, pane } => {
if let Some(p) = self.layout.panes.iter_mut().find(|p| p.target == pane) {
p.buffer.push_str(&str);
}
}
RenderCommand::Exit => {
self.exit();
}
RenderCommand::ChangeLayout(l) => {
match l {
RenderLayoutKind::Cli => {
self.layout = l.generate();
}
}
}
RenderCommand::ClearPane(pane) => {
if matches!(pane, RenderPane::All) {
for p in self.layout.panes.iter_mut() {
p.buffer.clear();
}
} else if let Some(p) = self.layout.panes.iter_mut().find(|p| p.target == pane) {
p.buffer.clear();
}
}
}
}
Ok(())
}
}
impl Widget for &Renderer {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = self.layout.kind.rects(area);
for p in self.layout.panes.iter() {
let block = Block::bordered()
.title({
if let Some(t) = &p.title {
t.clone()
} else {
Default::default()
}
})
.border_set(border::THICK);
Paragraph::new(p.buffer.clone())
.wrap(Wrap::default())
.left_aligned()
.block(block)
.render(layout[p.layout_index as usize], buf);
}
}
}

141
src/watcher/watcher.rs Normal file
View File

@ -0,0 +1,141 @@
use crate::watcher::*;
use crossterm::{event::{self, Event, KeyCode, KeyEventKind}};
use tokio::sync::mpsc;
use std::io::{self};
use crate::{native_node::node::{NativeNode, NodeCommand}};
pub struct Watcher {
render_tx: mpsc::Sender<RenderCommand>,
parser_tx: mpsc::Sender<ParserCommand>,
node_tx: mpsc::Sender<NodeCommand>,
exec_tx: mpsc::Sender<ExecutorCommand>,
cmd_buffer: String,
handles: Vec<tokio::task::JoinHandle<()>>
}
#[derive(Default)]
pub struct WatcherBuilder {
addr: Option<String>,
seed_file: Option<String>
}
impl Watcher {
pub fn build() -> WatcherBuilder {
WatcherBuilder::new()
}
pub async fn exit(self) {
ratatui::restore();
// for (i, handle) in self.handles.into_iter().enumerate() {
// let _ = handle.await;
// println!("Joined thread #{i}")
// }
}
pub async fn poll(&mut self) -> io::Result<bool> {
match event::read()? {
Event::Key(k) if k.kind == KeyEventKind::Press => {
match k.code {
KeyCode::Char(c) => {
self.cmd_buffer.push(c);
let message = RenderCommand::RenderInput(k.code);
let _ = self.render_tx.send(message);
}
KeyCode::Backspace => {
self.cmd_buffer.pop();
let message = RenderCommand::RenderInput(k.code);
let _ = self.render_tx.send(message);
},
KeyCode::Enter => {
let rd_mes = RenderCommand::RenderInput(k.code);
let pr_mes = ParserCommand::ParseCmdString(self.cmd_buffer.clone());
let _ = self.render_tx.send(rd_mes);
let _ = self.parser_tx.send(pr_mes);
self.cmd_buffer.clear();
}
KeyCode::Esc => {
let rd_mes = RenderCommand::Exit;
let pr_mes = ParserCommand::Exit;
let exec_mes = ExecutorCommand::Exit;
let _ = self.render_tx.send(rd_mes);
let _ = self.parser_tx.send(pr_mes);
let _ = self.exec_tx.send(exec_mes);
return Ok(false);
}
_ => {}
};
}
_ => {}
}
Ok(true)
}
}
impl WatcherBuilder {
fn new() -> Self {
Self::default()
}
pub fn addr(mut self, addr: Option<String>) -> Self {
self.addr = addr;
self
}
pub fn file(mut self, seed_file: Option<String>) -> Self {
self.seed_file = seed_file;
self
}
pub fn start(self) -> Watcher {
let (render_tx, render_rx) = mpsc::channel::<RenderCommand>(100);
let (parser_tx, parser_rx) = mpsc::channel::<ParserCommand>(100);
let (exec_tx, exec_rx) = mpsc::channel::<ExecutorCommand>(100);
let (node_tx, node_rx) = mpsc::channel::<NodeCommand>(100);
let mut terminal = ratatui::init();
let render_handle = tokio::spawn({
async move {
let _ = Renderer::new(render_rx, RenderLayoutKind::Cli).run(&mut terminal).await;
}
});
let parser_handle = tokio::spawn({
let exec_tx = exec_tx.clone();
async move {
let _ = Parser::new(parser_rx, exec_tx).run().await;
}
});
let executor_handle = tokio::spawn({
let rend_tx = render_tx.clone();
let node_tx = node_tx.clone();
async move {
let _ = Executor::new(rend_tx, node_tx, exec_rx).run().await;
}
});
let blocks = self.seed_file
.as_ref()
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|content| serde_json::from_str(&content).ok());
let node_handle = tokio::spawn({
let exec_tx = exec_tx.clone();
async move {
let _ = NativeNode::new(self.addr.clone(), blocks, exec_tx, node_rx);
}
});
Watcher {
render_tx,
node_tx,
parser_tx,
exec_tx,
cmd_buffer: String::new(),
handles: vec![parser_handle, render_handle, executor_handle, node_handle]
}
}
}