This commit is contained in:
2026-06-17 16:06:09 +02:00
parent be12f5810b
commit 319be14aa6
37 changed files with 7129 additions and 363 deletions

View File

@@ -26,11 +26,14 @@ pub use dtos::DexDecodedEventDto;
pub use dtos::DexDto;
pub use dtos::DexEventCoverageEntryDto;
pub use dtos::DexEventCoverageSummaryDto;
pub use dtos::FeeEventAmountDto;
pub use dtos::FeeEventDto;
pub use dtos::InstructionObservationDto;
pub use dtos::InstructionObservationSourceRow;
pub use dtos::KnownHttpEndpointDto;
pub use dtos::KnownWsEndpointDto;
pub use dtos::LaunchAttributionDto;
pub use dtos::LaunchEventUpsertInput;
pub use dtos::LaunchSurfaceDto;
pub use dtos::LaunchSurfaceKeyDto;
pub use dtos::LiquidityEventDto;
@@ -98,6 +101,7 @@ pub use entities::DexDecodedEventEntity;
pub use entities::DexEntity;
pub use entities::DexEventCoverageEntryEntity;
pub use entities::DexEventCoverageSummaryEntity;
pub use entities::FeeEventAmountEntity;
pub use entities::FeeEventEntity;
pub use entities::InstructionObservationEntity;
pub use entities::KnownHttpEndpointEntity;
@@ -160,17 +164,17 @@ pub use queries::query_dex_decoded_events_delete_by_key;
pub use queries::query_dex_decoded_events_delete_instruction_audit_by_discriminator;
pub use queries::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id;
pub use queries::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
pub use queries::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator;
pub use queries::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit;
pub use queries::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits;
pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
pub use queries::query_dex_decoded_events_delete_related_instruction_audit;
pub use queries::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
pub use queries::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits;
pub use queries::query_dex_decoded_events_get_by_key;
pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use queries::query_dex_decoded_events_list_by_transaction_id;
pub use queries::query_dex_decoded_events_upsert;
pub use queries::query_dex_decoded_events_update_payload_json_by_id;
pub use queries::query_dex_decoded_events_upsert;
pub use queries::query_dex_event_coverage_entries_delete_by_decoder;
pub use queries::query_dex_event_coverage_entries_list_by_decoder;
pub use queries::query_dex_event_coverage_entries_list_summary_by_decoder;
@@ -180,6 +184,11 @@ pub use queries::query_dex_event_coverage_entries_upsert;
pub use queries::query_dexs_get_by_code;
pub use queries::query_dexs_list;
pub use queries::query_dexs_upsert;
pub use queries::query_fee_event_amounts_backfill_single_leg_from_fee_events;
pub use queries::query_fee_event_amounts_delete_by_fee_event_id;
pub use queries::query_fee_event_amounts_list_by_fee_event_id;
pub use queries::query_fee_event_amounts_replace_for_fee_event;
pub use queries::query_fee_events_upsert_with_amount_legs;
pub use queries::query_fee_events_get_by_decoded_event_id;
pub use queries::query_fee_events_list_recent;
pub use queries::query_fee_events_upsert;
@@ -187,7 +196,6 @@ pub use queries::query_instruction_observation_source_rows_list_by_signature;
pub use queries::query_instruction_observation_source_rows_list_recent;
pub use queries::query_instruction_observation_source_rows_list_replay_window;
pub use queries::query_instruction_observations_delete_by_transaction_ids;
pub use dtos::InstructionObservationSourceRow;
pub use queries::query_instruction_observations_list_by_filter;
pub use queries::query_instruction_observations_upsert;
pub use queries::query_known_http_endpoints_get;
@@ -200,7 +208,6 @@ pub use queries::query_launch_attributions_get_by_decoded_event_id;
pub use queries::query_launch_attributions_list_by_pool_id;
pub use queries::query_launch_attributions_upsert;
pub use queries::query_launch_events_upsert;
pub use dtos::LaunchEventUpsertInput;
pub use queries::query_launch_surface_keys_get_by_match;
pub use queries::query_launch_surface_keys_list_by_surface_id;
pub use queries::query_launch_surface_keys_upsert;

View File

@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event;
mod dex_event_coverage_entry;
mod fee_event;
mod fee_event_amount;
mod instruction_observation;
mod known_http_endpoint;
mod known_ws_endpoint;
@@ -88,6 +89,7 @@ pub use dex_decoded_event::DexDecodedEventDto;
pub use dex_event_coverage_entry::DexEventCoverageEntryDto;
pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
pub use fee_event::FeeEventDto;
pub use fee_event_amount::FeeEventAmountDto;
pub use instruction_observation::InstructionObservationDto;
pub use instruction_observation::InstructionObservationSourceRow;
pub use known_http_endpoint::KnownHttpEndpointDto;

View File

@@ -0,0 +1,110 @@
// file: kb_lib/src/db/dtos/fee_event_amount.rs
//! Fee event amount DTO.
/// Application-facing normalized fee event amount leg DTO.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FeeEventAmountDto {
/// Optional numeric primary key.
pub id: std::option::Option<i64>,
/// Related parent fee event id.
pub fee_event_id: i64,
/// Related transaction id.
pub transaction_id: i64,
/// Related decoded DEX event id, when available.
pub decoded_event_id: std::option::Option<i64>,
/// Stable leg index within one parent fee event.
pub leg_index: u32,
/// Fee component kind or semantic role.
pub fee_component_kind: std::string::String,
/// Token mint used by this fee amount leg.
pub token_mint: std::string::String,
/// Raw amount for this leg as decimal text.
pub amount_raw: std::string::String,
/// Source token account or lamport account, when decoded.
pub source_account: std::option::Option<std::string::String>,
/// Destination token account or lamport account, when decoded.
pub destination_account: std::option::Option<std::string::String>,
/// Extraction source used to prove this amount.
pub amount_source: std::string::String,
/// Source/proof payload JSON.
pub payload_json: std::string::String,
/// Creation timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl FeeEventAmountDto {
/// Creates a new fee event amount DTO.
#[allow(clippy::too_many_arguments)]
pub fn new(
fee_event_id: i64,
transaction_id: i64,
decoded_event_id: std::option::Option<i64>,
leg_index: u32,
fee_component_kind: std::string::String,
token_mint: std::string::String,
amount_raw: std::string::String,
source_account: std::option::Option<std::string::String>,
destination_account: std::option::Option<std::string::String>,
amount_source: std::string::String,
payload_json: std::string::String,
) -> Self {
return Self {
id: None,
fee_event_id,
transaction_id,
decoded_event_id,
leg_index,
fee_component_kind,
token_mint,
amount_raw,
source_account,
destination_account,
amount_source,
payload_json,
created_at: chrono::Utc::now(),
};
}
}
impl TryFrom<crate::FeeEventAmountEntity> for FeeEventAmountDto {
type Error = crate::Error;
fn try_from(entity: crate::FeeEventAmountEntity) -> Result<Self, Self::Error> {
let leg_index_result = u32::try_from(entity.leg_index);
let leg_index = match leg_index_result {
Ok(leg_index) => leg_index,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot convert fee event amount leg_index '{}' to u32: {}",
entity.leg_index, error
)));
},
};
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::Error::Db(format!(
"cannot parse fee event amount created_at '{}': {}",
entity.created_at, error
)));
},
};
return Ok(Self {
id: Some(entity.id),
fee_event_id: entity.fee_event_id,
transaction_id: entity.transaction_id,
decoded_event_id: entity.decoded_event_id,
leg_index,
fee_component_kind: entity.fee_component_kind,
token_mint: entity.token_mint,
amount_raw: entity.amount_raw,
source_account: entity.source_account,
destination_account: entity.destination_account,
amount_source: entity.amount_source,
payload_json: entity.payload_json,
created_at,
});
}
}

View File

@@ -15,6 +15,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event;
mod dex_event_coverage_entry;
mod fee_event;
mod fee_event_amount;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
@@ -63,6 +64,7 @@ pub use dex_decoded_event::DexDecodedEventEntity;
pub use dex_event_coverage_entry::DexEventCoverageEntryEntity;
pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
pub use fee_event::FeeEventEntity;
pub use fee_event_amount::FeeEventAmountEntity;
pub use known_http_endpoint::KnownHttpEndpointEntity;
pub use known_ws_endpoint::KnownWsEndpointEntity;
pub use instruction_observation::InstructionObservationEntity;

View File

@@ -0,0 +1,34 @@
// file: kb_lib/src/db/entities/fee_event_amount.rs
//! Fee event amount entity.
/// Persisted normalized fee event amount leg row.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct FeeEventAmountEntity {
/// Numeric primary key.
pub id: i64,
/// Related parent fee event id.
pub fee_event_id: i64,
/// Related transaction id.
pub transaction_id: i64,
/// Related decoded DEX event id, when available.
pub decoded_event_id: std::option::Option<i64>,
/// Stable leg index within one parent fee event.
pub leg_index: i64,
/// Fee component kind or semantic role.
pub fee_component_kind: std::string::String,
/// Token mint used by this fee amount leg.
pub token_mint: std::string::String,
/// Raw amount for this leg as decimal text.
pub amount_raw: std::string::String,
/// Source token account or lamport account, when decoded.
pub source_account: std::option::Option<std::string::String>,
/// Destination token account or lamport account, when decoded.
pub destination_account: std::option::Option<std::string::String>,
/// Extraction source used to prove this amount.
pub amount_source: std::string::String,
/// Source/proof payload JSON.
pub payload_json: std::string::String,
/// Creation timestamp encoded as RFC3339 UTC text.
pub created_at: std::string::String,
}

View File

