This commit is contained in:
2026-04-19 00:07:08 +02:00
parent ed490a96f6
commit bc089c1a10
6 changed files with 387 additions and 89 deletions

View File

@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "0.5.7"
version = "0.5.8"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"

84
context.md Normal file
View File

@@ -0,0 +1,84 @@
Tu travaille sur le projet Rust khadhroony-bobot
Contexte :
- projet personnel de détection et de trading automatisé sur Solana,
- base technique : `solana-client` + RPC HTTP + WebSocket Solana + gRpc,
- pas de ML au départ,
- architecture workspace multi-crates avec trois binaires principaux : `khbb_listener_app`, `khbb_trader_app` et `khbb_pattern_analyser_app`,
- style de code strict : no `anyhow`, no `thiserror`, no `?`, no `unwrap` / `expect`, explicit error handling, async first, tracing, no `mod.rs`, no pub mod, pub use only, no import with use (allowed only for Traits) and call for functions and struct with full path (ex : khbb_lib::Error)
- on se concentrera sur deux crates au départ : khbb_lib et khbb_listener_app
- objectif final de `khbb_listener_app` : detecter la création de Tokens (pair TKN/SOL ou TKN/WSOL ou TKN/USDT, etc..) sur la blockchain Solana, leur évolution de prix et les information les concernant (liquidité, marketcap, volume, prix etc.) via les différents dex tels que pubfun, punpswap, raydium, meteora,bags, fluxbeam, launchbeam, heaven, dexlab, moonit, zora...
- les bibliothèques/crate seront déclarés au niveau du workspace et seront réutilisés dans les sous-crates.
- dépendances de base (pouvant évoluer par la suite) :
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"] }
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 = [] }
- on ne va pas utiliser le client rpc ni pubsub de solana-client, mais soulement les types rpc et binaires de solana-rpc-client-api/solana-account-decoder-client-types, et établire notre propre connexion aux serveur rpc http/ws via reqwest/tokio-tungstenite ou au grpc de helios via yollowstone-grpc*
- la première partie sur laquelle son se concentre est khbb_lib/khbb_listener_app qui ne fait quécouter les différents flux et stoker les données en base de donnée.
- une fois cette partie effectuée on passera à la partie khbb_lib/khbb_patter_analyser_app qui servira a detecter les patterns redondant au niveau des mints/trades de token tel que durée de vie, scamm, tendances de noms etc..
- la troisieme partie correpondra à khbb_lib/khbb_trading_app qui correspond à la partie trading automatique de token lors de leur création en fonction de potentiel/risque + patterns analysés.
- la structure du skeleton du projet est la suivante :
├── Cargo.toml
├── clippy.toml
├── config.json
├── dbdata
│   └── app.db
├── khbb_lib
│   ├── Cargo.toml
│   ├── README.md
│   ├── src
│   │   └── lib.rs
│   └── TODO.md
├── khbb_ listener_app
│   ├── Cargo.toml
│   ├── README.md
│   ├── src
│   │   └── main.rs
│   └── TODO.md
...
├── rustfmt.toml
├── TODO.md
└── wallets
└── dev-wallet.json
les binaires rust ne font quappeler les fonctions exposée dans khbb_lib
caque fichier (pouvant accepter des commentaires) devra commencer par une entête indiquant son chemin dans le projet :
ex rust :
// file: khbb_lib/src/lib.rs
ex markdown :
<!-- file: khbb_trading_app/TODO.md →
ex Toml:
# file: khbb_listener_app/Cargo.toml
etc..
les fichiers lib.rs et main.rs devron contenir :
#![deny(unreachable_pub)]
#![warn(missing_docs)]

View File

@@ -28,6 +28,8 @@ mod account_enrichment;
mod enriched_classifier;
mod signal_correlation;
mod candidate;
mod session_candidate;
mod session_tracker;
/// Runs the listener application bootstrap workflow.
pub use crate::app::run_listener_app;
@@ -145,3 +147,11 @@ pub use crate::candidate::KhbbTokenAccountCandidate;
pub use crate::candidate::KhbbMintCandidate;
/// Candidate bootstrap flow.
pub use crate::candidate::KhbbBootstrapFlowCandidate;
/// Candidate confidence level.
pub use crate::session_candidate::KhbbCandidateConfidence;
/// Candidate tracked during the current listener session.
pub use crate::session_candidate::KhbbSessionCandidate;
/// In-memory tracker for candidates observed during a single listener session.
pub use crate::session_tracker::KhbbSessionCandidateTracker;
/// Result of inserting or updating a session candidate.
pub use crate::session_tracker::KhbbSessionCandidateUpdate;

