This commit is contained in:
2026-04-29 14:04:07 +02:00
parent e37f8415fb
commit f0831e4cd4
4 changed files with 443 additions and 46 deletions

View File

@@ -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 dun enregistrement programmatique des mappings Bags, et détection automatique Moonit via suffixe de token mint

View File

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

View File

@@ -578,15 +578,13 @@ Réalisé :
- conservation dune 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` à dautres 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 lextension future à dautres launchpads ou surfaces dérivées.
- extension de la couche `launch origins` à `Bags` et `Moonit`,
- ajout dun enregistrement programmatique des mappings `Bags` à partir des champs `tokenMint`, `dbcConfigKey`, `dbcPoolKey` et `dammV2PoolKey`,
- prise en charge de lattribution `Bags` par matching exact sur `config_account`, `pool_account` et `token_mint`,
- prise en charge de lattribution `Moonit` par détection automatique des token mints se terminant par `moon`,
- conservation dune 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 louverture des couches analytiques plus riches.

View File

@@ -30,7 +30,154 @@ impl KbLaunchOriginService {
/// Creates a new launch-origin service.
pub fn new(database: std::sync::Arc<crate::KbDatabase>) -> 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<i64, crate::KbError> {
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<i64, crate::KbError> {
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<std::string::String>,
dbc_pool_key: std::option::Option<std::string::String>,
damm_v2_pool_key: std::option::Option<std::string::String>,
) -> Result<i64, crate::KbError> {
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::<serde_json::Value>(
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 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),
};
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<std::option::Option<KbMatchedLaunchSurface>, 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 surface_id_option = surface.id;
let current_surface_id = match surface_id_option {
let current_surface_id = match surface.id {
Some(current_surface_id) => current_surface_id,
None => continue,
};
if current_surface_id != surface_id || !surface.is_enabled {
if current_surface_id != key.launch_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,
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<std::option::Option<KbMatchedLaunchSurface>, 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<crate::KbDatabase>,
) {
async fn seed_fun_launch_registry(database: std::sync::Arc<crate::KbDatabase>) {
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<crate::KbDatabase>,
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<crate::KbDatabase>,
) {
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());
}
}