@@ -13,6 +13,7 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event;
mod dex_event_coverage_entry;
mod fee_event;
mod fee_event_amount;
mod instruction_observation;
mod known_http_endpoint;
mod known_ws_endpoint;
@@ -99,6 +100,11 @@ pub use dex_event_coverage_entry::query_dex_event_coverage_entries_upsert;
pub use fee_event::query_fee_events_get_by_decoded_event_id;
pub use fee_event::query_fee_events_list_recent;
pub use fee_event::query_fee_events_upsert;
pub use fee_event_amount::query_fee_event_amounts_backfill_single_leg_from_fee_events;
pub use fee_event_amount::query_fee_event_amounts_delete_by_fee_event_id;
pub use fee_event_amount::query_fee_event_amounts_list_by_fee_event_id;
pub use fee_event_amount::query_fee_event_amounts_replace_for_fee_event;
pub use fee_event_amount::query_fee_events_upsert_with_amount_legs;
pub use instruction_observation::query_instruction_observation_source_rows_list_by_signature;
pub use instruction_observation::query_instruction_observation_source_rows_list_recent;
pub use instruction_observation::query_instruction_observation_source_rows_list_replay_window;

View File

@@ -136,6 +136,10 @@ WHERE decoded_event_id IN (
"k_sol_pool_lifecycle_events",
"DELETE FROM k_sol_pool_lifecycle_events WHERE transaction_id = ?",
),
(
"k_sol_fee_event_amounts",
"DELETE FROM k_sol_fee_event_amounts WHERE transaction_id = ?",
),
("k_sol_fee_events", "DELETE FROM k_sol_fee_events WHERE transaction_id = ?"),
(
"k_sol_reward_events",

View File

@@ -156,6 +156,10 @@ WHERE id = ?
id, error
)));
}
let leg_result = query_fee_events_replace_scalar_amount_leg(database, id, dto).await;
if let Err(error) = leg_result {
return Err(error);
}
return Ok(id);
}
let insert_result = sqlx::query(
@@ -227,7 +231,13 @@ LIMIT 1
.fetch_one(pool)
.await;
match id_result {
Ok(id) => return Ok(id),
Ok(id) => {
let leg_result = query_fee_events_replace_scalar_amount_leg(database, id, dto).await;
if let Err(error) = leg_result {
return Err(error);
}
return Ok(id);
},
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch inserted k_sol_fee_events id for signature '{}' on sqlite: {}",
@@ -239,6 +249,88 @@ LIMIT 1
}
}
async fn query_fee_events_replace_scalar_amount_leg(
database: &crate::Database,
fee_event_id: i64,
dto: &crate::FeeEventDto,
) -> Result<(), crate::Error> {
let token_mint = match dto.fee_token_mint.as_ref() {
Some(token_mint) => {
if token_mint.trim().is_empty() {
return Ok(());
}
token_mint.clone()
},
None => return Ok(()),
};
let amount_raw = match dto.fee_amount_raw.as_ref() {
Some(amount_raw) => {
if amount_raw.trim().is_empty() {
return Ok(());
}
amount_raw.clone()
},
None => return Ok(()),
};
let payload_json = scalar_fee_event_amount_payload_json(dto, token_mint.as_str(), amount_raw.as_str());
let amount_leg = crate::FeeEventAmountDto::new(
fee_event_id,
dto.transaction_id,
dto.decoded_event_id,
0,
dto.event_kind.clone(),
token_mint,
amount_raw,
None,
None,
"parent_fee_event_amount".to_string(),
payload_json,
);
let replace_result = crate::query_fee_event_amounts_replace_for_fee_event(
database,
fee_event_id,
&[amount_leg],
)
.await;
match replace_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
fn scalar_fee_event_amount_payload_json(
dto: &crate::FeeEventDto,
token_mint: &str,
amount_raw: &str,
) -> std::string::String {
let mut object = serde_json::Map::new();
object.insert(
"decodedEventKind".to_string(),
serde_json::Value::String(dto.event_kind.clone()),
);
object.insert(
"protocolName".to_string(),
serde_json::Value::String(dto.protocol_name.clone()),
);
object.insert(
"legIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(0_u64)),
);
object.insert(
"tokenMint".to_string(),
serde_json::Value::String(token_mint.to_string()),
);
object.insert(
"amountRaw".to_string(),
serde_json::Value::String(amount_raw.to_string()),
);
object.insert(
"amountSource".to_string(),
serde_json::Value::String("parent_fee_event_amount".to_string()),
);
return serde_json::Value::Object(object).to_string();
}
/// Lists recent fee events ordered from newest to oldest.
pub async fn query_fee_events_list_recent(
database: &crate::Database,

View File

@@ -0,0 +1,677 @@
// file: kb_lib/src/db/queries/fee_event_amount.rs
//! Queries for `k_sol_fee_event_amounts`.
/// Deletes fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_delete_by_fee_event_id(
database: &crate::Database,
fee_event_id: i64,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_fee_event_amounts
WHERE fee_event_id = ?
"#,
)
.bind(fee_event_id)
.execute(pool)
.await;
match delete_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
fee_event_id, error
)));
},
}
},
}
}
/// Replaces all fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_replace_for_fee_event(
database: &crate::Database,
fee_event_id: i64,
dtos: &[crate::FeeEventAmountDto],
) -> Result<u64, crate::Error> {
let delete_result =
query_fee_event_amounts_delete_by_fee_event_id(database, fee_event_id).await;
if let Err(error) = delete_result {
return Err(error);
}
let mut inserted: u64 = 0;
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
for dto in dtos {
let leg_index_i64 = i64::from(dto.leg_index);
let insert_result = sqlx::query(
r#"
INSERT INTO k_sol_fee_event_amounts (
fee_event_id,
transaction_id,
decoded_event_id,
leg_index,
fee_component_kind,
token_mint,
amount_raw,
source_account,
destination_account,
amount_source,
payload_json,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(dto.fee_event_id)
.bind(dto.transaction_id)
.bind(dto.decoded_event_id)
.bind(leg_index_i64)
.bind(dto.fee_component_kind.clone())
.bind(dto.token_mint.clone())
.bind(dto.amount_raw.clone())
.bind(dto.source_account.clone())
.bind(dto.destination_account.clone())
.bind(dto.amount_source.clone())
.bind(dto.payload_json.clone())
.bind(dto.created_at.to_rfc3339())
.execute(pool)
.await;
match insert_result {
Ok(_) => inserted += 1,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot insert k_sol_fee_event_amounts leg '{}' for fee_event_id '{}' on sqlite: {}",
dto.leg_index, fee_event_id, error
)));
},
}
}
},
}
return Ok(inserted);
}
/// Lists fee amount legs for one parent fee event.
pub async fn query_fee_event_amounts_list_by_fee_event_id(
database: &crate::Database,
fee_event_id: i64,
) -> Result<std::vec::Vec<crate::FeeEventAmountDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventAmountEntity>(
r#"
SELECT
id,
fee_event_id,
transaction_id,
decoded_event_id,
leg_index,
fee_component_kind,
token_mint,
amount_raw,
source_account,
destination_account,
amount_source,
payload_json,
created_at
FROM k_sol_fee_event_amounts
WHERE fee_event_id = ?
ORDER BY leg_index ASC
"#,
)
.bind(fee_event_id)
.fetch_all(pool)
.await;
let entities = match query_result {
Ok(entities) => entities,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list k_sol_fee_event_amounts for fee_event_id '{}' on sqlite: {}",
fee_event_id, error
)));
},
};
let mut dtos = std::vec::Vec::new();
for entity in entities {
let dto_result = crate::FeeEventAmountDto::try_from(entity);
match dto_result {
Ok(dto) => dtos.push(dto),
Err(error) => return Err(error),
}
}
return Ok(dtos);
},
}
}
/// Inserts or updates a fee parent and atomically replaces its amount legs.
pub async fn query_fee_events_upsert_with_amount_legs(
database: &crate::Database,
fee_event: &crate::FeeEventDto,
amount_legs: &[crate::FeeEventAmountDto],
) -> Result<i64, crate::Error> {
let fee_event_id_result = crate::query_fee_events_upsert(database, fee_event).await;
let fee_event_id = match fee_event_id_result {
Ok(fee_event_id) => fee_event_id,
Err(error) => return Err(error),
};
let mut normalized_legs = std::vec::Vec::new();
for amount_leg in amount_legs {
let mut normalized_leg = amount_leg.clone();
normalized_leg.fee_event_id = fee_event_id;
normalized_leg.transaction_id = fee_event.transaction_id;
normalized_leg.decoded_event_id = fee_event.decoded_event_id;
normalized_legs.push(normalized_leg);
}
let replace_result = query_fee_event_amounts_replace_for_fee_event(
database,
fee_event_id,
normalized_legs.as_slice(),
)
.await;
if let Err(error) = replace_result {
return Err(error);
}
return Ok(fee_event_id);
}
/// Backfills one amount leg for existing fee parents that expose one scalar amount.
pub async fn query_fee_event_amounts_backfill_single_leg_from_fee_events(
database: &crate::Database,
) -> Result<u64, crate::Error> {
let entities = match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::FeeEventEntity>(
r#"
SELECT
f.id,
f.transaction_id,
f.decoded_event_id,
f.dex_id,
f.pool_id,
f.pair_id,
f.signature,
f.slot,
f.protocol_name,
f.program_id,
f.event_kind,
f.pool_account,
f.actor_wallet,
f.fee_token_mint,
f.fee_amount_raw,
f.payload_json,
f.executed_at,
f.created_at
FROM k_sol_fee_events f
WHERE f.fee_token_mint IS NOT NULL
AND TRIM(f.fee_token_mint) <> ''
AND f.fee_amount_raw IS NOT NULL
AND TRIM(f.fee_amount_raw) <> ''
AND NOT EXISTS (
SELECT 1
FROM k_sol_fee_event_amounts a
WHERE a.fee_event_id = f.id
LIMIT 1
)
ORDER BY f.id ASC
"#,
)
.fetch_all(pool)
.await;
match query_result {
Ok(entities) => entities,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list fee events for single-leg amount backfill on sqlite: {}",
error
)));
},
}
},
};
let mut backfilled: u64 = 0;
for entity in entities {
let dto_result = crate::FeeEventDto::try_from(entity);
let dto = match dto_result {
Ok(dto) => dto,
Err(error) => return Err(error),
};
let fee_event_id = match dto.id {
Some(id) => id,
None => {
return Err(crate::Error::InvalidState(
"fee event backfill row does not contain an id".to_string(),
));
},
};
let token_mint = match dto.fee_token_mint.as_ref() {
Some(token_mint) => token_mint.clone(),
None => continue,
};
let amount_raw = match dto.fee_amount_raw.as_ref() {
Some(amount_raw) => amount_raw.clone(),
None => continue,
};
let payload_json = single_fee_event_amount_backfill_payload_json(
&dto,
token_mint.as_str(),
amount_raw.as_str(),
);
let amount_leg = crate::FeeEventAmountDto::new(
fee_event_id,
dto.transaction_id,
dto.decoded_event_id,
0,
dto.event_kind.clone(),
token_mint,
amount_raw,
None,
None,
"parent_fee_event_backfill".to_string(),
payload_json,
);
let replace_result =
query_fee_event_amounts_replace_for_fee_event(database, fee_event_id, &[amount_leg])
.await;
match replace_result {
Ok(inserted) => backfilled += inserted,
Err(error) => return Err(error),
}
}
return Ok(backfilled);
}
fn single_fee_event_amount_backfill_payload_json(
dto: &crate::FeeEventDto,
token_mint: &str,
amount_raw: &str,
) -> std::string::String {
let mut object = serde_json::Map::new();
object.insert(
"decodedEventKind".to_string(),
serde_json::Value::String(dto.event_kind.clone()),
);
object.insert("protocolName".to_string(), serde_json::Value::String(dto.protocol_name.clone()));
object.insert(
"legIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(0_u64)),
);
object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.to_string()));
object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.to_string()));
object.insert(
"amountSource".to_string(),
serde_json::Value::String("parent_fee_event_backfill".to_string()),
);
return serde_json::Value::Object(object).to_string();
}
#[cfg(test)]
mod tests {
async fn make_database() -> crate::Database {
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("fee_event_amount.sqlite3");
let config = crate::DatabaseConfig {
enabled: true,
backend: crate::DatabaseBackend::Sqlite,
sqlite: crate::SqliteDatabaseConfig {
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::Database::connect_and_initialize(&config).await;
match database_result {
Ok(database) => return database,
Err(error) => panic!("database init must succeed: {}", error),
}
}
fn make_leg(
fee_event_id: i64,
leg_index: u32,
token_mint: &str,
amount_raw: &str,
) -> crate::FeeEventAmountDto {
return crate::FeeEventAmountDto::new(
fee_event_id,
7001,
Some(8001),
leg_index,
"trading_fee".to_string(),
token_mint.to_string(),
amount_raw.to_string(),
Some(format!("Source{}", leg_index)),
Some(format!("Destination{}", leg_index)),
"inner_spl_transfer".to_string(),
serde_json::json!({
"test": true,
"legIndex": leg_index
})
.to_string(),
);
}
#[tokio::test]
async fn fee_event_amounts_replace_roundtrip_supports_multiple_legs() {
let database = make_database().await;
let fee_event_id = 501;
let legs = vec![
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
make_leg(fee_event_id, 1, "TokenMint111", "200"),
];
let replace_result =
crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
.await;
let inserted = match replace_result {
Ok(inserted) => inserted,
Err(error) => panic!("replace must succeed: {}", error),
};
assert_eq!(inserted, 2);
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert_eq!(listed.len(), 2);
assert_eq!(listed[0].leg_index, 0);
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
assert_eq!(listed[0].amount_raw, "100");
assert_eq!(listed[1].leg_index, 1);
assert_eq!(listed[1].token_mint, "TokenMint111");
assert_eq!(listed[1].amount_raw, "200");
}
#[tokio::test]
async fn fee_event_amounts_replace_deletes_stale_legs() {
let database = make_database().await;
let fee_event_id = 502;
let initial_legs = vec![
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
make_leg(fee_event_id, 1, "TokenMint222", "200"),
];
let initial_result = crate::query_fee_event_amounts_replace_for_fee_event(
&database,
fee_event_id,
&initial_legs,
)
.await;
if let Err(error) = initial_result {
panic!("initial replace must succeed: {}", error);
}
let replacement_legs = vec![make_leg(fee_event_id, 0, "TokenMint333", "300")];
let replacement_result = crate::query_fee_event_amounts_replace_for_fee_event(
&database,
fee_event_id,
&replacement_legs,
)
.await;
let inserted = match replacement_result {
Ok(inserted) => inserted,
Err(error) => panic!("replacement must succeed: {}", error),
};
assert_eq!(inserted, 1);
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].leg_index, 0);
assert_eq!(listed[0].token_mint, "TokenMint333");
assert_eq!(listed[0].amount_raw, "300");
}
fn make_fee_event(
transaction_id: i64,
decoded_event_id: i64,
signature: &str,
fee_token_mint: std::option::Option<&str>,
fee_amount_raw: std::option::Option<&str>,
) -> crate::FeeEventDto {
return crate::FeeEventDto::new(
transaction_id,
Some(decoded_event_id),
None,
None,
None,
signature.to_string(),
Some(1234),
"test_protocol".to_string(),
"TestProgram111".to_string(),
"test_protocol.claim_fee".to_string(),
None,
None,
fee_token_mint.map(|value| return value.to_string()),
fee_amount_raw.map(|value| return value.to_string()),
serde_json::json!({"test": true}).to_string(),
);
}
async fn insert_legacy_fee_event_parent(
database: &crate::Database,
fee_event: &crate::FeeEventDto,
) -> i64 {
let slot_i64 = match fee_event.slot {
Some(slot) => {
let slot_result = i64::try_from(slot);
match slot_result {
Ok(slot_i64) => Some(slot_i64),
Err(error) => panic!("slot conversion must succeed: {}", error),
}
},
None => None,
};
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let insert_result = sqlx::query(
r#"
INSERT INTO k_sol_fee_events (
transaction_id,
decoded_event_id,
dex_id,
pool_id,
pair_id,
signature,
slot,
protocol_name,
program_id,
event_kind,
pool_account,
actor_wallet,
fee_token_mint,
fee_amount_raw,
payload_json,
executed_at,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(fee_event.transaction_id)
.bind(fee_event.decoded_event_id)
.bind(fee_event.dex_id)
.bind(fee_event.pool_id)
.bind(fee_event.pair_id)
.bind(fee_event.signature.clone())
.bind(slot_i64)
.bind(fee_event.protocol_name.clone())
.bind(fee_event.program_id.clone())
.bind(fee_event.event_kind.clone())
.bind(fee_event.pool_account.clone())
.bind(fee_event.actor_wallet.clone())
.bind(fee_event.fee_token_mint.clone())
.bind(fee_event.fee_amount_raw.clone())
.bind(fee_event.payload_json.clone())
.bind(fee_event.executed_at.to_rfc3339())
.bind(fee_event.created_at.to_rfc3339())
.execute(pool)
.await;
if let Err(error) = insert_result {
panic!("legacy parent insert must succeed: {}", error);
}
let id_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_fee_events
WHERE signature = ?
ORDER BY id DESC
LIMIT 1
"#,
)
.bind(fee_event.signature.clone())
.fetch_one(pool)
.await;
match id_result {
Ok(id) => return id,
Err(error) => panic!("legacy parent id fetch must succeed: {}", error),
}
},
}
}
#[tokio::test]
async fn fee_events_upsert_with_amount_legs_normalizes_parent_id() {
let database = make_database().await;
let fee_event = make_fee_event(7101, 8101, "signature-helper", None, None);
let legs = vec![
make_leg(0, 0, crate::WSOL_MINT_ID, "100"),
make_leg(0, 1, "TokenMint555", "200"),
];
let upsert_result =
crate::query_fee_events_upsert_with_amount_legs(&database, &fee_event, legs.as_slice())
.await;
let fee_event_id = match upsert_result {
Ok(fee_event_id) => fee_event_id,
Err(error) => panic!("upsert with legs must succeed: {}", error),
};
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert_eq!(listed.len(), 2);
assert_eq!(listed[0].fee_event_id, fee_event_id);
assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
assert_eq!(listed[1].fee_event_id, fee_event_id);
assert_eq!(listed[1].transaction_id, fee_event.transaction_id);
assert_eq!(listed[1].decoded_event_id, fee_event.decoded_event_id);
}
#[tokio::test]
async fn fee_events_upsert_automatically_creates_scalar_amount_leg() {
let database = make_database().await;
let fee_event = make_fee_event(
7151,
8151,
"signature-auto-leg",
Some(crate::WSOL_MINT_ID),
Some("777"),
);
let upsert_result = crate::query_fee_events_upsert(&database, &fee_event).await;
let fee_event_id = match upsert_result {
Ok(fee_event_id) => fee_event_id,
Err(error) => panic!("fee parent upsert must succeed: {}", error),
};
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].fee_event_id, fee_event_id);
assert_eq!(listed[0].transaction_id, fee_event.transaction_id);
assert_eq!(listed[0].decoded_event_id, fee_event.decoded_event_id);
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
assert_eq!(listed[0].amount_raw, "777");
assert_eq!(listed[0].amount_source, "parent_fee_event_amount");
}
#[tokio::test]
async fn fee_event_amounts_backfill_single_leg_from_existing_parent() {
let database = make_database().await;
let fee_event = make_fee_event(
7201,
8201,
"signature-backfill",
Some(crate::WSOL_MINT_ID),
Some("999"),
);
let fee_event_id = insert_legacy_fee_event_parent(&database, &fee_event).await;
let backfill_result =
crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
let backfilled = match backfill_result {
Ok(backfilled) => backfilled,
Err(error) => panic!("backfill must succeed: {}", error),
};
assert_eq!(backfilled, 1);
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].token_mint, crate::WSOL_MINT_ID);
assert_eq!(listed[0].amount_raw, "999");
assert_eq!(listed[0].amount_source, "parent_fee_event_backfill");
let second_backfill_result =
crate::query_fee_event_amounts_backfill_single_leg_from_fee_events(&database).await;
let second_backfilled = match second_backfill_result {
Ok(second_backfilled) => second_backfilled,
Err(error) => panic!("second backfill must succeed: {}", error),
};
assert_eq!(second_backfilled, 0);
}
#[tokio::test]
async fn fee_event_amounts_delete_by_parent_removes_all_legs() {
let database = make_database().await;
let fee_event_id = 503;
let legs = vec![
make_leg(fee_event_id, 0, crate::WSOL_MINT_ID, "100"),
make_leg(fee_event_id, 1, "TokenMint444", "200"),
];
let replace_result =
crate::query_fee_event_amounts_replace_for_fee_event(&database, fee_event_id, &legs)
.await;
if let Err(error) = replace_result {
panic!("replace must succeed: {}", error);
}
let delete_result =
crate::query_fee_event_amounts_delete_by_fee_event_id(&database, fee_event_id).await;
let deleted = match delete_result {
Ok(deleted) => deleted,
Err(error) => panic!("delete must succeed: {}", error),
};
assert_eq!(deleted, 2);
let list_result =
crate::query_fee_event_amounts_list_by_fee_event_id(&database, fee_event_id).await;
let listed = match list_result {
Ok(listed) => listed,
Err(error) => panic!("list must succeed: {}", error),
};
assert!(listed.is_empty());
}
}

