This commit is contained in:
2026-04-17 18:55:25 +02:00
commit d6a33a7fcb
16 changed files with 580 additions and 0 deletions

42
.gitignore vendored Normal file
View File

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

47
Cargo.toml Normal file
View File

@@ -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 <sinus@sasedev.net>"]
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 = [] }

34
clippy.toml Normal file
View File

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

40
khbb_lib/Cargo.toml Normal file
View File

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

33
khbb_lib/README.md Normal file
View File

@@ -0,0 +1,33 @@
<!-- file: khbb_lib/README.md -->
# 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

22
khbb_lib/TODO.md Normal file
View File

@@ -0,0 +1,22 @@
<!-- file: khbb_lib/TODO.md -->
# 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

59
khbb_lib/src/app.rs Normal file
View File

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

71
khbb_lib/src/config.rs Normal file
View File

@@ -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<std::string::String>,
/// 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<Self, crate::KhbbError> {
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::<Self>(&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(())
}
}

76
khbb_lib/src/error.rs Normal file
View File

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

23
khbb_lib/src/lib.rs Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<!-- file: khbb_listener_app/README.md -->
# 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`

View File

@@ -0,0 +1,9 @@
<!-- file: khbb_listener_app/TODO.md -->
# 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

View File

@@ -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::<std::vec::Vec<std::string::String>>();
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
},
}
}

38
rustfmt.toml Normal file
View File

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