View File

@@ -40,6 +40,7 @@ pub async fn run_listener_runtime(
let mut interval = tokio::time::interval(tick_duration);
let mut tick_count: u64 = 0;
let mut final_status = std::string::String::from("stopped");
let mut session_candidate_tracker = crate::KhbbSessionCandidateTracker::new();
let http_client_config =
crate::KhbbSolanaHttpRpcClientConfig { url: config.solana_http_rpc_url.clone() };
let http_client_result = crate::KhbbSolanaHttpRpcClient::new(http_client_config);
@@ -693,39 +694,25 @@ pub async fn run_listener_runtime(
lamports = ?correlated.lamports,
"correlated confirmed token account update signal"
);
let candidate_result =
crate::candidate::build_candidates_from_correlated_signal(
let tracker_update =
session_candidate_tracker.upsert_from_correlated_signal(
&crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(
correlated.clone(),
),
);
match candidate_result {
Ok(candidates) => {
for candidate in candidates {
match candidate {
crate::KhbbCandidate::TokenAccount(inner) => {
tracing::trace!(
listener_session_id = session.id,
pubkey = %inner.pubkey,
context_slot = inner.context_slot,
owner = ?inner.owner,
lamports = ?inner.lamports,
"token account candidate created"
);
}
crate::KhbbCandidate::Mint(_) => {}
crate::KhbbCandidate::BootstrapFlow(_) => {}
}
}
}
Err(error) => {
tracing::error!(
listener_session_id = session.id,
error = %error,
"failed to build token account candidates from correlated signal"
);
}
}
tracing::trace!(
listener_session_id = session.id,
is_new = tracker_update.is_new,
key = %tracker_update.candidate.key,
category = %tracker_update.candidate.category,
pubkey = ?tracker_update.candidate.pubkey,
first_seen_slot = tracker_update.candidate.first_seen_slot,
last_seen_slot = tracker_update.candidate.last_seen_slot,
seen_count = tracker_update.candidate.seen_count,
score = tracker_update.candidate.score,
confidence = ?tracker_update.candidate.confidence,
"token account session candidate upserted"
);
}
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(correlated) => {
tracing::trace!(
@@ -781,39 +768,25 @@ pub async fn run_listener_runtime(
lamports = ?correlated.lamports,
"correlated potential new token mint signal"
);
let candidate_result =
crate::candidate::build_candidates_from_correlated_signal(
let tracker_update =
session_candidate_tracker.upsert_from_correlated_signal(
&crate::KhbbCorrelatedSignal::PotentialNewTokenMint(
correlated.clone(),
),
);
match candidate_result {
Ok(candidates) => {
for candidate in candidates {
match candidate {
crate::KhbbCandidate::Mint(inner) => {
tracing::trace!(
listener_session_id = session.id,
pubkey = %inner.pubkey,
context_slot = inner.context_slot,
owner = ?inner.owner,
lamports = ?inner.lamports,
"mint candidate created"
);
}
crate::KhbbCandidate::TokenAccount(_) => {}
crate::KhbbCandidate::BootstrapFlow(_) => {}
}
}
}
Err(error) => {
tracing::error!(
listener_session_id = session.id,
error = %error,
"failed to build mint candidates from correlated signal"
);
}
}
tracing::trace!(
listener_session_id = session.id,
is_new = tracker_update.is_new,
key = %tracker_update.candidate.key,
category = %tracker_update.candidate.category,
pubkey = ?tracker_update.candidate.pubkey,
first_seen_slot = tracker_update.candidate.first_seen_slot,
last_seen_slot = tracker_update.candidate.last_seen_slot,
seen_count = tracker_update.candidate.seen_count,
score = tracker_update.candidate.score,
confidence = ?tracker_update.candidate.confidence,
"mint session candidate upserted"
);
}
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(_) => {}
@@ -860,40 +833,25 @@ pub async fn run_listener_runtime(
saw_bootstrap_logs = correlated.saw_bootstrap_logs,
"correlated potential token bootstrap flow signal"
);
let candidate_result =
crate::candidate::build_candidates_from_correlated_signal(
let tracker_update =
session_candidate_tracker.upsert_from_correlated_signal(
&crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(
correlated.clone(),
),
);
match candidate_result {
Ok(candidates) => {
for candidate in candidates {
match candidate {
crate::KhbbCandidate::BootstrapFlow(inner) => {
tracing::trace!(
listener_session_id = session.id,
pubkey = ?inner.pubkey,
context_slot = inner.context_slot,
saw_token_program = inner.saw_token_program,
saw_associated_token_account = inner.saw_associated_token_account,
saw_bootstrap_logs = inner.saw_bootstrap_logs,
"bootstrap flow candidate created"
);
}
crate::KhbbCandidate::TokenAccount(_) => {}
crate::KhbbCandidate::Mint(_) => {}
}
}
}
Err(error) => {
tracing::error!(
listener_session_id = session.id,
error = %error,
"failed to build bootstrap flow candidates from correlated signal"
);
}
}
tracing::trace!(
listener_session_id = session.id,
is_new = tracker_update.is_new,
key = %tracker_update.candidate.key,
category = %tracker_update.candidate.category,
pubkey = ?tracker_update.candidate.pubkey,
first_seen_slot = tracker_update.candidate.first_seen_slot,
last_seen_slot = tracker_update.candidate.last_seen_slot,
seen_count = tracker_update.candidate.seen_count,
score = tracker_update.candidate.score,
confidence = ?tracker_update.candidate.confidence,
"bootstrap flow session candidate upserted"
);
}
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
crate::KhbbCorrelatedSignal::PotentialNewTokenMint(_) => {}

View File

@@ -0,0 +1,35 @@
// file: khbb_lib/src/session_candidate.rs
//! Short-lived in-memory candidate tracking for a single listener session.
/// Candidate confidence level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum KhbbCandidateConfidence {
/// Weak confidence.
Low,
/// Medium confidence.
Medium,
/// Strong confidence.
High,
}
/// Candidate tracked during the current listener session.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KhbbSessionCandidate {
/// Stable local key used for deduplication.
pub key: std::string::String,
/// Candidate category.
pub category: std::string::String,
/// Optional pubkey attached to the candidate.
pub pubkey: std::option::Option<std::string::String>,
/// First slot at which the candidate was seen.
pub first_seen_slot: u64,
/// Most recent slot at which the candidate was seen.
pub last_seen_slot: u64,
/// Number of times this candidate was observed during the session.
pub seen_count: u64,
/// Lightweight numeric score.
pub score: u64,
/// Confidence level derived from the score.
pub confidence: KhbbCandidateConfidence,
}

View File

@@ -0,0 +1,211 @@
// file: khbb_lib/src/session_tracker.rs
//! In-memory session tracking for correlated candidates.
/// In-memory tracker for candidates observed during a single listener session.
#[derive(Debug, Default)]
pub struct KhbbSessionCandidateTracker {
candidates: std::collections::BTreeMap<std::string::String, crate::KhbbSessionCandidate>,
}
/// Result of inserting or updating a session candidate.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KhbbSessionCandidateUpdate {
/// Whether the candidate was newly inserted.
pub is_new: bool,
/// Current candidate snapshot after the update.
pub candidate: crate::KhbbSessionCandidate,
}
impl KhbbSessionCandidateTracker {
/// Creates a new empty tracker.
pub fn new() -> Self {
Self {
candidates: std::collections::BTreeMap::new(),
}
}
/// Upserts a candidate built from a correlated signal.
pub fn upsert_from_correlated_signal(
&mut self,
signal: &crate::KhbbCorrelatedSignal,
) -> crate::KhbbSessionCandidateUpdate {
let candidate_blueprint = build_session_candidate_from_correlated_signal(signal);
let existing_option = self.candidates.get_mut(&candidate_blueprint.key);
match existing_option {
Some(existing) => {
existing.last_seen_slot = candidate_blueprint.last_seen_slot;
existing.seen_count = existing.seen_count.saturating_add(1);
existing.score =
compute_candidate_score(existing.category.as_str(), existing.seen_count);
existing.confidence = compute_candidate_confidence(existing.score);
crate::KhbbSessionCandidateUpdate {
is_new: false,
candidate: existing.clone(),
}
},
None => {
self.candidates
.insert(candidate_blueprint.key.clone(), candidate_blueprint.clone());
crate::KhbbSessionCandidateUpdate {
is_new: true,
candidate: candidate_blueprint,
}
},
}
}
}
fn build_session_candidate_from_correlated_signal(
signal: &crate::KhbbCorrelatedSignal,
) -> crate::KhbbSessionCandidate {
match signal {
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(inner) => {
let key = std::format!("token_account:{}", inner.pubkey);
let category = std::string::String::from("token_account");
let seen_count = 1u64;
let score = compute_candidate_score(category.as_str(), seen_count);
let confidence = compute_candidate_confidence(score);
crate::KhbbSessionCandidate {
key,
category,
pubkey: Some(inner.pubkey.clone()),
first_seen_slot: inner.context_slot,
last_seen_slot: inner.context_slot,
seen_count,
score,
confidence,
}
},
crate::KhbbCorrelatedSignal::PotentialNewTokenMint(inner) => {
let key = std::format!("mint:{}", inner.pubkey);
let category = std::string::String::from("mint");
let seen_count = 1u64;
let score = compute_candidate_score(category.as_str(), seen_count);
let confidence = compute_candidate_confidence(score);
crate::KhbbSessionCandidate {
key,
category,
pubkey: Some(inner.pubkey.clone()),
first_seen_slot: inner.context_slot,
last_seen_slot: inner.context_slot,
seen_count,
score,
confidence,
}
},
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(inner) => {
let key = match &inner.pubkey {
Some(value) => std::format!("bootstrap:{}", value),
None => std::format!("bootstrap:slot:{}", inner.context_slot),
};
let category = std::string::String::from("bootstrap_flow");
let seen_count = 1u64;
let score = compute_candidate_score(category.as_str(), seen_count);
let confidence = compute_candidate_confidence(score);
crate::KhbbSessionCandidate {
key,
category,
pubkey: inner.pubkey.clone(),
first_seen_slot: inner.context_slot,
last_seen_slot: inner.context_slot,
seen_count,
score,
confidence,
}
},
}
}
fn compute_candidate_score(category: &str, seen_count: u64) -> u64 {
let base_score = match category {
"mint" => 80u64,
"bootstrap_flow" => 60u64,
"token_account" => 40u64,
_ => 20u64,
};
base_score.saturating_add(seen_count.saturating_sub(1).saturating_mul(10))
}
fn compute_candidate_confidence(score: u64) -> crate::KhbbCandidateConfidence {
if score >= 80 {
return crate::KhbbCandidateConfidence::High;
}
if score >= 50 {
return crate::KhbbCandidateConfidence::Medium;
}
crate::KhbbCandidateConfidence::Low
}
#[cfg(test)]
mod tests {
#[test]
fn upsert_from_correlated_signal_inserts_new_token_account_candidate() {
let mut tracker = super::KhbbSessionCandidateTracker::new();
let signal = crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(
crate::KhbbConfirmedTokenAccountUpdateSignal {
pubkey: std::string::String::from("SomePubkey"),
context_slot: 100,
owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()),
lamports: Some(123),
},
);
let update = tracker.upsert_from_correlated_signal(&signal);
assert!(update.is_new);
assert_eq!(update.candidate.key, "token_account:SomePubkey");
assert_eq!(update.candidate.seen_count, 1);
assert_eq!(update.candidate.confidence, crate::KhbbCandidateConfidence::Low);
}
#[test]
fn upsert_from_correlated_signal_updates_existing_candidate() {
let mut tracker = super::KhbbSessionCandidateTracker::new();
let signal = crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(
crate::KhbbConfirmedTokenAccountUpdateSignal {
pubkey: std::string::String::from("SomePubkey"),
context_slot: 100,
owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()),
lamports: Some(123),
},
);
let first = tracker.upsert_from_correlated_signal(&signal);
assert!(first.is_new);
let second = tracker.upsert_from_correlated_signal(&signal);
assert!(!second.is_new);
assert_eq!(second.candidate.seen_count, 2);
assert_eq!(second.candidate.score, 50);
assert_eq!(second.candidate.confidence, crate::KhbbCandidateConfidence::Medium);
}
#[test]
fn mint_candidate_starts_with_high_confidence() {
let mut tracker = super::KhbbSessionCandidateTracker::new();
let signal = crate::KhbbCorrelatedSignal::PotentialNewTokenMint(
crate::KhbbPotentialNewTokenMintSignal {
pubkey: std::string::String::from("MintPubkey"),
context_slot: 200,
owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()),
lamports: Some(456),
},
);
let update = tracker.upsert_from_correlated_signal(&signal);
assert!(update.is_new);
assert_eq!(update.candidate.confidence, crate::KhbbCandidateConfidence::High);
}
#[test]
fn bootstrap_candidate_starts_with_medium_confidence() {
let mut tracker = super::KhbbSessionCandidateTracker::new();
let signal = crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(
crate::KhbbPotentialTokenBootstrapFlowSignal {
pubkey: Some(std::string::String::from("FlowPubkey")),
context_slot: 300,
saw_token_program: true,
saw_associated_token_account: true,
saw_bootstrap_logs: false,
},
);
let update = tracker.upsert_from_correlated_signal(&signal);
assert!(update.is_new);
assert_eq!(update.candidate.confidence, crate::KhbbCandidateConfidence::Medium);
}
}