View File

@@ -342,6 +342,30 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_fee_event_amounts(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_fee_event_amounts_fee_event_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_fee_event_amounts_transaction_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_fee_event_amounts_decoded_event_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_fee_event_amounts_token_mint(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_uix_fee_event_amounts_fee_event_leg(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_reward_events(pool).await;
if let Err(error) = result {
return Err(error);
@@ -2702,6 +2726,107 @@ WHERE decoded_event_id IS NOT NULL
.await;
}
/// Creates `k_sol_fee_event_amounts`.
async fn create_tbl_fee_event_amounts(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_tbl_fee_event_amounts",
r#"
CREATE TABLE IF NOT EXISTS k_sol_fee_event_amounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
fee_event_id INTEGER NOT NULL,
transaction_id INTEGER NOT NULL,
decoded_event_id INTEGER NULL,
leg_index INTEGER NOT NULL,
fee_component_kind TEXT NOT NULL,
token_mint TEXT NOT NULL,
amount_raw TEXT NOT NULL,
source_account TEXT NULL,
destination_account TEXT NULL,
amount_source TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL
)
"#,
)
.await;
}
/// Creates index on `k_sol_fee_event_amounts(fee_event_id)`.
async fn create_idx_fee_event_amounts_fee_event_id(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_fee_event_amounts_fee_event_id",
r#"
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_fee_event_id
ON k_sol_fee_event_amounts (fee_event_id)
"#,
)
.await;
}
/// Creates index on `k_sol_fee_event_amounts(transaction_id)`.
async fn create_idx_fee_event_amounts_transaction_id(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_fee_event_amounts_transaction_id",
r#"
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_transaction_id
ON k_sol_fee_event_amounts (transaction_id)
"#,
)
.await;
}
/// Creates index on `k_sol_fee_event_amounts(decoded_event_id)`.
async fn create_idx_fee_event_amounts_decoded_event_id(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_fee_event_amounts_decoded_event_id",
r#"
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_decoded_event_id
ON k_sol_fee_event_amounts (decoded_event_id)
"#,
)
.await;
}
/// Creates index on `k_sol_fee_event_amounts(token_mint)`.
async fn create_idx_fee_event_amounts_token_mint(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_fee_event_amounts_token_mint",
r#"
CREATE INDEX IF NOT EXISTS idx_fee_event_amounts_token_mint
ON k_sol_fee_event_amounts (token_mint)
"#,
)
.await;
}
/// Creates unique index on one fee amount leg per parent fee event and leg index.
async fn create_uix_fee_event_amounts_fee_event_leg(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_uix_fee_event_amounts_fee_event_leg",
r#"
CREATE UNIQUE INDEX IF NOT EXISTS uix_fee_event_amounts_fee_event_leg
ON k_sol_fee_event_amounts (fee_event_id, leg_index)
"#,
)
.await;
}
/// Creates `k_sol_reward_events`.
async fn create_tbl_reward_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(

View File

@@ -43,6 +43,7 @@ pub use meteora_damm_v2::MeteoraDammV2SwapDecoded;
pub use meteora_dbc::MeteoraDbcCreatePoolDecoded;
pub use meteora_dbc::MeteoraDbcDecodedEvent;
pub use meteora_dbc::MeteoraDbcDecoder;
pub use meteora_dbc::MeteoraDbcInstructionDecoded;
pub use meteora_dbc::MeteoraDbcSwapDecoded;
pub use meteora_dlmm::MeteoraDlmmCreatePoolDecoded;
pub use meteora_dlmm::MeteoraDlmmDecodedEvent;

File diff suppressed because it is too large Load Diff

View File

@@ -1045,7 +1045,7 @@ impl DexDecodeService {
event.instruction_id,
"meteora_dbc",
event.program_id.clone(),
"meteora_dbc.create_pool",
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
@@ -1063,7 +1063,7 @@ impl DexDecodeService {
event.instruction_id,
"meteora_dbc",
event.program_id.clone(),
"meteora_dbc.swap",
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
@@ -1073,6 +1073,24 @@ impl DexDecodeService {
)
.await;
},
crate::MeteoraDbcDecodedEvent::Instruction(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dbc",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.related_account.clone(),
event.payload_json.clone(),
)
.await;
},
}
}

