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

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