From f0831e4cd46c887b75f213aa825d8a734f1b59bb Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 29 Apr 2026 14:04:07 +0200 Subject: [PATCH] 0.7.13 --- CHANGELOG.md | 1 + Cargo.toml | 2 +- ROADMAP.md | 14 +- kb_lib/src/launch_origin.rs | 472 +++++++++++++++++++++++++++++++++--- 4 files changed, 443 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95c57cf..6da7cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,3 +43,4 @@ 0.7.10 - Ajout du premier support Orca Whirlpools avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing 0.7.11 - Ajout du premier support FluxBeam avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing 0.7.12 - Ajout du premier support DexLab Swap/Pool avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing +0.7.13 - Extension de la couche launch origins avec Bags et Moonit, ajout d’un enregistrement programmatique des mappings Bags, et détection automatique Moonit via suffixe de token mint diff --git a/Cargo.toml b/Cargo.toml index 6412245..1e773f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.12" +version = "0.7.13" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/ROADMAP.md b/ROADMAP.md index f250dcb..99959af 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -578,15 +578,13 @@ Réalisé : - conservation d’une séparation entre pool DexLab natif et éventuel `OpenBook Market ID` créé ensuite. ### 6.045. Version `0.7.13` — Bags / Moonit comme origines de lancement -Objectif : étendre la couche `launch origins` à d’autres surfaces au-dessus des protocoles DEX déjà intégrés. +Réalisé : -À faire : - -- ajouter `Bags` comme surface de lancement détectable, -- ajouter `Moonit` comme surface de lancement détectable, -- relier ces surfaces aux pools et paires finalement créés, -- conserver une séparation stricte entre origine de lancement et protocole on-chain, -- préparer l’extension future à d’autres launchpads ou surfaces dérivées. +- extension de la couche `launch origins` à `Bags` et `Moonit`, +- ajout d’un enregistrement programmatique des mappings `Bags` à partir des champs `tokenMint`, `dbcConfigKey`, `dbcPoolKey` et `dammV2PoolKey`, +- prise en charge de l’attribution `Bags` par matching exact sur `config_account`, `pool_account` et `token_mint`, +- prise en charge de l’attribution `Moonit` par détection automatique des token mints se terminant par `moon`, +- conservation d’une séparation stricte entre origine de lancement et protocole on-chain. ### 6.046. Version `0.7.14` — Consolidation multi-DEX Objectif : unifier le comportement des connecteurs DEX v1 avant l’ouverture des couches analytiques plus riches. diff --git a/kb_lib/src/launch_origin.rs b/kb_lib/src/launch_origin.rs index a1c0ceb..c661a0e 100644 --- a/kb_lib/src/launch_origin.rs +++ b/kb_lib/src/launch_origin.rs @@ -30,7 +30,154 @@ impl KbLaunchOriginService { /// Creates a new launch-origin service. pub fn new(database: std::sync::Arc) -> Self { let persistence = crate::KbDetectionPersistenceService::new(database.clone()); - Self { database, persistence } + Self { + database, + persistence, + } + } + + /// Ensures that the built-in `moonit` launch surface exists and returns its id. + pub async fn ensure_moonit_surface(&self) -> Result { + let existing_result = + crate::get_launch_surface_by_code(self.database.as_ref(), "moonit").await; + let existing_option = match existing_result { + Ok(existing_option) => existing_option, + Err(error) => return Err(error), + }; + let surface_id = match existing_option { + Some(existing) => match existing.id { + Some(surface_id) => surface_id, + None => { + return Err(crate::KbError::InvalidState( + "moonit launch surface has no internal id".to_string(), + )); + } + }, + None => { + let dto = crate::KbLaunchSurfaceDto::new( + "moonit".to_string(), + "Moonit".to_string(), + Some("launchpad".to_string()), + true, + ); + let insert_result = + crate::upsert_launch_surface(self.database.as_ref(), &dto).await; + match insert_result { + Ok(surface_id) => surface_id, + Err(error) => return Err(error), + } + } + }; + let suffix_key_result = crate::upsert_launch_surface_key( + self.database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "token_mint_suffix".to_string(), + "moon".to_string(), + ), + ) + .await; + if let Err(error) = suffix_key_result { + return Err(error); + } + Ok(surface_id) + } + + /// Ensures that the built-in `bags` launch surface exists and returns its id. + pub async fn ensure_bags_surface(&self) -> Result { + let existing_result = + crate::get_launch_surface_by_code(self.database.as_ref(), "bags").await; + let existing_option = match existing_result { + Ok(existing_option) => existing_option, + Err(error) => return Err(error), + }; + match existing_option { + Some(existing) => match existing.id { + Some(surface_id) => Ok(surface_id), + None => Err(crate::KbError::InvalidState( + "bags launch surface has no internal id".to_string(), + )), + }, + None => { + let dto = crate::KbLaunchSurfaceDto::new( + "bags".to_string(), + "Bags".to_string(), + Some("launchpad".to_string()), + true, + ); + crate::upsert_launch_surface(self.database.as_ref(), &dto).await + } + } + } + + /// Registers one Bags pool mapping from the documented Bags API fields. + pub async fn register_bags_pool_mapping( + &self, + token_mint: &str, + dbc_config_key: std::option::Option, + dbc_pool_key: std::option::Option, + damm_v2_pool_key: std::option::Option, + ) -> Result { + let surface_id_result = self.ensure_bags_surface().await; + let surface_id = match surface_id_result { + Ok(surface_id) => surface_id, + Err(error) => return Err(error), + }; + let token_key_result = crate::upsert_launch_surface_key( + self.database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "token_mint".to_string(), + token_mint.to_string(), + ), + ) + .await; + if let Err(error) = token_key_result { + return Err(error); + } + if let Some(dbc_config_key) = dbc_config_key { + let key_result = crate::upsert_launch_surface_key( + self.database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "config_account".to_string(), + dbc_config_key, + ), + ) + .await; + if let Err(error) = key_result { + return Err(error); + } + } + if let Some(dbc_pool_key) = dbc_pool_key { + let key_result = crate::upsert_launch_surface_key( + self.database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "pool_account".to_string(), + dbc_pool_key, + ), + ) + .await; + if let Err(error) = key_result { + return Err(error); + } + } + if let Some(damm_v2_pool_key) = damm_v2_pool_key { + let key_result = crate::upsert_launch_surface_key( + self.database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "pool_account".to_string(), + damm_v2_pool_key, + ), + ) + .await; + if let Err(error) = key_result { + return Err(error); + } + } + Ok(surface_id) } /// Attributes one transaction to known launch surfaces from decoded events. @@ -62,6 +209,10 @@ impl KbLaunchOriginService { ))); } }; + let ensure_moonit_result = self.ensure_moonit_surface().await; + if let Err(error) = ensure_moonit_result { + return Err(error); + } let decoded_events_result = crate::list_dex_decoded_events_by_transaction_id( self.database.as_ref(), transaction_id, @@ -73,9 +224,7 @@ impl KbLaunchOriginService { }; let mut results = std::vec::Vec::new(); for decoded_event in &decoded_events { - let candidate_result = self - .match_surface_for_decoded_event(decoded_event) - .await; + let candidate_result = self.match_surface_for_decoded_event(decoded_event).await; let candidate = match candidate_result { Ok(candidate) => candidate, Err(error) => return Err(error), @@ -188,10 +337,7 @@ impl KbLaunchOriginService { let signal_result = self .persistence .record_signal(&crate::KbDetectionSignalInput::new( - format!( - "signal.launch_surface.{}.detected", - candidate.surface_code - ), + format!("signal.launch_surface.{}.detected", candidate.surface_code), crate::KbAnalysisSignalSeverity::Low, transaction.signature.clone(), Some(observation_id), @@ -224,6 +370,15 @@ impl KbLaunchOriginService { if let Some(config_account) = decoded_event.market_account.clone() { candidates.push(("config_account".to_string(), config_account)); } + if let Some(pool_account) = decoded_event.pool_account.clone() { + candidates.push(("pool_account".to_string(), pool_account)); + } + if let Some(token_a_mint) = decoded_event.token_a_mint.clone() { + candidates.push(("token_mint".to_string(), token_a_mint)); + } + if let Some(token_b_mint) = decoded_event.token_b_mint.clone() { + candidates.push(("token_mint".to_string(), token_b_mint)); + } let payload_result = serde_json::from_str::( decoded_event.payload_json.as_str(), ); @@ -234,13 +389,13 @@ impl KbLaunchOriginService { if let Some(payload) = payload.as_ref() { let creator = kb_extract_payload_string( payload, - &["creator", "poolCreator", "user", "owner"], + &["creator", "poolCreator", "user", "owner", "payer"], ); if let Some(creator) = creator { candidates.push(("creator".to_string(), creator)); } } - for (match_kind, matched_value) in candidates { + for (match_kind, matched_value) in &candidates { let key_result = crate::get_launch_surface_key_by_match( self.database.as_ref(), match_kind.as_str(), @@ -255,34 +410,109 @@ impl KbLaunchOriginService { Some(key) => key, None => continue, }; - let surface_id = key.launch_surface_id; - let surface_result = crate::list_launch_surfaces(self.database.as_ref()).await; - let surfaces = match surface_result { - Ok(surfaces) => surfaces, + let matched_result = self + .build_matched_surface_from_key( + key, + match_kind.clone(), + matched_value.clone(), + ) + .await; + let matched_option = match matched_result { + Ok(matched_option) => matched_option, Err(error) => return Err(error), }; - for surface in surfaces { - let surface_id_option = surface.id; - let current_surface_id = match surface_id_option { - Some(current_surface_id) => current_surface_id, - None => continue, - }; - if current_surface_id != surface_id || !surface.is_enabled { - continue; - } - let matched_key_id = key.id; - return Ok(Some(KbMatchedLaunchSurface { - launch_surface_id: current_surface_id, - matched_key_id, - surface_code: surface.code, - surface_name: surface.name, - match_kind, - matched_value, - })); + if matched_option.is_some() { + return Ok(matched_option); } } + let moonit_result = self.match_moonit_by_suffix(decoded_event).await; + match moonit_result { + Ok(moonit_result) => Ok(moonit_result), + Err(error) => Err(error), + } + } + + async fn build_matched_surface_from_key( + &self, + key: crate::KbLaunchSurfaceKeyDto, + match_kind: std::string::String, + matched_value: std::string::String, + ) -> Result, crate::KbError> { + let surface_result = crate::list_launch_surfaces(self.database.as_ref()).await; + let surfaces = match surface_result { + Ok(surfaces) => surfaces, + Err(error) => return Err(error), + }; + for surface in surfaces { + let current_surface_id = match surface.id { + Some(current_surface_id) => current_surface_id, + None => continue, + }; + if current_surface_id != key.launch_surface_id || !surface.is_enabled { + continue; + } + return Ok(Some(KbMatchedLaunchSurface { + launch_surface_id: current_surface_id, + matched_key_id: key.id, + surface_code: surface.code, + surface_name: surface.name, + match_kind, + matched_value, + })); + } Ok(None) } + + async fn match_moonit_by_suffix( + &self, + decoded_event: &crate::KbDexDecodedEventDto, + ) -> Result, crate::KbError> { + let mut token_mints = std::vec::Vec::new(); + if let Some(token_a_mint) = decoded_event.token_a_mint.clone() { + token_mints.push(token_a_mint); + } + if let Some(token_b_mint) = decoded_event.token_b_mint.clone() { + token_mints.push(token_b_mint); + } + let mut matched_token_option = None; + for token_mint in token_mints { + if token_mint.ends_with("moon") { + matched_token_option = Some(token_mint); + break; + } + } + let matched_token = match matched_token_option { + Some(matched_token) => matched_token, + None => return Ok(None), + }; + let surface_id_result = self.ensure_moonit_surface().await; + let surface_id = match surface_id_result { + Ok(surface_id) => surface_id, + Err(error) => return Err(error), + }; + let key_result = crate::get_launch_surface_key_by_match( + self.database.as_ref(), + "token_mint_suffix", + "moon", + ) + .await; + let key_option = match key_result { + Ok(key_option) => key_option, + Err(error) => return Err(error), + }; + let matched_key_id = match key_option { + Some(key) => key.id, + None => None, + }; + Ok(Some(KbMatchedLaunchSurface { + launch_surface_id: surface_id, + matched_key_id, + surface_code: "moonit".to_string(), + surface_name: "Moonit".to_string(), + match_kind: "token_mint_suffix".to_string(), + matched_value: matched_token, + })) + } } #[derive(Debug, Clone)] @@ -357,9 +587,7 @@ mod tests { std::sync::Arc::new(database) } - async fn seed_fun_launch_registry( - database: std::sync::Arc, - ) { + async fn seed_fun_launch_registry(database: std::sync::Arc) { let surface_id_result = crate::upsert_launch_surface( database.as_ref(), &crate::KbLaunchSurfaceDto::new( @@ -469,8 +697,11 @@ mod tests { }; assert_eq!(results.len(), 1); assert!(results[0].created_attribution); - let list_result = - crate::list_launch_attributions_by_pool_id(database.as_ref(), results[0].pool_id.unwrap()).await; + let list_result = crate::list_launch_attributions_by_pool_id( + database.as_ref(), + results[0].pool_id.unwrap(), + ) + .await; let listed = match list_result { Ok(listed) => listed, Err(error) => panic!("launch attribution list must succeed: {}", error), @@ -479,4 +710,171 @@ mod tests { assert_eq!(listed[0].match_kind, "config_account".to_string()); assert_eq!(listed[0].matched_value, "DbcDetectConfig111".to_string()); } + + async fn seed_decoded_meteora_damm_v2_event_with_moon_token( + database: std::sync::Arc, + signature: &str, + ) { + let transaction_model = crate::KbTransactionModelService::new(database.clone()); + let dex_decode = crate::KbDexDecodeService::new(database.clone()); + let dex_detect = crate::KbDexDetectService::new(database); + let resolved_transaction = serde_json::json!({ + "slot": 910105, + "blockTime": 1779100105, + "version": 0, + "transaction": { + "message": { + "instructions": [ + { + "programId": crate::KB_METEORA_DAMM_V2_PROGRAM_ID, + "program": "meteora-damm-v2", + "stackHeight": 1, + "accounts": [ + "MoonitDammV2Pool111", + "ExampleTokenmoon", + "So11111111111111111111111111111111111111112", + "MoonitDammV2Config111", + "MoonitCreator111" + ], + "parsed": { + "info": { + "instruction": "initialize_customizable_pool", + "pool": "MoonitDammV2Pool111", + "tokenAMint": "ExampleTokenmoon", + "tokenBMint": "So11111111111111111111111111111111111111112", + "creator": "MoonitCreator111", + "isCustomizablePool": true + } + }, + "data": "opaque" + } + ] + } + }, + "meta": { + "err": null, + "logMessages": [ + "Program log: Instruction: InitializeCustomizablePool" + ] + } + }); + let project_result = transaction_model + .persist_resolved_transaction( + signature, + Some("helius_primary_http".to_string()), + &resolved_transaction, + ) + .await; + if let Err(error) = project_result { + panic!("projection must succeed: {}", error); + } + let decode_result = dex_decode.decode_transaction_by_signature(signature).await; + if let Err(error) = decode_result { + panic!("dex decode must succeed: {}", error); + } + let detect_result = dex_detect.detect_transaction_by_signature(signature).await; + if let Err(error) = detect_result { + panic!("dex detect must succeed: {}", error); + } + } + + async fn seed_bags_registry( + database: std::sync::Arc, + ) { + let service = crate::KbLaunchOriginService::new(database); + let register_result = service + .register_bags_pool_mapping( + "DbcDetectTokenA111", + Some("DbcDetectConfig111".to_string()), + Some("DbcDetectPool111".to_string()), + Some("BagsMigratedPool111".to_string()), + ) + .await; + if let Err(error) = register_result { + panic!("bags registry must succeed: {}", error); + } + } + + #[tokio::test] + async fn attribute_transaction_by_signature_detects_bags_from_registered_mapping() { + let database = make_database().await; + seed_bags_registry(database.clone()).await; + seed_decoded_meteora_dbc_event(database.clone(), "sig-launch-origin-bags-1").await; + let service = crate::KbLaunchOriginService::new(database.clone()); + let result = service + .attribute_transaction_by_signature("sig-launch-origin-bags-1") + .await; + let results = match result { + Ok(results) => results, + Err(error) => panic!("bags attribution must succeed: {}", error), + }; + assert_eq!(results.len(), 1); + assert!(results[0].created_attribution); + let surface_result = + crate::get_launch_surface_by_code(database.as_ref(), "bags").await; + let surface_option = match surface_result { + Ok(surface_option) => surface_option, + Err(error) => panic!("bags surface fetch must succeed: {}", error), + }; + let surface = match surface_option { + Some(surface) => surface, + None => panic!("bags surface must exist"), + }; + assert_eq!(surface.id, Some(results[0].launch_surface_id)); + let listed_result = crate::list_launch_attributions_by_pool_id( + database.as_ref(), + results[0].pool_id.unwrap(), + ) + .await; + let listed = match listed_result { + Ok(listed) => listed, + Err(error) => panic!("bags attribution list must succeed: {}", error), + }; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].match_kind, "config_account".to_string()); + assert_eq!(listed[0].matched_value, "DbcDetectConfig111".to_string()); + } + + #[tokio::test] + async fn attribute_transaction_by_signature_detects_moonit_from_token_suffix() { + let database = make_database().await; + seed_decoded_meteora_damm_v2_event_with_moon_token( + database.clone(), + "sig-launch-origin-moonit-1", + ) + .await; + let service = crate::KbLaunchOriginService::new(database.clone()); + let result = service + .attribute_transaction_by_signature("sig-launch-origin-moonit-1") + .await; + let results = match result { + Ok(results) => results, + Err(error) => panic!("moonit attribution must succeed: {}", error), + }; + assert_eq!(results.len(), 1); + assert!(results[0].created_attribution); + let surface_result = + crate::get_launch_surface_by_code(database.as_ref(), "moonit").await; + let surface_option = match surface_result { + Ok(surface_option) => surface_option, + Err(error) => panic!("moonit surface fetch must succeed: {}", error), + }; + let surface = match surface_option { + Some(surface) => surface, + None => panic!("moonit surface must exist"), + }; + assert_eq!(surface.id, Some(results[0].launch_surface_id)); + let listed_result = crate::list_launch_attributions_by_pool_id( + database.as_ref(), + results[0].pool_id.unwrap(), + ) + .await; + let listed = match listed_result { + Ok(listed) => listed, + Err(error) => panic!("moonit attribution list must succeed: {}", error), + }; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].match_kind, "token_mint_suffix".to_string()); + assert_eq!(listed[0].matched_value, "ExampleTokenmoon".to_string()); + } }