0.5.8
This commit is contained in:
@@ -8,7 +8,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.7"
|
version = "0.5.8"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"
|
||||||
|
|||||||
84
context.md
Normal file
84
context.md
Normal 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 qu’appeler 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)]
|
||||||
@@ -28,6 +28,8 @@ mod account_enrichment;
|
|||||||
mod enriched_classifier;
|
mod enriched_classifier;
|
||||||
mod signal_correlation;
|
mod signal_correlation;
|
||||||
mod candidate;
|
mod candidate;
|
||||||
|
mod session_candidate;
|
||||||
|
mod session_tracker;
|
||||||
|
|
||||||
/// Runs the listener application bootstrap workflow.
|
/// Runs the listener application bootstrap workflow.
|
||||||
pub use crate::app::run_listener_app;
|
pub use crate::app::run_listener_app;
|
||||||
@@ -145,3 +147,11 @@ pub use crate::candidate::KhbbTokenAccountCandidate;
|
|||||||
pub use crate::candidate::KhbbMintCandidate;
|
pub use crate::candidate::KhbbMintCandidate;
|
||||||
/// Candidate bootstrap flow.
|
/// Candidate bootstrap flow.
|
||||||
pub use crate::candidate::KhbbBootstrapFlowCandidate;
|
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;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub async fn run_listener_runtime(
|
|||||||
let mut interval = tokio::time::interval(tick_duration);
|
let mut interval = tokio::time::interval(tick_duration);
|
||||||
let mut tick_count: u64 = 0;
|
let mut tick_count: u64 = 0;
|
||||||
let mut final_status = std::string::String::from("stopped");
|
let mut final_status = std::string::String::from("stopped");
|
||||||
|
let mut session_candidate_tracker = crate::KhbbSessionCandidateTracker::new();
|
||||||
let http_client_config =
|
let http_client_config =
|
||||||
crate::KhbbSolanaHttpRpcClientConfig { url: config.solana_http_rpc_url.clone() };
|
crate::KhbbSolanaHttpRpcClientConfig { url: config.solana_http_rpc_url.clone() };
|
||||||
let http_client_result = crate::KhbbSolanaHttpRpcClient::new(http_client_config);
|
let http_client_result = crate::KhbbSolanaHttpRpcClient::new(http_client_config);
|
||||||
@@ -693,40 +694,26 @@ pub async fn run_listener_runtime(
|
|||||||
lamports = ?correlated.lamports,
|
lamports = ?correlated.lamports,
|
||||||
"correlated confirmed token account update signal"
|
"correlated confirmed token account update signal"
|
||||||
);
|
);
|
||||||
let candidate_result =
|
let tracker_update =
|
||||||
crate::candidate::build_candidates_from_correlated_signal(
|
session_candidate_tracker.upsert_from_correlated_signal(
|
||||||
&crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(
|
&crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(
|
||||||
correlated.clone(),
|
correlated.clone(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
match candidate_result {
|
|
||||||
Ok(candidates) => {
|
|
||||||
for candidate in candidates {
|
|
||||||
match candidate {
|
|
||||||
crate::KhbbCandidate::TokenAccount(inner) => {
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
pubkey = %inner.pubkey,
|
is_new = tracker_update.is_new,
|
||||||
context_slot = inner.context_slot,
|
key = %tracker_update.candidate.key,
|
||||||
owner = ?inner.owner,
|
category = %tracker_update.candidate.category,
|
||||||
lamports = ?inner.lamports,
|
pubkey = ?tracker_update.candidate.pubkey,
|
||||||
"token account candidate created"
|
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::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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(correlated) => {
|
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(correlated) => {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
@@ -781,40 +768,26 @@ pub async fn run_listener_runtime(
|
|||||||
lamports = ?correlated.lamports,
|
lamports = ?correlated.lamports,
|
||||||
"correlated potential new token mint signal"
|
"correlated potential new token mint signal"
|
||||||
);
|
);
|
||||||
let candidate_result =
|
let tracker_update =
|
||||||
crate::candidate::build_candidates_from_correlated_signal(
|
session_candidate_tracker.upsert_from_correlated_signal(
|
||||||
&crate::KhbbCorrelatedSignal::PotentialNewTokenMint(
|
&crate::KhbbCorrelatedSignal::PotentialNewTokenMint(
|
||||||
correlated.clone(),
|
correlated.clone(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
match candidate_result {
|
|
||||||
Ok(candidates) => {
|
|
||||||
for candidate in candidates {
|
|
||||||
match candidate {
|
|
||||||
crate::KhbbCandidate::Mint(inner) => {
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
pubkey = %inner.pubkey,
|
is_new = tracker_update.is_new,
|
||||||
context_slot = inner.context_slot,
|
key = %tracker_update.candidate.key,
|
||||||
owner = ?inner.owner,
|
category = %tracker_update.candidate.category,
|
||||||
lamports = ?inner.lamports,
|
pubkey = ?tracker_update.candidate.pubkey,
|
||||||
"mint candidate created"
|
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::KhbbCandidate::TokenAccount(_) => {}
|
|
||||||
crate::KhbbCandidate::BootstrapFlow(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(
|
|
||||||
listener_session_id = session.id,
|
|
||||||
error = %error,
|
|
||||||
"failed to build mint candidates from correlated signal"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
|
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
|
||||||
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(_) => {}
|
crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(_) => {}
|
||||||
}
|
}
|
||||||
@@ -860,41 +833,26 @@ pub async fn run_listener_runtime(
|
|||||||
saw_bootstrap_logs = correlated.saw_bootstrap_logs,
|
saw_bootstrap_logs = correlated.saw_bootstrap_logs,
|
||||||
"correlated potential token bootstrap flow signal"
|
"correlated potential token bootstrap flow signal"
|
||||||
);
|
);
|
||||||
let candidate_result =
|
let tracker_update =
|
||||||
crate::candidate::build_candidates_from_correlated_signal(
|
session_candidate_tracker.upsert_from_correlated_signal(
|
||||||
&crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(
|
&crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(
|
||||||
correlated.clone(),
|
correlated.clone(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
match candidate_result {
|
|
||||||
Ok(candidates) => {
|
|
||||||
for candidate in candidates {
|
|
||||||
match candidate {
|
|
||||||
crate::KhbbCandidate::BootstrapFlow(inner) => {
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
pubkey = ?inner.pubkey,
|
is_new = tracker_update.is_new,
|
||||||
context_slot = inner.context_slot,
|
key = %tracker_update.candidate.key,
|
||||||
saw_token_program = inner.saw_token_program,
|
category = %tracker_update.candidate.category,
|
||||||
saw_associated_token_account = inner.saw_associated_token_account,
|
pubkey = ?tracker_update.candidate.pubkey,
|
||||||
saw_bootstrap_logs = inner.saw_bootstrap_logs,
|
first_seen_slot = tracker_update.candidate.first_seen_slot,
|
||||||
"bootstrap flow candidate created"
|
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::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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
|
crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {}
|
||||||
crate::KhbbCorrelatedSignal::PotentialNewTokenMint(_) => {}
|
crate::KhbbCorrelatedSignal::PotentialNewTokenMint(_) => {}
|
||||||
}
|
}
|
||||||
|
|||||||
35
khbb_lib/src/session_candidate.rs
Normal file
35
khbb_lib/src/session_candidate.rs
Normal 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,
|
||||||
|
}
|
||||||
211
khbb_lib/src/session_tracker.rs
Normal file
211
khbb_lib/src/session_tracker.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user