From d7b03c91b9df7fbc8dcab9580fc6d214c2a4eeff Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 29 Apr 2026 07:40:44 +0200 Subject: [PATCH] 0.7.9 --- CHANGELOG.md | 1 + Cargo.toml | 2 +- ROADMAP.md | 17 +- kb_lib/src/db.rs | 18 + kb_lib/src/db/dtos.rs | 6 + kb_lib/src/db/dtos/launch_attribution.rs | 104 ++++ kb_lib/src/db/dtos/launch_surface.rs | 79 +++ kb_lib/src/db/dtos/launch_surface_key.rs | 74 +++ kb_lib/src/db/entities.rs | 6 + kb_lib/src/db/entities/launch_attribution.rs | 32 ++ kb_lib/src/db/entities/launch_surface.rs | 22 + kb_lib/src/db/entities/launch_surface_key.rs | 20 + kb_lib/src/db/queries.rs | 12 + kb_lib/src/db/queries/launch_attribution.rs | 181 +++++++ kb_lib/src/db/queries/launch_surface.rs | 153 ++++++ kb_lib/src/db/queries/launch_surface_key.rs | 153 ++++++ kb_lib/src/db/schema.rs | 139 ++++++ kb_lib/src/launch_origin.rs | 482 +++++++++++++++++++ kb_lib/src/lib.rs | 18 + kb_lib/src/tx_resolution.rs | 15 +- 20 files changed, 1523 insertions(+), 11 deletions(-) create mode 100644 kb_lib/src/db/dtos/launch_attribution.rs create mode 100644 kb_lib/src/db/dtos/launch_surface.rs create mode 100644 kb_lib/src/db/dtos/launch_surface_key.rs create mode 100644 kb_lib/src/db/entities/launch_attribution.rs create mode 100644 kb_lib/src/db/entities/launch_surface.rs create mode 100644 kb_lib/src/db/entities/launch_surface_key.rs create mode 100644 kb_lib/src/db/queries/launch_attribution.rs create mode 100644 kb_lib/src/db/queries/launch_surface.rs create mode 100644 kb_lib/src/db/queries/launch_surface_key.rs create mode 100644 kb_lib/src/launch_origin.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e69b36..9540128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,3 +39,4 @@ 0.7.6 - Ajout du premier support Meteora DBC avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing 0.7.7 - Ajout du premier support Meteora DAMM v2 avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing 0.7.8 - Ajout du premier support Meteora DAMM v1 avec décodage create_pool/swap, persistance des événements décodés et détection métier automatique pool/pair/listing +0.7.9 - Ajout d’un registre des surfaces de lancement, d’une attribution automatique des pools détectés à une origine de lancement, et d’un premier support Meteora Fun Launch au-dessus de Meteora DBC / DAMM diff --git a/Cargo.toml b/Cargo.toml index 0ea9857..efa40f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.7.8" +version = "0.7.9" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/ROADMAP.md b/ROADMAP.md index ecf2c1f..317c967 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -537,16 +537,15 @@ Réalisé : - ajout de la détection métier `Meteora DAMM v1` vers `pool / pair / listing`, - préparation du rattachement futur entre `Meteora DBC` et `Meteora DAMM v1`. -### 6.041. Version `0.7.9` — LaunchLab / Fun Launch -Objectif : prendre en charge la couche de lancement construite autour de `Meteora DBC` et `Meteora DAMM v2`. +### 6.041. Version `0.7.9` — Launch origins / Fun Launch +Réalisé : -À faire : - -- ajouter un décodeur dédié `LaunchLab / Fun Launch` lorsque des signatures ou programmes propres sont observables, -- distinguer les événements de scaffolding / launchpad des événements `DBC` et `DAMM v2`, -- relier les lancements `LaunchLab / Fun Launch` aux pools et paires finalement créés, -- préparer le suivi des transitions entre lancement, bonding curve et AMM, -- éviter de confondre la couche launchpad avec les programmes DEX sous-jacents. +- ajout d’un registre des surfaces de lancement, +- ajout d’un registre de clés observables par surface de lancement, +- ajout d’une attribution entre événements/pools détectés et surfaces connues, +- premier support de `Meteora Fun Launch` comme surface d’origine au-dessus de `Meteora DBC`, +- branchement automatique de l’attribution depuis le pipeline de résolution transactionnelle, +- conservation d’une séparation stricte entre protocole on-chain et origine de lancement. ### 6.042. Version `0.7.10` — 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/db.rs b/kb_lib/src/db.rs index db2c7c3..5a541d6 100644 --- a/kb_lib/src/db.rs +++ b/kb_lib/src/db.rs @@ -121,3 +121,21 @@ pub use types::KbPoolKind; pub use types::KbPoolStatus; pub use types::KbPoolTokenRole; pub use types::KbSwapTradeSide; + +pub use dtos::KbLaunchSurfaceDto; +pub use dtos::KbLaunchSurfaceKeyDto; +pub use dtos::KbLaunchAttributionDto; + +pub use entities::KbLaunchSurfaceEntity; +pub use entities::KbLaunchSurfaceKeyEntity; +pub use entities::KbLaunchAttributionEntity; + +pub use queries::upsert_launch_surface; +pub use queries::get_launch_surface_by_code; +pub use queries::list_launch_surfaces; +pub use queries::upsert_launch_surface_key; +pub use queries::get_launch_surface_key_by_match; +pub use queries::list_launch_surface_keys_by_surface_id; +pub use queries::upsert_launch_attribution; +pub use queries::get_launch_attribution_by_decoded_event_id; +pub use queries::list_launch_attributions_by_pool_id; diff --git a/kb_lib/src/db/dtos.rs b/kb_lib/src/db/dtos.rs index b580619..89e2bbe 100644 --- a/kb_lib/src/db/dtos.rs +++ b/kb_lib/src/db/dtos.rs @@ -23,6 +23,9 @@ mod chain_instruction; mod chain_slot; mod chain_transaction; mod dex_decoded_event; +mod launch_surface; +mod launch_surface_key; +mod launch_attribution; pub use analysis_signal::KbAnalysisSignalDto; pub use db_metadata::KbDbMetadataDto; @@ -45,3 +48,6 @@ pub use chain_instruction::KbChainInstructionDto; pub use chain_slot::KbChainSlotDto; pub use chain_transaction::KbChainTransactionDto; pub use dex_decoded_event::KbDexDecodedEventDto; +pub use launch_surface::KbLaunchSurfaceDto; +pub use launch_surface_key::KbLaunchSurfaceKeyDto; +pub use launch_attribution::KbLaunchAttributionDto; diff --git a/kb_lib/src/db/dtos/launch_attribution.rs b/kb_lib/src/db/dtos/launch_attribution.rs new file mode 100644 index 0000000..2c57f47 --- /dev/null +++ b/kb_lib/src/db/dtos/launch_attribution.rs @@ -0,0 +1,104 @@ +// file: kb_lib/src/db/dtos/launch_attribution.rs + +//! Launch attribution DTO. + +/// Application-facing launch attribution DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbLaunchAttributionDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related launch surface id. + pub launch_surface_id: i64, + /// Related transaction id. + pub transaction_id: i64, + /// Related decoded event id. + pub decoded_event_id: i64, + /// Optional related pool id. + pub pool_id: std::option::Option, + /// Optional related pair id. + pub pair_id: std::option::Option, + /// Optional related matched key id. + pub matched_key_id: std::option::Option, + /// Decoded protocol name. + pub protocol_name: std::string::String, + /// Match kind used for attribution. + pub match_kind: std::string::String, + /// Matched value used for attribution. + pub matched_value: std::string::String, + /// Attribution timestamp. + pub attributed_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbLaunchAttributionDto { + /// Creates a new launch attribution DTO. + pub fn new( + launch_surface_id: i64, + transaction_id: i64, + decoded_event_id: i64, + pool_id: std::option::Option, + pair_id: std::option::Option, + matched_key_id: std::option::Option, + protocol_name: std::string::String, + match_kind: std::string::String, + matched_value: std::string::String, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + launch_surface_id, + transaction_id, + decoded_event_id, + pool_id, + pair_id, + matched_key_id, + protocol_name, + match_kind, + matched_value, + attributed_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbLaunchAttributionDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbLaunchAttributionEntity) -> Result { + let attributed_at_result = chrono::DateTime::parse_from_rfc3339(&entity.attributed_at); + let attributed_at = match attributed_at_result { + Ok(attributed_at) => attributed_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_attribution attributed_at '{}': {}", + entity.attributed_at, error + ))); + } + }; + let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at); + let updated_at = match updated_at_result { + Ok(updated_at) => updated_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_attribution updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + launch_surface_id: entity.launch_surface_id, + transaction_id: entity.transaction_id, + decoded_event_id: entity.decoded_event_id, + pool_id: entity.pool_id, + pair_id: entity.pair_id, + matched_key_id: entity.matched_key_id, + protocol_name: entity.protocol_name, + match_kind: entity.match_kind, + matched_value: entity.matched_value, + attributed_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/launch_surface.rs b/kb_lib/src/db/dtos/launch_surface.rs new file mode 100644 index 0000000..ab782bb --- /dev/null +++ b/kb_lib/src/db/dtos/launch_surface.rs @@ -0,0 +1,79 @@ +// file: kb_lib/src/db/dtos/launch_surface.rs + +//! Launch surface DTO. + +/// Application-facing launch surface DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbLaunchSurfaceDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Stable short code. + pub code: std::string::String, + /// Display name. + pub name: std::string::String, + /// Optional protocol family hint. + pub protocol_family: std::option::Option, + /// Whether the surface is enabled. + pub is_enabled: bool, + /// Creation timestamp. + pub created_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbLaunchSurfaceDto { + /// Creates a new launch surface DTO. + pub fn new( + code: std::string::String, + name: std::string::String, + protocol_family: std::option::Option, + is_enabled: bool, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + code, + name, + protocol_family, + is_enabled, + created_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbLaunchSurfaceDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbLaunchSurfaceEntity) -> Result { + let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at); + let created_at = match created_at_result { + Ok(created_at) => created_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_surface created_at '{}': {}", + entity.created_at, error + ))); + } + }; + let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at); + let updated_at = match updated_at_result { + Ok(updated_at) => updated_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_surface updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + code: entity.code, + name: entity.name, + protocol_family: entity.protocol_family, + is_enabled: entity.is_enabled != 0, + created_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/launch_surface_key.rs b/kb_lib/src/db/dtos/launch_surface_key.rs new file mode 100644 index 0000000..1aef978 --- /dev/null +++ b/kb_lib/src/db/dtos/launch_surface_key.rs @@ -0,0 +1,74 @@ +// file: kb_lib/src/db/dtos/launch_surface_key.rs + +//! Launch surface key DTO. + +/// Application-facing launch surface key DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbLaunchSurfaceKeyDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related launch surface id. + pub launch_surface_id: i64, + /// Stable key kind. + pub match_kind: std::string::String, + /// Stable key value. + pub match_value: std::string::String, + /// Creation timestamp. + pub created_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbLaunchSurfaceKeyDto { + /// Creates a new launch surface key DTO. + pub fn new( + launch_surface_id: i64, + match_kind: std::string::String, + match_value: std::string::String, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + launch_surface_id, + match_kind, + match_value, + created_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbLaunchSurfaceKeyDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbLaunchSurfaceKeyEntity) -> Result { + let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at); + let created_at = match created_at_result { + Ok(created_at) => created_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_surface_key created_at '{}': {}", + entity.created_at, error + ))); + } + }; + let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at); + let updated_at = match updated_at_result { + Ok(updated_at) => updated_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse launch_surface_key updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + launch_surface_id: entity.launch_surface_id, + match_kind: entity.match_kind, + match_value: entity.match_value, + created_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/entities.rs b/kb_lib/src/db/entities.rs index c73ea3b..7c8dde8 100644 --- a/kb_lib/src/db/entities.rs +++ b/kb_lib/src/db/entities.rs @@ -25,6 +25,9 @@ mod chain_instruction; mod chain_slot; mod chain_transaction; mod dex_decoded_event; +mod launch_surface; +mod launch_surface_key; +mod launch_attribution; pub use analysis_signal::KbAnalysisSignalEntity; pub use db_metadata::KbDbMetadataEntity; @@ -47,3 +50,6 @@ pub use chain_instruction::KbChainInstructionEntity; pub use chain_slot::KbChainSlotEntity; pub use chain_transaction::KbChainTransactionEntity; pub use dex_decoded_event::KbDexDecodedEventEntity; +pub use launch_surface::KbLaunchSurfaceEntity; +pub use launch_surface_key::KbLaunchSurfaceKeyEntity; +pub use launch_attribution::KbLaunchAttributionEntity; diff --git a/kb_lib/src/db/entities/launch_attribution.rs b/kb_lib/src/db/entities/launch_attribution.rs new file mode 100644 index 0000000..5e1174f --- /dev/null +++ b/kb_lib/src/db/entities/launch_attribution.rs @@ -0,0 +1,32 @@ +// file: kb_lib/src/db/entities/launch_attribution.rs + +//! Launch attribution entity. + +/// Persisted launch attribution row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbLaunchAttributionEntity { + /// Numeric primary key. + pub id: i64, + /// Related launch surface id. + pub launch_surface_id: i64, + /// Related transaction id. + pub transaction_id: i64, + /// Related decoded event id. + pub decoded_event_id: i64, + /// Optional related pool id. + pub pool_id: std::option::Option, + /// Optional related pair id. + pub pair_id: std::option::Option, + /// Optional related matched key id. + pub matched_key_id: std::option::Option, + /// Decoded protocol name. + pub protocol_name: std::string::String, + /// Match kind used for attribution. + pub match_kind: std::string::String, + /// Matched value used for attribution. + pub matched_value: std::string::String, + /// Attribution timestamp encoded as RFC3339 UTC text. + pub attributed_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/launch_surface.rs b/kb_lib/src/db/entities/launch_surface.rs new file mode 100644 index 0000000..a25f5f2 --- /dev/null +++ b/kb_lib/src/db/entities/launch_surface.rs @@ -0,0 +1,22 @@ +// file: kb_lib/src/db/entities/launch_surface.rs + +//! Launch surface entity. + +/// Persisted launch surface row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbLaunchSurfaceEntity { + /// Numeric primary key. + pub id: i64, + /// Stable short code. + pub code: std::string::String, + /// Display name. + pub name: std::string::String, + /// Optional protocol family hint. + pub protocol_family: std::option::Option, + /// Whether the surface is enabled. + pub is_enabled: i64, + /// Creation timestamp encoded as RFC3339 UTC text. + pub created_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/launch_surface_key.rs b/kb_lib/src/db/entities/launch_surface_key.rs new file mode 100644 index 0000000..cea1507 --- /dev/null +++ b/kb_lib/src/db/entities/launch_surface_key.rs @@ -0,0 +1,20 @@ +// file: kb_lib/src/db/entities/launch_surface_key.rs + +//! Launch surface key entity. + +/// Persisted launch surface matching key row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbLaunchSurfaceKeyEntity { + /// Numeric primary key. + pub id: i64, + /// Related launch surface id. + pub launch_surface_id: i64, + /// Stable key kind, for example `config_account` or `creator`. + pub match_kind: std::string::String, + /// Stable key value. + pub match_value: std::string::String, + /// Creation timestamp encoded as RFC3339 UTC text. + pub created_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs index 50abfcc..02fbd06 100644 --- a/kb_lib/src/db/queries.rs +++ b/kb_lib/src/db/queries.rs @@ -27,6 +27,9 @@ mod chain_instruction; mod chain_slot; mod chain_transaction; mod dex_decoded_event; +mod launch_surface; +mod launch_surface_key; +mod launch_attribution; pub use analysis_signal::insert_analysis_signal; pub use analysis_signal::list_recent_analysis_signals; @@ -82,3 +85,12 @@ pub use chain_transaction::get_chain_transaction_by_signature; pub use dex_decoded_event::get_dex_decoded_event_by_key; pub use dex_decoded_event::list_dex_decoded_events_by_transaction_id; pub use dex_decoded_event::upsert_dex_decoded_event; +pub use launch_surface::upsert_launch_surface; +pub use launch_surface::get_launch_surface_by_code; +pub use launch_surface::list_launch_surfaces; +pub use launch_surface_key::upsert_launch_surface_key; +pub use launch_surface_key::get_launch_surface_key_by_match; +pub use launch_surface_key::list_launch_surface_keys_by_surface_id; +pub use launch_attribution::upsert_launch_attribution; +pub use launch_attribution::get_launch_attribution_by_decoded_event_id; +pub use launch_attribution::list_launch_attributions_by_pool_id; diff --git a/kb_lib/src/db/queries/launch_attribution.rs b/kb_lib/src/db/queries/launch_attribution.rs new file mode 100644 index 0000000..9aa25c1 --- /dev/null +++ b/kb_lib/src/db/queries/launch_attribution.rs @@ -0,0 +1,181 @@ +// file: kb_lib/src/db/queries/launch_attribution.rs + +//! Queries for `kb_launch_attributions`. + +/// Inserts or updates one launch attribution row and returns its stable internal id. +pub async fn upsert_launch_attribution( + database: &crate::KbDatabase, + dto: &crate::KbLaunchAttributionDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_launch_attributions ( + launch_surface_id, + transaction_id, + decoded_event_id, + pool_id, + pair_id, + matched_key_id, + protocol_name, + match_kind, + matched_value, + attributed_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(decoded_event_id) DO UPDATE SET + launch_surface_id = excluded.launch_surface_id, + transaction_id = excluded.transaction_id, + pool_id = excluded.pool_id, + pair_id = excluded.pair_id, + matched_key_id = excluded.matched_key_id, + protocol_name = excluded.protocol_name, + match_kind = excluded.match_kind, + matched_value = excluded.matched_value, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.launch_surface_id) + .bind(dto.transaction_id) + .bind(dto.decoded_event_id) + .bind(dto.pool_id) + .bind(dto.pair_id) + .bind(dto.matched_key_id) + .bind(dto.protocol_name.clone()) + .bind(dto.match_kind.clone()) + .bind(dto.matched_value.clone()) + .bind(dto.attributed_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_launch_attributions on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_launch_attributions +WHERE decoded_event_id = ? +LIMIT 1 + "#, + ) + .bind(dto.decoded_event_id) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_launch_attributions id for decoded_event_id '{}' on sqlite: {}", + dto.decoded_event_id, error + ))), + } + } + } +} + +/// Returns one launch attribution identified by its decoded-event id, if it exists. +pub async fn get_launch_attribution_by_decoded_event_id( + database: &crate::KbDatabase, + decoded_event_id: i64, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + launch_surface_id, + transaction_id, + decoded_event_id, + pool_id, + pair_id, + matched_key_id, + protocol_name, + match_kind, + matched_value, + attributed_at, + updated_at +FROM kb_launch_attributions +WHERE decoded_event_id = ? +LIMIT 1 + "#, + ) + .bind(decoded_event_id) + .fetch_optional(pool) + .await; + let entity_option = match query_result { + Ok(entity_option) => entity_option, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot read kb_launch_attributions by decoded_event_id '{}' on sqlite: {}", + decoded_event_id, error + ))); + } + }; + match entity_option { + Some(entity) => crate::KbLaunchAttributionDto::try_from(entity).map(Some), + None => Ok(None), + } + } + } +} + +/// Lists all launch attributions attached to one pool id. +pub async fn list_launch_attributions_by_pool_id( + database: &crate::KbDatabase, + pool_id: i64, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + launch_surface_id, + transaction_id, + decoded_event_id, + pool_id, + pair_id, + matched_key_id, + protocol_name, + match_kind, + matched_value, + attributed_at, + updated_at +FROM kb_launch_attributions +WHERE pool_id = ? +ORDER BY attributed_at ASC, id ASC + "#, + ) + .bind(pool_id) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list kb_launch_attributions by pool_id '{}' on sqlite: {}", + pool_id, error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbLaunchAttributionDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} diff --git a/kb_lib/src/db/queries/launch_surface.rs b/kb_lib/src/db/queries/launch_surface.rs new file mode 100644 index 0000000..879a118 --- /dev/null +++ b/kb_lib/src/db/queries/launch_surface.rs @@ -0,0 +1,153 @@ +// file: kb_lib/src/db/queries/launch_surface.rs + +//! Queries for `kb_launch_surfaces`. + +/// Inserts or updates one launch surface row and returns its stable internal id. +pub async fn upsert_launch_surface( + database: &crate::KbDatabase, + dto: &crate::KbLaunchSurfaceDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_launch_surfaces ( + code, + name, + protocol_family, + is_enabled, + created_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?) +ON CONFLICT(code) DO UPDATE SET + name = excluded.name, + protocol_family = excluded.protocol_family, + is_enabled = excluded.is_enabled, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.code.clone()) + .bind(dto.name.clone()) + .bind(dto.protocol_family.clone()) + .bind(if dto.is_enabled { 1_i64 } else { 0_i64 }) + .bind(dto.created_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_launch_surfaces on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_launch_surfaces +WHERE code = ? +LIMIT 1 + "#, + ) + .bind(dto.code.clone()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_launch_surfaces id for code '{}' on sqlite: {}", + dto.code, error + ))), + } + } + } +} + +/// Returns one launch surface identified by its stable short code, if it exists. +pub async fn get_launch_surface_by_code( + database: &crate::KbDatabase, + code: &str, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + code, + name, + protocol_family, + is_enabled, + created_at, + updated_at +FROM kb_launch_surfaces +WHERE code = ? +LIMIT 1 + "#, + ) + .bind(code) + .fetch_optional(pool) + .await; + let entity_option = match query_result { + Ok(entity_option) => entity_option, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot read kb_launch_surfaces '{}' on sqlite: {}", + code, error + ))); + } + }; + match entity_option { + Some(entity) => crate::KbLaunchSurfaceDto::try_from(entity).map(Some), + None => Ok(None), + } + } + } +} + +/// Lists all persisted launch surfaces ordered by code. +pub async fn list_launch_surfaces( + database: &crate::KbDatabase, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + code, + name, + protocol_family, + is_enabled, + created_at, + updated_at +FROM kb_launch_surfaces +ORDER BY code ASC + "#, + ) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list kb_launch_surfaces on sqlite: {}", + error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbLaunchSurfaceDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} diff --git a/kb_lib/src/db/queries/launch_surface_key.rs b/kb_lib/src/db/queries/launch_surface_key.rs new file mode 100644 index 0000000..f04444d --- /dev/null +++ b/kb_lib/src/db/queries/launch_surface_key.rs @@ -0,0 +1,153 @@ +// file: kb_lib/src/db/queries/launch_surface_key.rs + +//! Queries for `kb_launch_surface_keys`. + +/// Inserts or updates one launch-surface matching key and returns its stable internal id. +pub async fn upsert_launch_surface_key( + database: &crate::KbDatabase, + dto: &crate::KbLaunchSurfaceKeyDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_launch_surface_keys ( + launch_surface_id, + match_kind, + match_value, + created_at, + updated_at +) +VALUES (?, ?, ?, ?, ?) +ON CONFLICT(match_kind, match_value) DO UPDATE SET + launch_surface_id = excluded.launch_surface_id, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.launch_surface_id) + .bind(dto.match_kind.clone()) + .bind(dto.match_value.clone()) + .bind(dto.created_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_launch_surface_keys on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_launch_surface_keys +WHERE match_kind = ? AND match_value = ? +LIMIT 1 + "#, + ) + .bind(dto.match_kind.clone()) + .bind(dto.match_value.clone()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_launch_surface_keys id for '{}:{}' on sqlite: {}", + dto.match_kind, dto.match_value, error + ))), + } + } + } +} + +/// Returns one launch-surface matching key identified by its kind and value, if it exists. +pub async fn get_launch_surface_key_by_match( + database: &crate::KbDatabase, + match_kind: &str, + match_value: &str, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + launch_surface_id, + match_kind, + match_value, + created_at, + updated_at +FROM kb_launch_surface_keys +WHERE match_kind = ? AND match_value = ? +LIMIT 1 + "#, + ) + .bind(match_kind) + .bind(match_value) + .fetch_optional(pool) + .await; + let entity_option = match query_result { + Ok(entity_option) => entity_option, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot read kb_launch_surface_keys '{}:{}' on sqlite: {}", + match_kind, match_value, error + ))); + } + }; + match entity_option { + Some(entity) => crate::KbLaunchSurfaceKeyDto::try_from(entity).map(Some), + None => Ok(None), + } + } + } +} + +/// Lists all launch-surface matching keys attached to one launch surface id. +pub async fn list_launch_surface_keys_by_surface_id( + database: &crate::KbDatabase, + launch_surface_id: i64, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = + sqlx::query_as::( + r#" +SELECT + id, + launch_surface_id, + match_kind, + match_value, + created_at, + updated_at +FROM kb_launch_surface_keys +WHERE launch_surface_id = ? +ORDER BY match_kind ASC, match_value ASC + "#, + ) + .bind(launch_surface_id) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list kb_launch_surface_keys by surface_id '{}' on sqlite: {}", + launch_surface_id, error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbLaunchSurfaceKeyDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} diff --git a/kb_lib/src/db/schema.rs b/kb_lib/src/db/schema.rs index 90b1037..72a2f34 100644 --- a/kb_lib/src/db/schema.rs +++ b/kb_lib/src/db/schema.rs @@ -230,6 +230,30 @@ pub(crate) async fn ensure_schema(database: &crate::KbDatabase) -> Result<(), cr if let Err(error) = result { return Err(error); } + let result = create_kb_launch_surfaces_table(pool).await; + if let Err(error) = result { + return Err(error); + } + let result = create_kb_launch_surface_keys_table(pool).await; + if let Err(error) = result { + return Err(error); + } + let result = create_kb_idx_launch_surface_keys_surface_id(pool).await; + if let Err(error) = result { + return Err(error); + } + let result = create_kb_launch_attributions_table(pool).await; + if let Err(error) = result { + return Err(error); + } + let result = create_kb_idx_launch_attributions_surface_id(pool).await; + if let Err(error) = result { + return Err(error); + } + let result = create_kb_idx_launch_attributions_pool_id(pool).await; + if let Err(error) = result { + return Err(error); + } Ok(()) } } @@ -1253,3 +1277,118 @@ ON kb_dex_decoded_events (transaction_id, instruction_id, event_kind) ) .await } + +async fn create_kb_launch_surfaces_table(pool: &sqlx::SqlitePool) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_launch_surfaces_table", + r#" +CREATE TABLE IF NOT EXISTS kb_launch_surfaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + protocol_family TEXT NULL, + is_enabled INTEGER NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) + "#, + ) + .await +} + +async fn create_kb_launch_surface_keys_table( + pool: &sqlx::SqlitePool, +) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_launch_surface_keys_table", + r#" +CREATE TABLE IF NOT EXISTS kb_launch_surface_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + launch_surface_id INTEGER NOT NULL, + match_kind TEXT NOT NULL, + match_value TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(launch_surface_id) REFERENCES kb_launch_surfaces(id) ON DELETE CASCADE, + UNIQUE(match_kind, match_value) +) + "#, + ) + .await +} + +async fn create_kb_idx_launch_surface_keys_surface_id( + pool: &sqlx::SqlitePool, +) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_idx_launch_surface_keys_surface_id", + r#" +CREATE INDEX IF NOT EXISTS kb_idx_launch_surface_keys_surface_id +ON kb_launch_surface_keys(launch_surface_id) + "#, + ) + .await +} + +async fn create_kb_launch_attributions_table( + pool: &sqlx::SqlitePool, +) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_launch_attributions_table", + r#" +CREATE TABLE IF NOT EXISTS kb_launch_attributions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + launch_surface_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + decoded_event_id INTEGER NOT NULL UNIQUE, + pool_id INTEGER NULL, + pair_id INTEGER NULL, + matched_key_id INTEGER NULL, + protocol_name TEXT NOT NULL, + match_kind TEXT NOT NULL, + matched_value TEXT NOT NULL, + attributed_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(launch_surface_id) REFERENCES kb_launch_surfaces(id) ON DELETE CASCADE, + FOREIGN KEY(transaction_id) REFERENCES kb_chain_transactions(id) ON DELETE CASCADE, + FOREIGN KEY(decoded_event_id) REFERENCES kb_dex_decoded_events(id) ON DELETE CASCADE, + FOREIGN KEY(pool_id) REFERENCES kb_pools(id) ON DELETE SET NULL, + FOREIGN KEY(pair_id) REFERENCES kb_pairs(id) ON DELETE SET NULL, + FOREIGN KEY(matched_key_id) REFERENCES kb_launch_surface_keys(id) ON DELETE SET NULL +) + "#, + ) + .await +} + +async fn create_kb_idx_launch_attributions_surface_id( + pool: &sqlx::SqlitePool, +) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_idx_launch_attributions_surface_id", + r#" +CREATE INDEX IF NOT EXISTS kb_idx_launch_attributions_surface_id +ON kb_launch_attributions(launch_surface_id) + "#, + ) + .await +} + +async fn create_kb_idx_launch_attributions_pool_id( + pool: &sqlx::SqlitePool, +) -> Result<(), crate::KbError> { + execute_sqlite_schema_statement( + pool, + "create_kb_idx_launch_attributions_pool_id", + r#" +CREATE INDEX IF NOT EXISTS kb_idx_launch_attributions_pool_id +ON kb_launch_attributions(pool_id) + "#, + ) + .await +} diff --git a/kb_lib/src/launch_origin.rs b/kb_lib/src/launch_origin.rs new file mode 100644 index 0000000..a1c0ceb --- /dev/null +++ b/kb_lib/src/launch_origin.rs @@ -0,0 +1,482 @@ +// file: kb_lib/src/launch_origin.rs + +//! Launch surface attribution service. + +/// Result of one launch surface attribution. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbLaunchAttributionResult { + /// Decoded event id. + pub decoded_event_id: i64, + /// Launch surface id. + pub launch_surface_id: i64, + /// Launch attribution id. + pub launch_attribution_id: i64, + /// Optional related pool id. + pub pool_id: std::option::Option, + /// Optional related pair id. + pub pair_id: std::option::Option, + /// Whether the attribution was newly created. + pub created_attribution: bool, +} + +/// Launch surface attribution service. +#[derive(Debug, Clone)] +pub struct KbLaunchOriginService { + database: std::sync::Arc, + persistence: crate::KbDetectionPersistenceService, +} + +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 } + } + + /// Attributes one transaction to known launch surfaces from decoded events. + pub async fn attribute_transaction_by_signature( + &self, + signature: &str, + ) -> Result, crate::KbError> { + let transaction_result = + crate::get_chain_transaction_by_signature(self.database.as_ref(), signature).await; + let transaction_option = match transaction_result { + Ok(transaction_option) => transaction_option, + Err(error) => return Err(error), + }; + let transaction = match transaction_option { + Some(transaction) => transaction, + None => { + return Err(crate::KbError::InvalidState(format!( + "cannot attribute unknown transaction '{}'", + signature + ))); + } + }; + let transaction_id = match transaction.id { + Some(transaction_id) => transaction_id, + None => { + return Err(crate::KbError::InvalidState(format!( + "transaction '{}' has no internal id", + signature + ))); + } + }; + let decoded_events_result = crate::list_dex_decoded_events_by_transaction_id( + self.database.as_ref(), + transaction_id, + ) + .await; + let decoded_events = match decoded_events_result { + Ok(decoded_events) => decoded_events, + Err(error) => return Err(error), + }; + 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 = match candidate_result { + Ok(candidate) => candidate, + Err(error) => return Err(error), + }; + let candidate = match candidate { + Some(candidate) => candidate, + None => continue, + }; + let decoded_event_id = match decoded_event.id { + Some(decoded_event_id) => decoded_event_id, + None => { + return Err(crate::KbError::InvalidState( + "decoded event has no internal id".to_string(), + )); + } + }; + let pool_id = match decoded_event.pool_account.clone() { + Some(pool_address) => { + let pool_result = + crate::get_pool_by_address(self.database.as_ref(), pool_address.as_str()) + .await; + let pool_option = match pool_result { + Ok(pool_option) => pool_option, + Err(error) => return Err(error), + }; + match pool_option { + Some(pool) => pool.id, + None => None, + } + } + None => None, + }; + let pair_id = match pool_id { + Some(pool_id) => { + let pair_result = + crate::get_pair_by_pool_id(self.database.as_ref(), pool_id).await; + let pair_option = match pair_result { + Ok(pair_option) => pair_option, + Err(error) => return Err(error), + }; + match pair_option { + Some(pair) => pair.id, + None => None, + } + } + None => None, + }; + let existing_result = crate::get_launch_attribution_by_decoded_event_id( + self.database.as_ref(), + decoded_event_id, + ) + .await; + let existing_option = match existing_result { + Ok(existing_option) => existing_option, + Err(error) => return Err(error), + }; + let created_attribution = existing_option.is_none(); + let attribution_id = if let Some(existing) = existing_option { + match existing.id { + Some(existing_id) => existing_id, + None => { + return Err(crate::KbError::InvalidState( + "launch attribution has no internal id".to_string(), + )); + } + } + } else { + let dto = crate::KbLaunchAttributionDto::new( + candidate.launch_surface_id, + transaction_id, + decoded_event_id, + pool_id, + pair_id, + candidate.matched_key_id, + decoded_event.protocol_name.clone(), + candidate.match_kind.clone(), + candidate.matched_value.clone(), + ); + let insert_result = + crate::upsert_launch_attribution(self.database.as_ref(), &dto).await; + let attribution_id = match insert_result { + Ok(attribution_id) => attribution_id, + Err(error) => return Err(error), + }; + let payload = serde_json::json!({ + "surfaceCode": candidate.surface_code, + "surfaceName": candidate.surface_name, + "protocolName": decoded_event.protocol_name, + "eventKind": decoded_event.event_kind, + "matchKind": candidate.match_kind, + "matchedValue": candidate.matched_value, + "poolAccount": decoded_event.pool_account, + "transactionSignature": transaction.signature + }); + let observation_result = self + .persistence + .record_observation(&crate::KbDetectionObservationInput::new( + "launch_surface.attribution".to_string(), + crate::KbObservationSourceKind::Dex, + transaction.source_endpoint_name.clone(), + transaction.signature.clone(), + transaction.slot, + payload.clone(), + )) + .await; + let observation_id = match observation_result { + Ok(observation_id) => observation_id, + Err(error) => return Err(error), + }; + let signal_result = self + .persistence + .record_signal(&crate::KbDetectionSignalInput::new( + format!( + "signal.launch_surface.{}.detected", + candidate.surface_code + ), + crate::KbAnalysisSignalSeverity::Low, + transaction.signature.clone(), + Some(observation_id), + None, + payload, + )) + .await; + if let Err(error) = signal_result { + return Err(error); + } + attribution_id + }; + results.push(crate::KbLaunchAttributionResult { + decoded_event_id, + launch_surface_id: candidate.launch_surface_id, + launch_attribution_id: attribution_id, + pool_id, + pair_id, + created_attribution, + }); + } + Ok(results) + } + + async fn match_surface_for_decoded_event( + &self, + decoded_event: &crate::KbDexDecodedEventDto, + ) -> Result, crate::KbError> { + let mut candidates = std::vec::Vec::new(); + if let Some(config_account) = decoded_event.market_account.clone() { + candidates.push(("config_account".to_string(), config_account)); + } + let payload_result = serde_json::from_str::( + decoded_event.payload_json.as_str(), + ); + let payload = match payload_result { + Ok(payload) => Some(payload), + Err(_) => None, + }; + if let Some(payload) = payload.as_ref() { + let creator = kb_extract_payload_string( + payload, + &["creator", "poolCreator", "user", "owner"], + ); + if let Some(creator) = creator { + candidates.push(("creator".to_string(), creator)); + } + } + 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(), + matched_value.as_str(), + ) + .await; + let key_option = match key_result { + Ok(key_option) => key_option, + Err(error) => return Err(error), + }; + let key = match key_option { + 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, + 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, + })); + } + } + Ok(None) + } +} + +#[derive(Debug, Clone)] +struct KbMatchedLaunchSurface { + launch_surface_id: i64, + matched_key_id: std::option::Option, + surface_code: std::string::String, + surface_name: std::string::String, + match_kind: std::string::String, + matched_value: std::string::String, +} + +fn kb_extract_payload_string( + value: &serde_json::Value, + candidate_keys: &[&str], +) -> std::option::Option { + if let Some(object) = value.as_object() { + for candidate_key in candidate_keys { + let direct_option = object.get(*candidate_key); + if let Some(direct) = direct_option { + let text_option = direct.as_str(); + if let Some(text) = text_option { + return Some(text.to_string()); + } + } + } + for nested_value in object.values() { + let nested_result = kb_extract_payload_string(nested_value, candidate_keys); + if nested_result.is_some() { + return nested_result; + } + } + return None; + } + if let Some(array) = value.as_array() { + for nested_value in array { + let nested_result = kb_extract_payload_string(nested_value, candidate_keys); + if nested_result.is_some() { + return nested_result; + } + } + } + None +} + +#[cfg(test)] +mod tests { + async fn make_database() -> std::sync::Arc { + let tempdir_result = tempfile::tempdir(); + let tempdir = match tempdir_result { + Ok(tempdir) => tempdir, + Err(error) => panic!("tempdir must succeed: {}", error), + }; + let database_path = tempdir.path().join("launch_origin.sqlite3"); + let config = crate::KbDatabaseConfig { + enabled: true, + backend: crate::KbDatabaseBackend::Sqlite, + sqlite: crate::KbSqliteDatabaseConfig { + path: database_path.to_string_lossy().to_string(), + create_if_missing: true, + busy_timeout_ms: 5000, + max_connections: 1, + auto_initialize_schema: true, + use_wal: true, + }, + }; + let database_result = crate::KbDatabase::connect_and_initialize(&config).await; + let database = match database_result { + Ok(database) => database, + Err(error) => panic!("database init must succeed: {}", error), + }; + std::sync::Arc::new(database) + } + + async fn seed_fun_launch_registry( + database: std::sync::Arc, + ) { + let surface_id_result = crate::upsert_launch_surface( + database.as_ref(), + &crate::KbLaunchSurfaceDto::new( + "meteora_fun_launch".to_string(), + "Meteora Fun Launch".to_string(), + Some("meteora".to_string()), + true, + ), + ) + .await; + let surface_id = match surface_id_result { + Ok(surface_id) => surface_id, + Err(error) => panic!("launch surface upsert must succeed: {}", error), + }; + let key_result = crate::upsert_launch_surface_key( + database.as_ref(), + &crate::KbLaunchSurfaceKeyDto::new( + surface_id, + "config_account".to_string(), + "DbcDetectConfig111".to_string(), + ), + ) + .await; + if let Err(error) = key_result { + panic!("launch surface key upsert must succeed: {}", error); + } + } + + async fn seed_decoded_meteora_dbc_event( + 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": 910004, + "blockTime": 1779100004, + "version": 0, + "transaction": { + "message": { + "instructions": [ + { + "programId": crate::KB_METEORA_DBC_PROGRAM_ID, + "program": "meteora-dbc", + "stackHeight": 1, + "accounts": [ + "DbcDetectPool111", + "DbcDetectTokenA111", + "So11111111111111111111111111111111111111112", + "DbcDetectConfig111", + "DbcDetectCreator111" + ], + "parsed": { + "info": { + "pool": "DbcDetectPool111", + "baseMint": "DbcDetectTokenA111", + "quoteMint": "So11111111111111111111111111111111111111112", + "poolConfig": "DbcDetectConfig111", + "creator": "DbcDetectCreator111" + } + }, + "data": "opaque" + } + ] + } + }, + "meta": { + "err": null, + "logMessages": [ + "Program log: Instruction: CreatePool" + ] + } + }); + 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); + } + } + + #[tokio::test] + async fn attribute_transaction_by_signature_detects_meteora_fun_launch() { + let database = make_database().await; + seed_fun_launch_registry(database.clone()).await; + seed_decoded_meteora_dbc_event(database.clone(), "sig-launch-origin-1").await; + let service = crate::KbLaunchOriginService::new(database.clone()); + let result = service + .attribute_transaction_by_signature("sig-launch-origin-1") + .await; + let results = match result { + Ok(results) => results, + Err(error) => panic!("launch attribution must succeed: {}", error), + }; + 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 listed = match list_result { + Ok(listed) => listed, + Err(error) => panic!("launch 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()); + } +} diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index e3d01be..3ce55cd 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -26,6 +26,7 @@ mod tx_model; mod dex; mod dex_decode; mod dex_detect; +mod launch_origin; pub use constants::*; pub use error::KbError; @@ -130,6 +131,21 @@ pub use db::KbChainSlotEntity; pub use db::KbChainTransactionEntity; pub use db::KbDexDecodedEventDto; pub use db::KbDexDecodedEventEntity; +pub use db::KbLaunchSurfaceDto; +pub use db::KbLaunchSurfaceKeyDto; +pub use db::KbLaunchAttributionDto; +pub use db::KbLaunchSurfaceEntity; +pub use db::KbLaunchSurfaceKeyEntity; +pub use db::KbLaunchAttributionEntity; +pub use db::upsert_launch_surface; +pub use db::get_launch_surface_by_code; +pub use db::list_launch_surfaces; +pub use db::upsert_launch_surface_key; +pub use db::get_launch_surface_key_by_match; +pub use db::list_launch_surface_keys_by_surface_id; +pub use db::upsert_launch_attribution; +pub use db::get_launch_attribution_by_decoded_event_id; +pub use db::list_launch_attributions_by_pool_id; pub use db::delete_chain_instructions_by_transaction_id; pub use db::get_chain_slot; pub use db::get_chain_transaction_by_signature; @@ -230,3 +246,5 @@ pub use dex::KbMeteoraDammV2SwapDecoded; pub use dex_decode::KbDexDecodeService; pub use dex_detect::KbDexDetectService; pub use dex_detect::KbDexPoolDetectionResult; +pub use launch_origin::KbLaunchAttributionResult; +pub use launch_origin::KbLaunchOriginService; diff --git a/kb_lib/src/tx_resolution.rs b/kb_lib/src/tx_resolution.rs index 3cd7c8c..42cb363 100644 --- a/kb_lib/src/tx_resolution.rs +++ b/kb_lib/src/tx_resolution.rs @@ -101,6 +101,7 @@ pub struct KbTransactionResolutionService { transaction_model: crate::KbTransactionModelService, dex_decode_service: crate::KbDexDecodeService, dex_detect_service: crate::KbDexDetectService, + launch_origin_service: crate::KbLaunchOriginService, http_role: std::string::String, resolved_signatures: std::sync::Arc>>, @@ -116,13 +117,15 @@ impl KbTransactionResolutionService { let persistence = crate::KbDetectionPersistenceService::new(database.clone()); let transaction_model = crate::KbTransactionModelService::new(database.clone()); let dex_decode_service = crate::KbDexDecodeService::new(database.clone()); - let dex_detect_service = crate::KbDexDetectService::new(database); + let dex_detect_service = crate::KbDexDetectService::new(database.clone()); + let launch_origin_service = crate::KbLaunchOriginService::new(database.clone()); Self { http_pool, persistence, transaction_model, dex_decode_service, dex_detect_service, + launch_origin_service, http_role, resolved_signatures: std::sync::Arc::new(tokio::sync::Mutex::new( std::collections::HashSet::new(), @@ -309,6 +312,15 @@ impl KbTransactionResolutionService { .dex_detect_service .detect_transaction_by_signature(request.signature.as_str()) .await; + let launch_attributions_result = self + .launch_origin_service + .attribute_transaction_by_signature(request.signature.as_str()) + .await; + let launch_attributions = match launch_attributions_result { + Ok(launch_attributions) => launch_attributions, + Err(error) => return Err(error), + }; + let launch_attribution_count = launch_attributions.len(); let detection_results = match detection_results_result { Ok(detection_results) => detection_results, Err(error) => return Err(error), @@ -323,6 +335,7 @@ impl KbTransactionResolutionService { "projectedTransactionId": projected_transaction_id, "decodedEventCount": decoded_event_count, "detectedObjectCount": detected_object_count, + "launchAttributionCount": launch_attribution_count, "triggerPayload": request.trigger_payload.clone(), "transaction": transaction_value });