commit d6a33a7fcb9c4611f6473bc0934cdb6b9944d88b Author: SinuS Von SifriduS Date: Fri Apr 17 18:55:25 2026 +0200 0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29202a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +Cargo.lock +/target/ + +# Logs +logs +*.log +npm-debug.log* + +# mails +*.eml +# Node +node_modules +package-lock.json +dist + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.settings +.project + +# PID +*.pid + +# var folder +var/ +# env +.env +!.env.dev +config.json + +# sqlite +*.db +*.db-shm +*.db-wal diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ae93392 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,47 @@ +# file: Cargo.toml + +[workspace] +resolver = "3" +members = [ + "khbb_lib", + "khbb_listener_app", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT" +repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot" +authors = ["SinuS von SifriduS "] +publish = false + +[workspace.dependencies] +async-trait = { version = "^0.1", features = [] } +base64 = { version = "^0.22", features = [] } +chrono = { version = "^0.4", features = ["serde"] } +futures-util = { version = "^0.3", features = [] } +reqwest = { version = "^0.13", default-features = false, features = ["charset", "cookies", "deflate", "form", "gzip", "http2", "json", "multipart", "query", "rustls", "socks", "stream", "zstd"] } +rustls = { version = "^0.23", features = ["aws-lc-rs"] } +serde = { version = "^1.0", features = ["derive"] } +serde_json = { version = "^1.0", features = [] } +solana-account-decoder-client-types = { version = "4.0.0-beta.7", features = ["zstd"] } +solana-address-lookup-table-interface = { version = "^3.0", features = ["bincode", "serde"] } +solana-client = { version = "^3.1", features = [] } +solana-compute-budget-interface = { version = "^3.0", features = ["borsh", "serde"] } +solana-rpc-client-api = { version = "4.0.0-beta.7", features = [] } +solana-sdk = { version = "^4.0", features = ["full"] } +solana-sdk-ids = { version = "^3.1", features = [] } +solana-system-interface = { version = "^3.0", features = ["alloc", "bincode", "serde", "std"] } +solana-transaction-status-client-types = { version = "4.0.0-beta.7", features = [] } +spl-associated-token-account-interface = { version = "^2.0", features = ["borsh"] } +spl-memo-interface = { version = "^2.0", features = [] } +spl-token-interface = { version = "^2.0", features = [] } +spl-token-2022-interface = { version = "^2.1", features = [] } +sqlx = { version = "^0.8", features = ["chrono", "uuid", "bigdecimal", "json", "sqlite", "runtime-tokio-rustls"] } +tokio = { version = "^1.52", features = ["full"] } +tokio-stream = { version = "^0.1", features = ["full"] } +tokio-tungstenite = { version = "^0.29", default-features = false, features = ["connect", "handshake", "rustls-tls-webpki-roots", "stream", "url"] } +tracing = { version = "^0.1", features = [] } +tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] } +yellowstone-grpc-client = { version = "^13.0", features = [] } +yellowstone-grpc-proto = { version = "^12.2", features = [] } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..049eeca --- /dev/null +++ b/clippy.toml @@ -0,0 +1,34 @@ +# file: clippy.toml + +msrv = "1.85.0" + +# The project favors explicit control flow and visible intent. +# These settings complement the coding rules already enforced manually +# in code review: no `?`, no `unwrap`, no `expect`, explicit error paths. + +too-many-arguments-threshold = 8 +type-complexity-threshold = 250 +single-char-binding-names-threshold = 3 +trivial-copy-size-limit = 16 +pass-by-value-size-limit = 256 +stack-size-threshold = 512000 +vec-box-size-threshold = 4096 +max-fn-params-bools = 2 +max-include-file-size = 1048576 +cognitive-complexity-threshold = 25 +too-large-for-stack = 2048 +enum-variant-size-threshold = 200 +large-error-threshold = 128 +avoid-breaking-exported-api = true +disallowed-macros = [] +disallowed-methods = [] +disallowed-names = ["foo", "bar", "baz", "tmp"] +disallowed-types = [] +allowed-idents-below-min-chars = [ + "id", + "tx", + "rx", + "ms", + "pcm", + "vad", +] diff --git a/khbb_lib/Cargo.toml b/khbb_lib/Cargo.toml new file mode 100644 index 0000000..5a8b868 --- /dev/null +++ b/khbb_lib/Cargo.toml @@ -0,0 +1,40 @@ +# file: khbb_lib/Cargo.toml + +[package] +name = "khbb_lib" +edition.workspace = true +version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + +[dependencies] +async-trait.workspace = true +base64.workspace = true +chrono.workspace = true +futures-util.workspace = true +reqwest.workspace = true +rustls.workspace = true +serde.workspace = true +serde_json.workspace = true +solana-account-decoder-client-types.workspace = true +solana-address-lookup-table-interface.workspace = true +solana-client.workspace = true +solana-compute-budget-interface.workspace = true +solana-rpc-client-api.workspace = true +solana-sdk.workspace = true +solana-sdk-ids.workspace = true +solana-system-interface.workspace = true +solana-transaction-status-client-types.workspace = true +spl-associated-token-account-interface.workspace = true +spl-memo-interface.workspace = true +spl-token-2022-interface.workspace = true +spl-token-interface.workspace = true +sqlx.workspace = true +tokio.workspace = true +tokio-stream.workspace = true +tokio-tungstenite.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +yellowstone-grpc-client.workspace = true +yellowstone-grpc-proto.workspace = true diff --git a/khbb_lib/README.md b/khbb_lib/README.md new file mode 100644 index 0000000..044faf3 --- /dev/null +++ b/khbb_lib/README.md @@ -0,0 +1,33 @@ + + +# khbb_lib + +Core library for the `khadhroony-bobot` workspace. + +## Goals + +- centralize reusable logic +- expose explicit APIs to binaries +- provide Solana RPC HTTP / WS / gRPC integrations +- provide storage and domain layers +- avoid hidden logic in binaries + +## Rules + +- no `anyhow` +- no `thiserror` +- no `?` +- no `unwrap` / `expect` +- explicit error handling +- async first +- `tracing` +- no `mod.rs` +- no `pub mod` +- `pub use` only from `lib.rs` + +## Initial scope + +- config loading +- tracing initialization +- SQLite connectivity +- listener runtime bootstrap diff --git a/khbb_lib/TODO.md b/khbb_lib/TODO.md new file mode 100644 index 0000000..4682e4f --- /dev/null +++ b/khbb_lib/TODO.md @@ -0,0 +1,22 @@ + + +# khbb_lib TODO + +## Foundation + +- [x] create public library entrypoint +- [x] create explicit error type +- [x] create config loader +- [x] create tracing bootstrap +- [x] create listener app runner + +## Next + +- [ ] add storage layer +- [ ] add SQLite schema bootstrap +- [ ] add HTTP RPC client layer based on `reqwest` +- [ ] add WebSocket RPC client layer based on `tokio-tungstenite` +- [ ] add Yellowstone gRPC client layer +- [ ] add event normalization types +- [ ] add listener orchestration +- [ ] add tests \ No newline at end of file diff --git a/khbb_lib/src/app.rs b/khbb_lib/src/app.rs new file mode 100644 index 0000000..1818b79 --- /dev/null +++ b/khbb_lib/src/app.rs @@ -0,0 +1,59 @@ +// file: khbb_lib/src/app.rs + +/// Runs the initial listener application workflow. +/// +/// This first version only: +/// - loads configuration +/// - opens the SQLite connection pool +/// - verifies connectivity +/// - keeps the runtime alive as the future integration point for listener tasks +pub async fn run_listener_app(config_path: &str) -> core::result::Result<(), crate::KhbbError> { + let config_result = crate::KhbbAppConfig::load_from_json_file(config_path).await; + let config = match config_result { + Ok(value) => value, + Err(error) => { + return Err(error); + }, + }; + let tracing_result = crate::init_tracing(&config.log_filter); + match tracing_result { + Ok(()) => {}, + Err(error) => { + return Err(error); + }, + } + tracing::info!( + database_url = %config.database_url, + solana_http_rpc_url = %config.solana_http_rpc_url, + solana_ws_rpc_url = %config.solana_ws_rpc_url, + yellowstone_grpc_url = ?config.yellowstone_grpc_url, + "khbb listener app starting" + ); + let connect_result = sqlx::sqlite::SqlitePoolOptions::new() + .max_connections(1) + .connect(&config.database_url) + .await; + let pool = match connect_result { + Ok(value) => value, + Err(error) => { + return Err(crate::KhbbError::Database { + context: "connect sqlite pool", + message: error.to_string(), + }); + }, + }; + let ping_result = sqlx::query("SELECT 1;").execute(&pool).await; + match ping_result { + Ok(_) => { + tracing::info!("sqlite connectivity check succeeded"); + }, + Err(error) => { + return Err(crate::KhbbError::Database { + context: "ping sqlite database", + message: error.to_string(), + }); + }, + } + tracing::info!("listener tasks are not wired yet"); + Ok(()) +} diff --git a/khbb_lib/src/config.rs b/khbb_lib/src/config.rs new file mode 100644 index 0000000..8f22822 --- /dev/null +++ b/khbb_lib/src/config.rs @@ -0,0 +1,71 @@ +// file: khbb_lib/src/config.rs + +/// Root application configuration used by the initial listener stack. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbAppConfig { + /// Path or URL to the SQLite database. + pub database_url: std::string::String, + /// Solana HTTP RPC endpoint. + pub solana_http_rpc_url: std::string::String, + /// Solana WebSocket RPC endpoint. + pub solana_ws_rpc_url: std::string::String, + /// Optional Yellowstone gRPC endpoint. + pub yellowstone_grpc_url: std::option::Option, + /// Tracing filter string. + pub log_filter: std::string::String, +} + +impl KhbbAppConfig { + /// Loads the application configuration from a JSON file. + pub async fn load_from_json_file(path: &str) -> core::result::Result { + let file_content_result = tokio::fs::read_to_string(path).await; + let file_content = match file_content_result { + Ok(value) => value, + Err(error) => { + return Err(crate::KhbbError::Io { + context: "read config file", + message: error.to_string(), + }); + }, + }; + let parse_result = serde_json::from_str::(&file_content); + let config = match parse_result { + Ok(value) => value, + Err(error) => { + return Err(crate::KhbbError::Json { + context: "parse config json", + message: error.to_string(), + }); + }, + }; + let validate_result = config.validate(); + match validate_result { + Ok(()) => Ok(config), + Err(error) => Err(error), + } + } + /// Validates the application configuration. + pub fn validate(&self) -> core::result::Result<(), crate::KhbbError> { + if self.database_url.trim().is_empty() { + return Err(crate::KhbbError::Config { + message: std::string::String::from("database_url must not be empty"), + }); + } + if self.solana_http_rpc_url.trim().is_empty() { + return Err(crate::KhbbError::Config { + message: std::string::String::from("solana_http_rpc_url must not be empty"), + }); + } + if self.solana_ws_rpc_url.trim().is_empty() { + return Err(crate::KhbbError::Config { + message: std::string::String::from("solana_ws_rpc_url must not be empty"), + }); + } + if self.log_filter.trim().is_empty() { + return Err(crate::KhbbError::Config { + message: std::string::String::from("log_filter must not be empty"), + }); + } + Ok(()) + } +} diff --git a/khbb_lib/src/error.rs b/khbb_lib/src/error.rs new file mode 100644 index 0000000..f9d4540 --- /dev/null +++ b/khbb_lib/src/error.rs @@ -0,0 +1,76 @@ +// file: khbb_lib/src/error.rs + +/// Main error type used across the khbb workspace. +/// +/// This project intentionally uses a single explicit error enum instead of +/// `anyhow` or `thiserror`. +#[derive(Debug)] +pub enum KhbbError { + /// Returned when a filesystem operation fails. + Io { + /// Human-readable operation label. + context: &'static str, + /// Source message. + message: std::string::String, + }, + /// Returned when JSON decoding or encoding fails. + Json { + /// Human-readable operation label. + context: &'static str, + /// Source message. + message: std::string::String, + }, + /// Returned when configuration validation fails. + Config { + /// Validation message. + message: std::string::String, + }, + /// Returned when SQLx operations fail. + Database { + /// Human-readable operation label. + context: &'static str, + /// Source message. + message: std::string::String, + }, + /// Returned when tracing initialization fails. + Tracing { + /// Human-readable operation label. + context: &'static str, + /// Source message. + message: std::string::String, + }, + /// Returned when a runtime task fails. + Runtime { + /// Human-readable operation label. + context: &'static str, + /// Source message. + message: std::string::String, + }, +} + +impl core::fmt::Display for KhbbError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Io { context, message } => { + write!(f, "io error during {context}: {message}") + }, + Self::Json { context, message } => { + write!(f, "json error during {context}: {message}") + }, + Self::Config { message } => { + write!(f, "configuration error: {message}") + }, + Self::Database { context, message } => { + write!(f, "database error during {context}: {message}") + }, + Self::Tracing { context, message } => { + write!(f, "tracing error during {context}: {message}") + }, + Self::Runtime { context, message } => { + write!(f, "runtime error during {context}: {message}") + }, + } + } +} + +impl std::error::Error for KhbbError {} diff --git a/khbb_lib/src/lib.rs b/khbb_lib/src/lib.rs new file mode 100644 index 0000000..456380e --- /dev/null +++ b/khbb_lib/src/lib.rs @@ -0,0 +1,23 @@ +// file: khbb_lib/src/lib.rs + +//! Core public library for the `khadhroony-bobot` workspace. +//! +//! This crate exposes the reusable building blocks shared by the khbb +//! applications, starting with the listener runtime bootstrap. + +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +mod app; +mod config; +mod error; +mod tracing_setup; + +/// Public re-exports for the khbb core library. +pub use crate::app::run_listener_app; +/// Public re-exports for configuration loading. +pub use crate::config::KhbbAppConfig; +/// Public re-exports for configuration loading errors and runtime errors. +pub use crate::error::KhbbError; +/// Public re-exports for tracing initialization. +pub use crate::tracing_setup::init_tracing; diff --git a/khbb_lib/src/tracing_setup.rs b/khbb_lib/src/tracing_setup.rs new file mode 100644 index 0000000..a9f31d0 --- /dev/null +++ b/khbb_lib/src/tracing_setup.rs @@ -0,0 +1,31 @@ +// file: khbb_lib/src/tracing_setup.rs + +/// Initializes tracing subscribers for the application. +pub fn init_tracing(log_filter: &str) -> core::result::Result<(), crate::KhbbError> { + let env_filter_result = + tracing_subscriber::EnvFilter::try_new(std::string::String::from(log_filter)); + let env_filter = match env_filter_result { + Ok(value) => value, + Err(error) => { + return Err(crate::KhbbError::Tracing { + context: "build env filter", + message: error.to_string(), + }); + }, + }; + let subscriber = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_target(true) + .with_thread_ids(true) + .with_thread_names(true) + .with_ansi(true) + .finish(); + let set_result = tracing::subscriber::set_global_default(subscriber); + match set_result { + Ok(()) => Ok(()), + Err(error) => Err(crate::KhbbError::Tracing { + context: "set global subscriber", + message: error.to_string(), + }), + } +} diff --git a/khbb_listener_app/Cargo.toml b/khbb_listener_app/Cargo.toml new file mode 100644 index 0000000..269f3c5 --- /dev/null +++ b/khbb_listener_app/Cargo.toml @@ -0,0 +1,14 @@ +# file: khbb_listener_app/Cargo.toml + +[package] +name = "khbb_listener_app" +edition.workspace = true +version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true + +[dependencies] +khbb_lib = { path = "../khbb_lib" } +tokio.workspace = true +tracing.workspace = true diff --git a/khbb_listener_app/README.md b/khbb_listener_app/README.md new file mode 100644 index 0000000..480e72c --- /dev/null +++ b/khbb_listener_app/README.md @@ -0,0 +1,15 @@ + + +# khbb_listener_app + +Listener binary for the `khadhroony-bobot` workspace. + +## Role + +This binary starts the listener runtime and delegates all logic to `khbb_lib`. + +## Current state + +- reads `config.json` by default +- accepts an optional config path as first CLI argument +- initializes the runtime through `khbb_lib` diff --git a/khbb_listener_app/TODO.md b/khbb_listener_app/TODO.md new file mode 100644 index 0000000..660ff50 --- /dev/null +++ b/khbb_listener_app/TODO.md @@ -0,0 +1,9 @@ + + +# khbb_listener_app TODO + +- [x] create thin binary entrypoint +- [x] delegate startup to `khbb_lib` +- [ ] add clean shutdown handling +- [ ] add signal handling +- [ ] add richer CLI options later if needed diff --git a/khbb_listener_app/src/main.rs b/khbb_listener_app/src/main.rs new file mode 100644 index 0000000..7337926 --- /dev/null +++ b/khbb_listener_app/src/main.rs @@ -0,0 +1,26 @@ +// file: khbb_listener_app/src/main.rs + +//! Binary entrypoint for the khbb listener application. +//! +//! This binary remains intentionally thin and delegates its logic to `khbb_lib`. + +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +/// Entrypoint of the khbb listener binary. +/// +/// This binary is intentionally thin and delegates all business logic to +/// `khbb_lib`. +#[tokio::main] +async fn main() -> std::process::ExitCode { + let args = std::env::args().collect::>(); + let config_path = if args.len() >= 2 { args[1].as_str() } else { "config.json" }; + let run_result = khbb_lib::run_listener_app(config_path).await; + match run_result { + Ok(()) => std::process::ExitCode::SUCCESS, + Err(error) => { + eprintln!("khbb_listener_app failed: {error}"); + std::process::ExitCode::FAILURE + }, + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..09a40c5 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,38 @@ +# file: rustfmt.toml + +edition = "2024" +newline_style = "Unix" +use_small_heuristics = "Default" +hard_tabs = false +tab_spaces = 4 +max_width = 100 +chain_width = 80 +fn_call_width = 80 +attr_fn_like_width = 80 +struct_lit_width = 40 +struct_variant_width = 40 +array_width = 80 +single_line_if_else_max_width = 80 +single_line_let_else_max_width = 80 +imports_indent = "Block" +group_imports = "StdExternalCrate" +imports_granularity = "Module" +reorder_imports = true +reorder_modules = true +normalize_comments = false +normalize_doc_attributes = false +format_code_in_doc_comments = false +wrap_comments = false +format_strings = false +hex_literal_case = "Lower" +empty_item_single_line = true +struct_field_align_threshold = 0 +enum_discrim_align_threshold = 0 +match_arm_blocks = true +match_block_trailing_comma = true +trailing_comma = "Vertical" +use_field_init_shorthand = true +use_try_shorthand = false +force_explicit_abi = true +condense_wildcard_suffixes = false +unstable_features = false