View File

@@ -164,6 +164,9 @@ pub(crate) fn dex_detection_route(
("meteora_dbc", "meteora_dbc.swap") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
},
("meteora_dbc", "meteora_dbc.swap2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
},
("meteora_dlmm", "meteora_dlmm.create_pool") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool);
},
@@ -304,6 +307,26 @@ mod tests {
assert!(!crate::dex_detection_route::decoded_event_has_full_pool_context(&event));
}
#[test]
fn meteora_dbc_swap2_routes_to_pool_detection() {
let event = make_decoded_event(
"meteora_dbc",
"meteora_dbc.swap2",
Some("Pool111"),
Some("TokenA111"),
Some("TokenB111"),
);
let route_option = crate::dex_detection_route::dex_detection_route(&event);
let route = match route_option {
Some(route) => route,
None => panic!("route must be selected"),
};
assert_eq!(route, crate::dex_detection_route::DexDetectionRoute::MeteoraDbcPool);
assert!(crate::dex_detection_route::dex_detection_route_requires_full_pool_context(
route
));
}
#[test]
fn raydium_launchpad_initialize_route_requires_full_pool_context() {
let event = make_decoded_event(

View File

@@ -350,6 +350,9 @@ pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if event_kind.ends_with(".swap") {
return true;
}
if event_kind.ends_with(".swap2") {
return true;
}
if event_kind.contains(".swap_") {
return true;
}
@@ -375,6 +378,22 @@ pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool {
/// Returns true for liquidity lifecycle changes that must not become candles.
pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("fee")
|| event_kind.contains("surplus")
|| event_kind.contains("leftover")
|| event_kind.contains("zap_protocol_fee"))
{
return false;
}
if event_kind == "meteora_dbc.migrate_meteora_damm_claim_lp_token"
|| event_kind == "meteora_dbc.migrate_meteora_damm_lock_lp_token"
{
return false;
}
if event_kind.starts_with("meteora_dbc.") && event_kind.contains("lp_token") {
return true;
}
if event_kind.contains(".withdraw_pnl") {
return false;
}
@@ -490,6 +509,14 @@ pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
/// Returns true for fee collection events.
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("fee")
|| event_kind.contains("surplus")
|| event_kind.contains("leftover")
|| event_kind.contains("zap_protocol_fee"))
{
return true;
}
if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("donation_fee_pda_cranked")
|| event_kind.contains("sweep_buyback")
@@ -639,7 +666,7 @@ pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_amm_v4.pre_initialize" {
return true;
}
if event_kind.contains(".create_lock_escrow") {
if event_kind.contains(".create_lock_escrow") || event_kind.contains(".create_locker") {
return true;
}
if event_kind.contains(".initialize_bin_array") {
@@ -731,6 +758,9 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".migrate_pool_coin_creator") {
return false;
}
if event_kind.starts_with("meteora_dbc.") && event_kind.contains("metadata") {
return false;
}
if event_kind.contains(".migrate") {
return true;
}
@@ -742,6 +772,11 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
/// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind == "meteora_dbc.evt_initialize_pool_event"
|| event_kind.contains(".initialize_virtual_pool")
{
return true;
}
if event_kind == "raydium_amm_v4.pre_initialize" {
return true;
}
@@ -824,6 +859,15 @@ pub fn is_dex_token_account_close_event_kind(event_kind: &str) -> bool {
/// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("config")
|| event_kind.contains("operator")
|| event_kind.contains("metadata")
|| event_kind.contains("pool_creator")
|| event_kind == "meteora_dbc.transfer_pool_creator")
{
return true;
}
if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("authority")
|| event_kind.contains("admin")
@@ -1232,13 +1276,16 @@ mod tests {
);
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap"), "trade");
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("meteora_dbc.swap2"), "trade");
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.sell_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "trade");
assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
assert!(super::is_dex_trade_event_kind("meteora_dbc.swap2"));
assert!(super::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
assert!(super::is_dex_candle_candidate_event_kind("meteora_dbc.swap2"));
}
#[test]

View File

@@ -65,6 +65,25 @@ impl DexEventCoverageService {
Err(error) => return Err(error),
}
}
if decoder_code.as_deref().is_none() || decoder_code.as_deref() == Some("meteora_dbc") {
let supplemental_entries = local_meteora_dbc_registry_entries();
for entry in &supplemental_entries {
let coverage_entry = build_coverage_entry_from_upstream(entry);
let upsert_result = crate::query_dex_event_coverage_entries_upsert(
self.database.as_ref(),
&coverage_entry,
)
.await;
match upsert_result {
Ok(_) => upserted_entry_count += 1,
Err(error) => return Err(error),
}
}
return Ok((
search_result.entries.len() + supplemental_entries.len(),
upserted_entry_count,
));
}
return Ok((search_result.entries.len(), upserted_entry_count));
}
@@ -99,6 +118,11 @@ impl DexEventCoverageService {
if let Err(error) = duplicate_cleanup_result {
return Err(error);
}
let meteora_dbc_duplicate_cleanup_result =
self.cleanup_duplicate_meteora_dbc_logical_coverage_rows(&decoder_code).await;
if let Err(error) = meteora_dbc_duplicate_cleanup_result {
return Err(error);
}
let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => {
let refresh_result =
@@ -159,6 +183,11 @@ impl DexEventCoverageService {
if let Err(error) = duplicate_cleanup_result {
return Err(error);
}
let meteora_dbc_duplicate_cleanup_result =
self.cleanup_duplicate_meteora_dbc_logical_coverage_rows(&decoder_code).await;
if let Err(error) = meteora_dbc_duplicate_cleanup_result {
return Err(error);
}
let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => {
let refresh_result =
@@ -278,6 +307,50 @@ WHERE decoder_code = 'pump_swap'
},
}
}
async fn cleanup_duplicate_meteora_dbc_logical_coverage_rows(
&self,
decoder_code: &std::option::Option<std::string::String>,
) -> Result<u64, crate::Error> {
if let Some(decoder_code) = decoder_code {
if decoder_code != "meteora_dbc" {
return Ok(0);
}
}
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'meteora_dbc'
AND id NOT IN (
SELECT MIN(id)
FROM k_sol_dex_event_coverage_entries
WHERE decoder_code = 'meteora_dbc'
GROUP BY
decoder_code,
COALESCE(program_id, ''),
entry_kind,
entry_name,
COALESCE(discriminator_hex, ''),
COALESCE(local_event_kind, '')
)
"#,
)
.execute(pool)
.await;
match query_result {
Ok(query_result) => return Ok(query_result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete duplicate Meteora DBC logical coverage rows on sqlite: {}",
error
)));
},
}
},
}
}
}
fn build_coverage_entry_from_upstream(
@@ -309,6 +382,353 @@ fn build_coverage_entry_from_upstream(
return coverage_entry;
}
fn local_meteora_dbc_registry_entries() -> std::vec::Vec<crate::UpstreamRegistryEntryDto> {
let mut entries = std::vec::Vec::new();
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"anchor_self_cpi_log",
Some("e445a52e51cb9a1d"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"instruction_audit",
None,
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"claim_creator_trading_fee",
Some("52dcfabd03556b2d"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"claim_partner_pool_creation_fee",
Some("faee1a048b0a65f8"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"claim_protocol_fee",
Some("a5e4853063f9ff21"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"claim_protocol_pool_creation_fee",
Some("72cd53bcf0991936"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"claim_trading_fee",
Some("08ec5931987db151"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"close_claim_protocol_fee_operator",
Some("082957235030791a"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"close_operator_account",
Some("ab09d54a7817031d"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"create_config",
Some("c9cff3724b6f2fbd"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"create_locker",
Some("a75a899a4b2f1154"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"create_operator_account",
Some("dd40f695f099e5a3"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"create_partner_metadata",
Some("c0a8eabfbce2e3ff"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"create_virtual_pool_metadata",
Some("2d61bb67fe6d7c86"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"creator_withdraw_surplus",
Some("a50389071c864c50"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"initialize_virtual_pool_with_spl_token",
Some("8c55d7b06636684f"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"initialize_virtual_pool_with_token2022",
Some("a976334e916edc9b"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migrate_meteora_damm",
Some("1b013016b43f76d9"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migrate_meteora_damm_claim_lp_token",
Some("8b85021e5b917f9a"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migrate_meteora_damm_lock_lp_token",
Some("b137ee9dfb58a52a"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migration_damm_v2",
Some("9ca9e66735e45040"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migration_damm_v2_create_metadata",
Some("6dbd1324c3b7de52"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"migration_meteora_damm_create_metadata",
Some("2f5e7e73dde2c285"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"partner_withdraw_surplus",
Some("a8ad4864c962265c"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"swap",
Some("f8c69e91e17587c8"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"swap2",
Some("414b3f4ceb5b5b88"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"transfer_pool_creator",
Some("1407a9213a93a621"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"withdraw_leftover",
Some("14c6caedebf3b742"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"withdraw_migration_fee",
Some("ed8e2d178106dea2"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_INSTRUCTION,
"zap_protocol_fee",
Some("d59bbb2238b65bf0"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_claim_creator_trading_fee_event_local_idl",
Some("9ae4d7ca859bd68a"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_claim_pool_creation_fee_event_local_idl",
Some("956f952c8840af3e"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_claim_protocol_fee_event_local_idl",
Some("baf44bfbbc0d1921"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_claim_protocol_liquidity_migration_fee_event_local_idl",
Some("51a8741fa1561b23"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_claim_trading_fee_event_local_idl",
Some("1a5375f05cca70fe"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_close_claim_fee_operator_event_local_idl",
Some("6f2725376ed8c217"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_create_claim_fee_operator_event_local_idl",
Some("1506997844741cb1"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_create_config_event_local_idl",
Some("83cfb4aeb449a536"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_create_config_v2_event_local_idl",
Some("a34a42bb77c31a90"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_create_meteora_migration_metadata_event_local_idl",
Some("63a7853fd68faf8b"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_creator_withdraw_surplus_event_local_idl",
Some("9849150f4257359d"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_curve_complete_event_local_idl",
Some("e5e756549c864b18"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_initialize_pool_event_local_idl",
Some("e432f655cb428625"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_partner_claim_pool_creation_fee_event_local_idl",
Some("aedf2c96916259c3"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_partner_metadata_event_local_idl",
Some("c87f06370d200896"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_partner_withdraw_migration_fee_event_local_idl",
Some("b5697f4308bb7839"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_partner_withdraw_surplus_event_local_idl",
Some("c3389809e8482316"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_swap_event_local_idl",
Some("1b3c15d58aaabb93"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_swap2_event_local_idl",
Some("bd4233a826507599"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_update_pool_creator_event_local_idl",
Some("6be1a5ed5b9ed5dc"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_virtual_pool_metadata_event_local_idl",
Some("bc12484cc35b264a"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_withdraw_leftover_event_local_idl",
Some("bfbd688f6f9c5ee5"),
);
add_local_meteora_dbc_entry(
&mut entries,
crate::ENTRY_KIND_EVENT,
"evt_withdraw_migration_fee_event_local_idl",
Some("1acb5455a11764d6"),
);
return entries;
}
fn add_local_meteora_dbc_entry(
entries: &mut std::vec::Vec<crate::UpstreamRegistryEntryDto>,
entry_kind: &str,
entry_name: &str,
discriminator_hex: std::option::Option<&str>,
) {
entries.push(crate::UpstreamRegistryEntryDto {
source_repo: Some("local_idl".to_string()),
source_path: Some(
"idls/meteora_dbc.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.json".to_string(),
),
decoder_code: "meteora_dbc".to_string(),
program_id: Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
program_family: "meteora".to_string(),
surface_kind: "launch_bonding_curve".to_string(),
entry_kind: entry_kind.to_string(),
entry_name: entry_name.to_string(),
discriminator_hex: discriminator_hex.map(|value| return value.to_string()),
discriminator_len: discriminator_hex.map(|_| return 8_u16),
proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_MAPPED_UNVERIFIED.to_string(),
notes: "supplemental local Meteora DBC IDL coverage row".to_string(),
});
}
fn infer_expected_db_target_for_entry(
decoder_code: &str,
entry_name: &str,
@@ -324,6 +744,9 @@ fn infer_expected_db_target_for_entry(
if decoder_code == "pump_fees" {
return infer_pump_fees_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "meteora_dbc" {
return infer_meteora_dbc_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "raydium_cpmm"
&& (entry_name == "swap_event" || entry_name == "anchor_idl_instruction")
{
@@ -1094,9 +1517,236 @@ fn infer_event_family_for_entry(
if decoder_code == "raydium_stable_swap" {
return infer_raydium_stable_swap_event_family(entry_name, entry_kind);
}
if decoder_code == "meteora_dbc" {
return infer_meteora_dbc_event_family(entry_name, entry_kind);
}
return infer_event_family(entry_name, entry_kind);
}
fn infer_meteora_dbc_expected_db_target(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM || entry_kind == crate::ENTRY_KIND_ACCOUNT {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
let family = infer_meteora_dbc_event_family(entry_name, entry_kind);
let family = match family {
Some(family) => family,
None => {
return Some(
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
);
},
};
let normalized_entry_name = entry_name.strip_suffix("_local_idl").unwrap_or(entry_name);
if family == "swap" && (normalized_entry_name == "swap" || normalized_entry_name == "swap2") {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if family == "swap" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
if entry_kind == crate::ENTRY_KIND_EVENT
&& meteora_dbc_anchor_event_is_decoded_only_source(normalized_entry_name)
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
if family == "pool_create" || family == "migration" || family == "pool_lifecycle" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string());
}
if family == "liquidity" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string());
}
if family == "fee" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string());
}
if family == "admin_config" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string());
}
if family == "audit" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
fn meteora_dbc_anchor_event_is_decoded_only_source(entry_name: &str) -> bool {
return matches!(
entry_name,
"evt_claim_creator_trading_fee_event"
| "evt_claim_pool_creation_fee_event"
| "evt_claim_protocol_fee_event"
| "evt_claim_protocol_liquidity_migration_fee_event"
| "evt_claim_trading_fee_event"
| "evt_close_claim_fee_operator_event"
| "evt_create_claim_fee_operator_event"
| "evt_create_config_event"
| "evt_create_config_v2_event"
| "evt_create_meteora_migration_metadata_event"
| "evt_creator_withdraw_surplus_event"
| "evt_initialize_pool_event"
| "evt_partner_claim_pool_creation_fee_event"
| "evt_partner_metadata_event"
| "evt_partner_withdraw_migration_fee_event"
| "evt_partner_withdraw_surplus_event"
| "evt_update_pool_creator_event"
| "evt_virtual_pool_metadata_event"
| "evt_withdraw_leftover_event"
| "evt_withdraw_migration_fee_event"
);
}
fn infer_meteora_dbc_event_family(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
if entry_name == "anchor_self_cpi_log" || entry_name == "instruction_audit" {
return Some("audit".to_string());
}
if entry_name == "swap"
|| entry_name == "swap2"
|| entry_name == "evt_swap_event"
|| entry_name == "evt_swap2_event"
{
return Some("swap".to_string());
}
if entry_name == "initialize_virtual_pool_with_spl_token"
|| entry_name == "initialize_virtual_pool_with_token2022"
|| entry_name == "evt_initialize_pool_event"
{
return Some("pool_create".to_string());
}
if entry_name == "create_locker" {
return Some("pool_lifecycle".to_string());
}
if entry_name.contains("metadata") {
return Some("admin_config".to_string());
}
if entry_name.contains("fee")
|| entry_name.contains("surplus")
|| entry_name.contains("leftover")
|| entry_name == "zap_protocol_fee"
{
return Some("fee".to_string());
}
if entry_name.contains("migrate")
|| entry_name.contains("migration")
|| entry_name == "evt_curve_complete_event"
{
return Some("migration".to_string());
}
if entry_name.contains("lp_token") {
return Some("liquidity".to_string());
}
if entry_name.contains("config")
|| entry_name.contains("operator")
|| entry_name.contains("metadata")
|| entry_name == "transfer_pool_creator"
|| entry_name == "evt_update_pool_creator_event"
|| entry_name == "evt_partner_metadata_event"
|| entry_name == "evt_virtual_pool_metadata_event"
{
return Some("admin_config".to_string());
}
return infer_event_family(entry_name, entry_kind);
}
fn meteora_dbc_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
if entry_name == "program" || entry_name == "state" {
return None;
}
let normalized_event_name = match entry_name.strip_suffix("_local_idl") {
Some(value) => value,
None => entry_name,
};
if normalized_event_name == "anchor_self_cpi_log"
|| normalized_event_name == "instruction_audit"
{
return Some("meteora_dbc.instruction_audit".to_string());
}
if normalized_event_name == "initialize_virtual_pool_with_spl_token"
|| normalized_event_name == "initialize_virtual_pool_with_token2022"
{
return Some("meteora_dbc.create_pool".to_string());
}
if entry_name.starts_with("evt_") && !entry_name.ends_with("_local_idl") {
return None;
}
if meteora_dbc_local_instruction_entry_is_known(entry_name)
|| meteora_dbc_local_event_entry_is_known(normalized_event_name)
{
return Some(format!("meteora_dbc.{}", normalized_event_name));
}
return None;
}
fn meteora_dbc_local_instruction_entry_is_known(entry_name: &str) -> bool {
return matches!(
entry_name,
"anchor_self_cpi_log"
| "instruction_audit"
| "claim_creator_trading_fee"
| "claim_partner_pool_creation_fee"
| "claim_protocol_fee"
| "claim_protocol_pool_creation_fee"
| "claim_trading_fee"
| "close_claim_protocol_fee_operator"
| "close_operator_account"
| "create_config"
| "create_locker"
| "create_operator_account"
| "create_partner_metadata"
| "create_virtual_pool_metadata"
| "creator_withdraw_surplus"
| "initialize_virtual_pool_with_spl_token"
| "initialize_virtual_pool_with_token2022"
| "migrate_meteora_damm"
| "migrate_meteora_damm_claim_lp_token"
| "migrate_meteora_damm_lock_lp_token"
| "migration_damm_v2"
| "migration_damm_v2_create_metadata"
| "migration_meteora_damm_create_metadata"
| "partner_withdraw_surplus"
| "swap"
| "swap2"
| "transfer_pool_creator"
| "withdraw_leftover"
| "withdraw_migration_fee"
| "zap_protocol_fee"
);
}
fn meteora_dbc_local_event_entry_is_known(entry_name: &str) -> bool {
return matches!(
entry_name,
"evt_claim_creator_trading_fee_event"
| "evt_claim_pool_creation_fee_event"
| "evt_claim_protocol_fee_event"
| "evt_claim_protocol_liquidity_migration_fee_event"
| "evt_claim_trading_fee_event"
| "evt_close_claim_fee_operator_event"
| "evt_create_claim_fee_operator_event"
| "evt_create_config_event"
| "evt_create_config_v2_event"
| "evt_create_meteora_migration_metadata_event"
| "evt_creator_withdraw_surplus_event"
| "evt_curve_complete_event"
| "evt_initialize_pool_event"
| "evt_partner_claim_pool_creation_fee_event"
| "evt_partner_metadata_event"
| "evt_partner_withdraw_migration_fee_event"
| "evt_partner_withdraw_surplus_event"
| "evt_swap_event"
| "evt_swap2_event"
| "evt_update_pool_creator_event"
| "evt_virtual_pool_metadata_event"
| "evt_withdraw_leftover_event"
| "evt_withdraw_migration_fee_event"
);
}
fn infer_raydium_amm_v4_event_family(
entry_name: &str,
entry_kind: &str,
@@ -1479,6 +2129,9 @@ pub(crate) fn known_local_event_kind(
if decoder_code == "pump_swap" {
return pump_swap_local_event_kind(entry_name);
}
if decoder_code == "meteora_dbc" {
return meteora_dbc_local_event_kind(entry_name);
}
if decoder_code == "raydium_amm_v4" {
return raydium_amm_v4_local_event_kind(entry_name);
}

View File

@@ -200,6 +200,45 @@ fn resolve_instruction_name(
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
if program_id == crate::METEORA_DBC_PROGRAM_ID || decoder_code == Some("meteora_dbc") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "meteora_dbc.anchor_self_cpi_log",
"e992d18ecf6840bc" => "meteora_dbc.create_pool_legacy",
"5fb40aac54aee828" => "meteora_dbc.initialize_pool_legacy",
"a677d1b6d66d3ab5" => "meteora_dbc.launch_pool_legacy",
"52dcfabd03556b2d" => "meteora_dbc.claim_creator_trading_fee",
"faee1a048b0a65f8" => "meteora_dbc.claim_partner_pool_creation_fee",
"a5e4853063f9ff21" => "meteora_dbc.claim_protocol_fee",
"72cd53bcf0991936" => "meteora_dbc.claim_protocol_pool_creation_fee",
"08ec5931987db151" => "meteora_dbc.claim_trading_fee",
"082957235030791a" => "meteora_dbc.close_claim_protocol_fee_operator",
"ab09d54a7817031d" => "meteora_dbc.close_operator_account",
"c9cff3724b6f2fbd" => "meteora_dbc.create_config",
"a75a899a4b2f1154" => "meteora_dbc.create_locker",
"dd40f695f099e5a3" => "meteora_dbc.create_operator_account",
"c0a8eabfbce2e3ff" => "meteora_dbc.create_partner_metadata",
"2d61bb67fe6d7c86" => "meteora_dbc.create_virtual_pool_metadata",
"a50389071c864c50" => "meteora_dbc.creator_withdraw_surplus",
"8c55d7b06636684f" => "meteora_dbc.initialize_virtual_pool_with_spl_token",
"a976334e916edc9b" => "meteora_dbc.initialize_virtual_pool_with_token2022",
"1b013016b43f76d9" => "meteora_dbc.migrate_meteora_damm",
"8b85021e5b917f9a" => "meteora_dbc.migrate_meteora_damm_claim_lp_token",
"b137ee9dfb58a52a" => "meteora_dbc.migrate_meteora_damm_lock_lp_token",
"9ca9e66735e45040" => "meteora_dbc.migration_damm_v2",
"6dbd1324c3b7de52" => "meteora_dbc.migration_damm_v2_create_metadata",
"2f5e7e73dde2c285" => "meteora_dbc.migration_meteora_damm_create_metadata",
"a8ad4864c962265c" => "meteora_dbc.partner_withdraw_surplus",
"3688e18aacb6d6a7" => "meteora_dbc.protocol_withdraw_surplus",
"f8c69e91e17587c8" => "meteora_dbc.swap",
"414b3f4ceb5b5b88" => "meteora_dbc.swap2",
"1407a9213a93a621" => "meteora_dbc.transfer_pool_creator",
"14c6caedebf3b742" => "meteora_dbc.withdraw_leftover",
"ed8e2d178106dea2" => "meteora_dbc.withdraw_migration_fee",
"d59bbb2238b65bf0" => "meteora_dbc.zap_protocol_fee",
_ => return None,
};
return Some(name.to_string());
}
if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID || decoder_code == Some("raydium_amm_v4") {
let name = match discriminator_hex {
"00" => "raydium_amm_v4.initialize",
@@ -536,3 +575,31 @@ fn option_i64_key(value: std::option::Option<i64>) -> std::string::String {
None => return "-".to_string(),
}
}
#[cfg(test)]
mod tests {
#[test]
fn resolves_meteora_dbc_instruction_observation_names() {
let anchor_name = super::resolve_instruction_name(
crate::METEORA_DBC_PROGRAM_ID,
Some("meteora_dbc"),
Some("e445a52e51cb9a1d"),
);
assert_eq!(anchor_name.as_deref(), Some("meteora_dbc.anchor_self_cpi_log"));
let swap2_name = super::resolve_instruction_name(
crate::METEORA_DBC_PROGRAM_ID,
Some("meteora_dbc"),
Some("414b3f4ceb5b5b88"),
);
assert_eq!(swap2_name.as_deref(), Some("meteora_dbc.swap2"));
let create_name = super::resolve_instruction_name(
crate::METEORA_DBC_PROGRAM_ID,
Some("meteora_dbc"),
Some("8c55d7b06636684f"),
);
assert_eq!(
create_name.as_deref(),
Some("meteora_dbc.initialize_virtual_pool_with_spl_token")
);
}
}

View File

@@ -497,6 +497,10 @@ pub use db::DexEventCoverageEntryEntity;
pub use db::DexEventCoverageSummaryDto;
/// Aggregated DEX event coverage summary row.
pub use db::DexEventCoverageSummaryEntity;
/// Application-facing normalized fee event amount leg DTO.
pub use db::FeeEventAmountDto;
/// Persisted normalized fee event amount leg row.
pub use db::FeeEventAmountEntity;
/// Normalized fee event persisted from useful non-trade DEX events.
pub use db::FeeEventDto;
/// Persisted fee event row.
@@ -655,10 +659,6 @@ pub use db::ProgramInstructionDiscriminatorRowEntity;
/// Aggregated instruction discriminator diagnostic row.
pub use db::ProgramInstructionDiscriminatorSummaryDto;
/// Application-facing protocol candidate DTO.
///
/// A protocol candidate records a program/instruction that should be inspected
/// later because it may correspond to an unsupported DEX, launch surface,
/// migration path or protocol-specific non-trade event.
pub use db::ProtocolCandidateDto;
/// Persisted protocol candidate row.
pub use db::ProtocolCandidateEntity;
@@ -804,13 +804,22 @@ pub use db::query_dexs_get_by_code;
pub use db::query_dexs_list;
/// Inserts or updates one normalized DEX row by code.
pub use db::query_dexs_upsert;
/// Deletes amount legs for one normalized fee event.
pub use db::query_fee_event_amounts_backfill_single_leg_from_fee_events;
/// Deletes fee amount legs for one parent fee event.
pub use db::query_fee_event_amounts_delete_by_fee_event_id;
/// Lists amount legs for one normalized fee event.
pub use db::query_fee_event_amounts_list_by_fee_event_id;
/// Replaces amount legs for one normalized fee event.
pub use db::query_fee_event_amounts_replace_for_fee_event;
/// Returns one fee event by decoded-event id.
pub use db::query_fee_events_get_by_decoded_event_id;
/// Lists recent fee events ordered from newest to oldest.
pub use db::query_fee_events_list_recent;
/// Inserts or updates one normalized fee event row.
pub use db::query_fee_events_upsert;
/// Inserts one on-chain observation row and returns its numeric id.
/// Inserts or updates a fee parent and atomically replaces its amount legs.
pub use db::query_fee_events_upsert_with_amount_legs;
/// Lists instruction-observation source rows for one transaction signature.
pub use db::query_instruction_observation_source_rows_list_by_signature;
/// Lists recent instruction-observation source rows.
@@ -984,8 +993,6 @@ pub use db::query_program_instruction_discriminator_summaries_list_by_program_id
/// Lists protocol candidate summaries ordered by investigation priority.
pub use db::query_protocol_candidate_summaries_list_by_priority;
/// Deletes protocol candidates for one transaction.
///
/// This is useful before recomputing candidates for a replayed transaction.
pub use db::query_protocol_candidates_delete_by_transaction_id;
/// Inserts one protocol candidate row.
pub use db::query_protocol_candidates_insert;
@@ -1133,6 +1140,8 @@ pub use dex::MeteoraDbcCreatePoolDecoded;
pub use dex::MeteoraDbcDecodedEvent;
/// Meteora DBC decoder.
pub use dex::MeteoraDbcDecoder;
/// Decoded Meteora DBC instruction or Anchor event.
pub use dex::MeteoraDbcInstructionDecoded;
/// Decoded Meteora DBC swap event.
pub use dex::MeteoraDbcSwapDecoded;
/// Decoded Meteora DLMM create-pool event.
@@ -1293,6 +1302,7 @@ pub use dex_event_classification::is_dex_admin_event_kind;
pub use dex_event_classification::is_dex_candle_candidate_event_kind;
/// Returns true for fee collection DEX events.
pub use dex_event_classification::is_dex_fee_event_kind;
/// Returns true for decoded audit-only events retained for corpus analysis.
pub use dex_event_classification::is_dex_informational_event_kind;
/// Returns true for launch or bonding-curve creation DEX events.
pub use dex_event_classification::is_dex_launch_event_kind;
@@ -1347,9 +1357,6 @@ pub use dex_support_matrix::dex_support_matrix_entry_by_program_id;
/// Returns all DEX support matrix entries as owned DTOs.
pub use dex_support_matrix::dex_support_matrix_entry_dtos;
/// Global error type used by the `kb_lib` crate.
///
/// The project intentionally avoids `anyhow` and `thiserror`, so this
/// enum centralizes the main error families with explicit textual messages.
pub use error::Error;
/// Generic asynchronous HTTP client.
pub use http_client::HttpClient;
@@ -1394,19 +1401,10 @@ pub use json_rpc_ws::JsonRpcWsRequest;
/// JSON-RPC 2.0 success response.
pub use json_rpc_ws::JsonRpcWsSuccessResponse;
/// Parses a JSON value into a JSON-RPC incoming message.
///
/// This parser accepts only server-originating incoming message shapes:
/// success responses, error responses, and notifications.
pub use json_rpc_ws::is_probable_json_rpc_object_text;
/// Parses a raw text message into a JSON-RPC incoming message.
///
/// This parser accepts only server-originating incoming message shapes:
/// success responses, error responses, and notifications.
pub use json_rpc_ws::parse_json_rpc_ws_incoming_text;
/// Parses a JSON value into a JSON-RPC incoming message.
///
/// This parser accepts only server-originating incoming message shapes:
/// success responses, error responses, and notifications.
pub use json_rpc_ws::parse_json_rpc_ws_incoming_value;
/// Result of one launch surface attribution.
pub use launch_origin::LaunchAttributionResult;
@@ -1467,14 +1465,8 @@ pub use pair_analytic_signal::PairAnalyticSignalService;
/// One pair-candle aggregation result.
pub use pair_candle_aggregation::PairCandleAggregationResult;
/// Pair-candle aggregation service.
///
/// This service materializes a small set of standard timeframes in base storage.
/// Arbitrary timeframes are rebuilt on demand through `PairCandleQueryService`.
pub use pair_candle_aggregation::PairCandleAggregationService;
/// Pair-candle query service.
///
/// Standard materialized timeframes are served from base storage.
/// Arbitrary timeframes are rebuilt on demand from `trade_events`.
pub use pair_candle_query::PairCandleQueryService;
/// Summary produced by a pair-symbol refresh pass.
pub use pair_symbol::PairSymbolRefreshResult;
@@ -1493,11 +1485,6 @@ pub use solana_pubsub_ws::SolanaWsTypedNotification;
/// Parses a Solana JSON-RPC notification into an official typed payload.
pub use solana_pubsub_ws::parse_solana_ws_typed_notification;
/// Parses a typed Solana PubSub notification from a generic websocket event.
///
/// This returns:
/// - `Ok(Some(...))` for JSON-RPC notification-bearing events
/// - `Ok(None)` for events that do not carry a notification
/// - `Err(...)` when a notification is present but cannot be decoded
pub use solana_pubsub_ws::parse_solana_ws_typed_notification_from_event;
/// One pool-backfill result summary.
pub use token_backfill::PoolBackfillResult;
@@ -1510,22 +1497,14 @@ pub use token_backfill::SignatureBatchBackfillResult;
/// One token-backfill result summary.
pub use token_backfill::TokenBackfillResult;
/// Historical token backfill service.
///
/// This service reuses the existing transaction projection and downstream
/// DEX pipeline instead of introducing a separate historical code path.
pub use token_backfill::TokenBackfillService;
/// Summary produced by a token metadata backfill pass.
pub use token_metadata::TokenMetadataBackfillResult;
/// Service that enriches persisted token rows with mint and display metadata.
pub use token_metadata::TokenMetadataBackfillService;
/// Guard keeping non-blocking tracing workers alive.
///
/// The guard must remain alive during the whole application lifetime so that
/// buffered log records are flushed correctly.
pub use tracing::TracingGuard;
/// Initializes the global tracing subscriber.
///
/// This function is expected to be called once at application startup.
pub use tracing::init_tracing;
/// One trade-aggregation result.
pub use trade_aggregation::TradeAggregationResult;
@@ -1543,8 +1522,7 @@ pub use tx_resolution::TransactionResolutionRequest;
pub use tx_resolution::TransactionResolutionService;
/// One forwarded WebSocket notification envelope for transaction resolution.
pub use tx_resolution::WsTransactionResolutionEnvelope;
/// Relay that consumes forwarded WS notifications and resolves matching
/// signatures through HTTP `getTransaction`.
/// Relay that consumes forwarded WS notifications and resolves matching signatures through HTTP `getTransaction`.
pub use tx_resolution::WsTransactionResolutionRelay;
/// Runtime statistics for one transaction resolution relay worker.
pub use tx_resolution::WsTransactionResolutionRelayStats;

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,24 @@ impl TradeAggregationService {
continue;
},
};
let payload_base_vault_address =
crate::trade_aggregation::extract_payload_string_by_candidate_keys(
&payload,
&["baseVault", "base_vault", "baseVaultAddress", "base_vault_address"],
);
let payload_quote_vault_address =
crate::trade_aggregation::extract_payload_string_by_candidate_keys(
&payload,
&["quoteVault", "quote_vault", "quoteVaultAddress", "quote_vault_address"],
);
let effective_base_vault_address = match base_vault_address.as_deref() {
Some(base_vault_address) => Some(base_vault_address),
None => payload_base_vault_address.as_deref(),
};
let effective_quote_vault_address = match quote_vault_address.as_deref() {
Some(quote_vault_address) => Some(quote_vault_address),
None => payload_quote_vault_address.as_deref(),
};
if !crate::is_decoded_event_trade_candidate(decoded_event.event_kind.as_str(), &payload)
{
tracing::debug!(
@@ -150,8 +168,8 @@ impl TradeAggregationService {
quote_token_mint: quote_token_mint.as_deref(),
base_token_decimals,
quote_token_decimals,
base_vault_address: base_vault_address.as_deref(),
quote_vault_address: quote_vault_address.as_deref(),
base_vault_address: effective_base_vault_address,
quote_vault_address: effective_quote_vault_address,
};
let amount_resolution =
crate::trade_amount_resolution::resolve_trade_amounts(&amount_input).await;
@@ -212,6 +230,29 @@ impl TradeAggregationService {
}
}
fn extract_payload_string_by_candidate_keys(
payload: &serde_json::Value,
keys: &[&str],
) -> std::option::Option<std::string::String> {
let object = match payload.as_object() {
Some(object) => object,
None => return None,
};
for key in keys {
let value = object.get(*key);
let value = match value {
Some(value) => value,
None => continue,
};
if let Some(text) = value.as_str() {
if !text.trim().is_empty() {
return Some(text.to_string());
}
}
}
return None;
}
fn should_skip_pump_fun_duplicate_trade_event(
decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto],

View File

@@ -253,6 +253,36 @@ pub(crate) async fn resolve_trade_amounts(
return Err(error);
}
}
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
let resolution_result =
crate::trade_amount_resolution::apply_meteora_dbc_payload_transfer_amount_fallback(
input,
&mut base_amount_raw,
&mut quote_amount_raw,
&mut resolved_trade_side,
)
.await;
if let Err(error) = resolution_result {
return Err(error);
}
}
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
let resolution_result =
crate::trade_amount_resolution::apply_meteora_dbc_flattened_cpi_amount_fallback(
input,
&mut base_amount_raw,
&mut quote_amount_raw,
&mut resolved_trade_side,
)
.await;
if let Err(error) = resolution_result {
return Err(error);
}
}
if input.decoded_event.event_kind.starts_with("meteora_dlmm.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
@@ -283,6 +313,21 @@ pub(crate) async fn resolve_trade_amounts(
return Err(error);
}
}
if input.decoded_event.event_kind.starts_with("meteora_dbc.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback(
input,
input.base_vault_address,
input.quote_vault_address,
&mut base_amount_raw,
&mut quote_amount_raw,
&mut price_quote_per_base,
);
if let Err(error) = resolution_result {
return Err(error);
}
}
if input.decoded_event.event_kind.starts_with("raydium_amm_v4.") {
let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas(
input.transaction.meta_json.as_deref(),
@@ -316,6 +361,17 @@ pub(crate) async fn resolve_trade_amounts(
resolved_trade_side = vault_side;
}
}
if input.decoded_event.event_kind.starts_with("meteora_dbc.") {
let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas(
input.transaction.meta_json.as_deref(),
input.transaction.transaction_json.as_str(),
input.base_vault_address,
input.quote_vault_address,
);
if vault_side.is_some() {
resolved_trade_side = vault_side;
}
}
if price_quote_per_base.is_none() {
price_quote_per_base =
crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts_with_decimals(
@@ -452,10 +508,7 @@ async fn apply_pump_swap_amount_fallbacks(
};
let (input_vault_address, output_vault_address, input_token_account, output_token_account) =
if input.decoded_event.event_kind.ends_with(".buy")
|| input
.decoded_event
.event_kind
.ends_with(".buy_exact_quote_in")
|| input.decoded_event.event_kind.ends_with(".buy_exact_quote_in")
{
(
effective_quote_vault_address,
@@ -815,13 +868,14 @@ async fn apply_pump_fun_amount_fallback(
*price_quote_per_base = inferred.2;
}
if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() {
let sibling_result = crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
input,
base_amount_raw,
quote_amount_raw,
price_quote_per_base,
)
.await;
let sibling_result =
crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
input,
base_amount_raw,
quote_amount_raw,
price_quote_per_base,
)
.await;
if let Err(error) = sibling_result {
return Err(error);
}
@@ -1111,6 +1165,160 @@ async fn apply_meteora_damm_v1_flattened_cpi_amount_fallback(
.await;
}
async fn apply_meteora_dbc_flattened_cpi_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
) -> Result<(), crate::Error> {
return crate::trade_amount_resolution::apply_flattened_cpi_amount_fallback(
input,
"meteora_dbc",
base_amount_raw,
quote_amount_raw,
resolved_trade_side,
)
.await;
}
async fn apply_meteora_dbc_payload_transfer_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
) -> Result<(), crate::Error> {
let payload_token_a_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
input.payload,
&["tokenAMint", "token_a_mint", "baseMint", "base_mint"],
);
let payload_token_b_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
input.payload,
&["tokenBMint", "token_b_mint", "quoteMint", "quote_mint"],
);
let payload_base_vault = crate::trade_amount_resolution::extract_string_by_candidate_keys(
input.payload,
&["baseVault", "base_vault", "baseVaultAddress", "base_vault_address"],
);
let payload_quote_vault = crate::trade_amount_resolution::extract_string_by_candidate_keys(
input.payload,
&["quoteVault", "quote_vault", "quoteVaultAddress", "quote_vault_address"],
);
let payload_token_a_mint = match payload_token_a_mint {
Some(payload_token_a_mint) => payload_token_a_mint,
None => return Ok(()),
};
let payload_token_b_mint = match payload_token_b_mint {
Some(payload_token_b_mint) => payload_token_b_mint,
None => return Ok(()),
};
let payload_base_vault = match payload_base_vault {
Some(payload_base_vault) => payload_base_vault,
None => return Ok(()),
};
let payload_quote_vault = match payload_quote_vault {
Some(payload_quote_vault) => payload_quote_vault,
None => return Ok(()),
};
let decoded_instruction_result = crate::trade_amount_resolution::load_decoded_instruction(
input.database,
input.decoded_event,
)
.await;
let decoded_instruction = match decoded_instruction_result {
Ok(Some(decoded_instruction)) => decoded_instruction,
Ok(None) => return Ok(()),
Err(error) => return Err(error),
};
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
input.database,
input.decoded_event.transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
};
let payload_order_result = resolve_amounts_from_flattened_cpi_transfer_window(
&decoded_instruction,
&instructions,
Some(payload_token_a_mint.as_str()),
Some(payload_token_b_mint.as_str()),
Some(payload_base_vault.as_str()),
Some(payload_quote_vault.as_str()),
);
let payload_order = match payload_order_result {
Ok(payload_order) => payload_order,
Err(error) => return Err(error),
};
if payload_order.base_amount_raw.is_none() || payload_order.quote_amount_raw.is_none() {
return Ok(());
}
if crate::trade_amount_resolution::optional_mint_pair_matches_payload_order(
input.base_token_mint,
input.quote_token_mint,
payload_token_a_mint.as_str(),
payload_token_b_mint.as_str(),
) {
if base_amount_raw.is_none() {
*base_amount_raw = payload_order.base_amount_raw;
}
if quote_amount_raw.is_none() {
*quote_amount_raw = payload_order.quote_amount_raw;
}
if resolved_trade_side.is_none() {
*resolved_trade_side = payload_order.resolved_trade_side;
}
tracing::debug!(
event_kind = %input.decoded_event.event_kind,
decoded_event_id = ?input.decoded_event.id,
transaction_signature = %input.transaction.signature,
pool_account = ?input.decoded_event.pool_account,
payload_token_a_mint = %payload_token_a_mint,
payload_token_b_mint = %payload_token_b_mint,
payload_base_vault = %payload_base_vault,
payload_quote_vault = %payload_quote_vault,
base_amount_raw = ?base_amount_raw,
quote_amount_raw = ?quote_amount_raw,
resolved_trade_side = ?resolved_trade_side,
"meteora_dbc trade amounts recovered from payload-token CPI transfer window"
);
return Ok(());
}
if crate::trade_amount_resolution::optional_mint_pair_matches_payload_reverse_order(
input.base_token_mint,
input.quote_token_mint,
payload_token_a_mint.as_str(),
payload_token_b_mint.as_str(),
) {
if base_amount_raw.is_none() {
*base_amount_raw = payload_order.quote_amount_raw;
}
if quote_amount_raw.is_none() {
*quote_amount_raw = payload_order.base_amount_raw;
}
if resolved_trade_side.is_none() {
*resolved_trade_side = crate::trade_amount_resolution::reverse_swap_trade_side(
payload_order.resolved_trade_side,
);
}
tracing::debug!(
event_kind = %input.decoded_event.event_kind,
decoded_event_id = ?input.decoded_event.id,
transaction_signature = %input.transaction.signature,
pool_account = ?input.decoded_event.pool_account,
payload_token_a_mint = %payload_token_a_mint,
payload_token_b_mint = %payload_token_b_mint,
payload_base_vault = %payload_base_vault,
payload_quote_vault = %payload_quote_vault,
base_amount_raw = ?base_amount_raw,
quote_amount_raw = ?quote_amount_raw,
resolved_trade_side = ?resolved_trade_side,
"meteora_dbc trade amounts recovered from reversed payload-token CPI transfer window"
);
}
return Ok(());
}
async fn apply_flattened_cpi_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
protocol_label: &str,
@@ -1488,6 +1696,41 @@ fn infer_trade_side_from_transfer_directions(
}
}
fn optional_mint_pair_matches_payload_order(
base_token_mint: std::option::Option<&str>,
quote_token_mint: std::option::Option<&str>,
payload_token_a_mint: &str,
payload_token_b_mint: &str,
) -> bool {
return crate::trade_amount_resolution::string_option_equals(
base_token_mint,
payload_token_a_mint,
) && crate::trade_amount_resolution::string_option_equals(quote_token_mint, payload_token_b_mint);
}
fn optional_mint_pair_matches_payload_reverse_order(
base_token_mint: std::option::Option<&str>,
quote_token_mint: std::option::Option<&str>,
payload_token_a_mint: &str,
payload_token_b_mint: &str,
) -> bool {
return crate::trade_amount_resolution::string_option_equals(
base_token_mint,
payload_token_b_mint,
) && crate::trade_amount_resolution::string_option_equals(quote_token_mint, payload_token_a_mint);
}
fn reverse_swap_trade_side(
trade_side: std::option::Option<crate::SwapTradeSide>,
) -> std::option::Option<crate::SwapTradeSide> {
match trade_side {
Some(crate::SwapTradeSide::BuyBase) => return Some(crate::SwapTradeSide::SellBase),
Some(crate::SwapTradeSide::SellBase) => return Some(crate::SwapTradeSide::BuyBase),
Some(crate::SwapTradeSide::Unknown) => return Some(crate::SwapTradeSide::Unknown),
None => return None,
}
}
fn string_option_equals(left: std::option::Option<&str>, right: &str) -> bool {
let left = match left {
Some(left) => left.trim(),
@@ -1589,7 +1832,6 @@ async fn load_decoded_instruction(
return Ok(instruction_option);
}
fn normalize_pump_swap_anchor_buy_exact_quote_in_amounts(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,