This commit is contained in:
2026-06-09 10:13:03 +02:00
parent f2ea1a392f
commit bfdb2e69ae
41 changed files with 4485 additions and 1124 deletions

View File

@@ -155,8 +155,13 @@ pub use queries::query_db_runtime_events_list_recent;
pub use queries::query_dex_decode_replay_ledger_get_by_signature;
pub use queries::query_dex_decode_replay_ledger_get_by_transaction;
pub use queries::query_dex_decode_replay_ledger_upsert;
pub use queries::query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits;
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_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;
@@ -165,6 +170,7 @@ 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_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;
@@ -177,6 +183,11 @@ pub use queries::query_dexs_upsert;
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;
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;
@@ -188,6 +199,8 @@ pub use queries::query_known_ws_endpoints_upsert;
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;
@@ -237,6 +250,7 @@ pub use queries::query_pairs_get_by_pool_id;
pub use queries::query_pairs_list;
pub use queries::query_pairs_update_symbol;
pub use queries::query_pairs_upsert;
pub use queries::query_pool_admin_events_delete_by_decoded_event_id;
pub use queries::query_pool_admin_events_get_by_decoded_event_id;
pub use queries::query_pool_admin_events_list_recent;
pub use queries::query_pool_admin_events_upsert;

View File

@@ -13,10 +13,11 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event;
mod dex_event_coverage_entry;
mod fee_event;
mod instruction_observation;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution;
mod launch_event;
mod launch_surface;
mod launch_surface_key;
mod liquidity_event;
@@ -72,6 +73,8 @@ pub(crate) use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersRow;
pub(crate) use local_pipeline_diagnostics::LocalPoolOriginDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalRaydiumProgramInstructionDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalTokenMetadataGapDiagnosticSampleRow;
pub(crate) use program_instruction_discriminator_summary::ProgramInstructionDiscriminatorSummaryAccumulator;
pub(crate) use program_instruction_discriminator_summary::ProgramInstructionDiscriminatorSummaryKey;
pub use analysis_signal::AnalysisSignalDto;
pub use chain_instruction::ChainInstructionDto;
@@ -85,10 +88,12 @@ 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 instruction_observation::InstructionObservationDto;
pub use instruction_observation::InstructionObservationSourceRow;
pub use known_http_endpoint::KnownHttpEndpointDto;
pub use known_ws_endpoint::KnownWsEndpointDto;
pub use instruction_observation::InstructionObservationDto;
pub use launch_attribution::LaunchAttributionDto;
pub use launch_event::LaunchEventUpsertInput;
pub use launch_surface::LaunchSurfaceDto;
pub use launch_surface_key::LaunchSurfaceKeyDto;
pub use liquidity_event::LiquidityEventDto;

View File

@@ -31,7 +31,7 @@ pub struct InstructionObservationDto {
pub program_id: std::string::String,
/// Local decoder code when resolved.
pub decoder_code: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower-hex.
/// Instruction discriminator bytes as lower-hex; AMM v4 uses one byte, Anchor-style programs use eight bytes.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Known local instruction name when resolved.
pub instruction_name: std::option::Option<std::string::String>,
@@ -51,6 +51,41 @@ pub struct InstructionObservationDto {
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// Raw source row used to rebuild the technical instruction-observation index.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct InstructionObservationSourceRow {
/// Internal transaction id.
pub transaction_id: i64,
/// Transaction signature.
pub signature: std::string::String,
/// Optional slot number.
pub slot: std::option::Option<i64>,
/// Optional block time.
pub block_time: std::option::Option<i64>,
/// Optional transaction error JSON.
pub err_json: std::option::Option<std::string::String>,
/// Internal instruction id.
pub instruction_id: i64,
/// Optional parent instruction id for CPI rows.
pub parent_instruction_id: std::option::Option<i64>,
/// Outer instruction index.
pub instruction_index: i64,
/// Optional inner instruction index.
pub inner_instruction_index: std::option::Option<i64>,
/// Program id, when available.
pub program_id: std::option::Option<std::string::String>,
/// Instruction account list JSON.
pub accounts_json: std::string::String,
/// Optional instruction data JSON.
pub data_json: std::option::Option<std::string::String>,
/// Optional pool account resolved from a decoded event on the same instruction.
pub pool_account: std::option::Option<std::string::String>,
/// Optional decoded event kind on the same instruction.
pub decoded_event_kind: std::option::Option<std::string::String>,
/// Optional decoded event id on the same instruction.
pub decoded_event_id: std::option::Option<i64>,
}
impl InstructionObservationDto {
/// Creates a new instruction observation DTO.
#[allow(clippy::too_many_arguments)]

View File

@@ -0,0 +1,41 @@
// file: kb_lib/src/db/dtos/launch_event.rs
//! Launch event DTO helpers.
/// Input used to upsert one launch event row.
#[derive(Debug, Clone)]
pub struct LaunchEventUpsertInput {
/// Source transaction id.
pub transaction_id: i64,
/// Source decoded event id.
pub decoded_event_id: i64,
/// Optional DEX id.
pub dex_id: std::option::Option<i64>,
/// Optional pool id.
pub pool_id: std::option::Option<i64>,
/// Optional pair id.
pub pair_id: std::option::Option<i64>,
/// Transaction signature.
pub signature: std::string::String,
/// Optional slot number stored as i64.
pub slot: std::option::Option<i64>,
/// Protocol code.
pub protocol_name: std::string::String,
/// Program id.
pub program_id: std::string::String,
/// Decoded event kind.
pub event_kind: std::string::String,
/// Optional pool account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional actor wallet.
pub actor_wallet: std::option::Option<std::string::String>,
/// Launch event role.
pub event_role: std::string::String,
/// Optional related account.
pub related_account: std::option::Option<std::string::String>,
/// Optional related mint.
pub related_mint: std::option::Option<std::string::String>,
/// Raw decoded payload JSON.
pub payload_json: serde_json::Value,
}

View File

@@ -44,3 +44,32 @@ pub struct ProgramInstructionDiscriminatorSummaryDto {
/// Accounts JSON preview from the latest row.
pub latest_accounts_json_preview: std::option::Option<std::string::String>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct ProgramInstructionDiscriminatorSummaryKey {
pub(crate) program_id: std::string::String,
pub(crate) discriminator_hex: std::option::Option<std::string::String>,
pub(crate) accounts_count: u64,
pub(crate) stack_height: std::option::Option<u32>,
pub(crate) is_inner_instruction: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ProgramInstructionDiscriminatorSummaryAccumulator {
pub(crate) key: ProgramInstructionDiscriminatorSummaryKey,
pub(crate) known_instruction_name: std::option::Option<std::string::String>,
pub(crate) occurrence_count: u64,
pub(crate) decoded_event_count: u64,
pub(crate) transaction_signatures: std::collections::BTreeSet<std::string::String>,
pub(crate) latest_slot: std::option::Option<u64>,
pub(crate) latest_signature: std::string::String,
pub(crate) latest_instruction_id: i64,
pub(crate) latest_instruction_index: u32,
pub(crate) latest_inner_instruction_index: std::option::Option<u32>,
pub(crate) latest_parsed_type: std::option::Option<std::string::String>,
pub(crate) latest_decoded_event_kind: std::option::Option<std::string::String>,
pub(crate) latest_data_json_preview: std::option::Option<std::string::String>,
pub(crate) latest_accounts_json_preview: std::option::Option<std::string::String>,
}

View File

@@ -17,6 +17,7 @@ mod instruction_observation;
mod known_http_endpoint;
mod known_ws_endpoint;
mod launch_attribution;
mod launch_event;
mod launch_surface;
mod launch_surface_key;
mod liquidity_event;
@@ -73,15 +74,21 @@ pub use dex::query_dexs_upsert;
pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_signature;
pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_transaction;
pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_upsert;
pub use dex_decoded_event::query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits;
pub use dex_decoded_event::query_dex_decoded_events_delete_by_key;
pub use dex_decoded_event::query_dex_decoded_events_delete_instruction_audit_by_discriminator;
pub use dex_decoded_event::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id;
pub use dex_decoded_event::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
pub use dex_decoded_event::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
pub use dex_decoded_event::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator;
pub use dex_decoded_event::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit;
pub use dex_decoded_event::query_dex_decoded_events_delete_related_instruction_audit;
pub use dex_decoded_event::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
pub use dex_decoded_event::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits;
pub use dex_decoded_event::query_dex_decoded_events_get_by_key;
pub use dex_decoded_event::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use dex_decoded_event::query_dex_decoded_events_list_by_transaction_id;
pub use dex_decoded_event::query_dex_decoded_events_update_payload_json_by_id;
pub use dex_decoded_event::query_dex_decoded_events_upsert;
pub use dex_event_coverage_entry::query_dex_event_coverage_entries_delete_by_decoder;
pub use dex_event_coverage_entry::query_dex_event_coverage_entries_list_by_decoder;
@@ -92,6 +99,10 @@ 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 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;
pub use instruction_observation::query_instruction_observations_delete_by_transaction_ids;
pub use instruction_observation::query_instruction_observations_list_by_filter;
pub use instruction_observation::query_instruction_observations_upsert;
pub use known_http_endpoint::query_known_http_endpoints_get;
@@ -103,6 +114,7 @@ pub use known_ws_endpoint::query_known_ws_endpoints_upsert;
pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id;
pub use launch_attribution::query_launch_attributions_list_by_pool_id;
pub use launch_attribution::query_launch_attributions_upsert;
pub use launch_event::query_launch_events_upsert;
pub use launch_surface::query_launch_surfaces_get_by_code;
pub use launch_surface::query_launch_surfaces_list;
pub use launch_surface::query_launch_surfaces_upsert;
@@ -155,6 +167,7 @@ pub use pair_metric::query_pair_metrics_upsert;
pub use pool::query_pools_get_by_address;
pub use pool::query_pools_list;
pub use pool::query_pools_upsert;
pub use pool_admin_event::query_pool_admin_events_delete_by_decoded_event_id;
pub use pool_admin_event::query_pool_admin_events_get_by_decoded_event_id;
pub use pool_admin_event::query_pool_admin_events_list_recent;
pub use pool_admin_event::query_pool_admin_events_upsert;

View File

@@ -89,6 +89,135 @@ LIMIT 1
}
}
/// Deletes all locally decoded DEX rows and linked materialization for one transaction.
///
/// This reset is intentionally scoped by `protocol_name`, not by current coverage entries.
/// It removes legacy event kinds that no longer exist in the decoder, such as the old
/// `raydium_amm_v4.swap` row that was replaced by specialized AMM v4 swap variants.
pub async fn query_dex_decoded_events_delete_local_replay_scope_by_transaction_id(
database: &crate::Database,
transaction_id: i64,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let statements = [
(
"k_sol_instruction_observations decoded links",
r#"
UPDATE k_sol_instruction_observations
SET decoded_event_id = NULL
WHERE decoded_event_id IN (
SELECT id
FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name IN (
'raydium_amm_v4', 'raydium_cpmm', 'raydium_clmm', 'raydium_launchpad',
'pump_fun', 'pump_swap', 'meteora_dbc', 'meteora_dlmm', 'meteora_damm_v1',
'meteora_damm_v2', 'orca_whirlpools', 'fluxbeam', 'dexlab', 'openbook_v2',
'phoenix_v1'
)
)
"#,
),
(
"k_sol_launch_attributions",
"DELETE FROM k_sol_launch_attributions WHERE transaction_id = ?",
),
(
"k_sol_launch_events",
"DELETE FROM k_sol_launch_events WHERE transaction_id = ?",
),
("k_sol_trade_events", "DELETE FROM k_sol_trade_events WHERE transaction_id = ?"),
(
"k_sol_liquidity_events",
"DELETE FROM k_sol_liquidity_events WHERE transaction_id = ?",
),
(
"k_sol_pool_lifecycle_events",
"DELETE FROM k_sol_pool_lifecycle_events WHERE transaction_id = ?",
),
("k_sol_fee_events", "DELETE FROM k_sol_fee_events WHERE transaction_id = ?"),
(
"k_sol_reward_events",
"DELETE FROM k_sol_reward_events WHERE transaction_id = ?",
),
(
"k_sol_pool_admin_events",
"DELETE FROM k_sol_pool_admin_events WHERE transaction_id = ?",
),
(
"k_sol_orderbook_events",
"DELETE FROM k_sol_orderbook_events WHERE transaction_id = ?",
),
(
"k_sol_token_account_events",
"DELETE FROM k_sol_token_account_events WHERE transaction_id = ?",
),
(
"k_sol_dex_decoded_events",
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name IN (
'raydium_amm_v4', 'raydium_cpmm', 'raydium_clmm', 'raydium_launchpad',
'pump_fun', 'pump_swap', 'meteora_dbc', 'meteora_dlmm', 'meteora_damm_v1',
'meteora_damm_v2', 'orca_whirlpools', 'fluxbeam', 'dexlab', 'openbook_v2',
'phoenix_v1'
)
"#,
),
];
let mut deleted_count = 0_u64;
for (scope_name, statement) in statements {
let query_result = sqlx::query(statement).bind(transaction_id).execute(pool).await;
let result = match query_result {
Ok(result) => result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete local DEX replay scope '{}' for transaction id '{}' on sqlite: {}",
scope_name, transaction_id, error
)));
},
};
deleted_count = deleted_count.saturating_add(result.rows_affected());
}
return Ok(deleted_count);
},
}
}
/// Updates the persisted payload of one decoded DEX event row.
pub async fn query_dex_decoded_events_update_payload_json_by_id(
database: &crate::Database,
decoded_event_id: i64,
payload_json: &str,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
UPDATE k_sol_dex_decoded_events
SET payload_json = ?
WHERE id = ?
"#,
)
.bind(payload_json)
.bind(decoded_event_id)
.execute(pool)
.await;
match query_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot update k_sol_dex_decoded_events payload_json by id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
}
},
}
}
/// Deletes one decoded DEX event row by its natural key.
pub async fn query_dex_decoded_events_delete_by_key(
database: &crate::Database,
@@ -698,6 +827,195 @@ LIMIT 1
}
}
/// Deletes a Raydium CLMM instruction-audit row by discriminator for one transaction.
pub async fn query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator(
database: &crate::Database,
transaction_id: i64,
discriminator_hex: &str,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = 'raydium_clmm'
AND event_kind = 'raydium_clmm.instruction_audit'
AND (
json_extract(payload_json, '$.discriminatorHex') = ?
OR json_extract(payload_json, '$.discriminator_hex') = ?
OR json_extract(payload_json, '$.instructionDiscriminatorHex') = ?
OR json_extract(payload_json, '$.instruction_discriminator_hex') = ?
OR json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
OR json_extract(payload_json, '$.anchor_event_discriminator_hex') = ?
)
"#,
)
.bind(transaction_id)
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete Raydium CLMM residual instruction audit '{}': {}",
discriminator_hex, error
)));
},
}
},
}
}
/// Deletes a protocol instruction-audit row by discriminator for one transaction.
pub async fn query_dex_decoded_events_delete_instruction_audit_by_discriminator(
database: &crate::Database,
transaction_id: i64,
protocol_name: &str,
audit_event_kind: &str,
discriminator_hex: &str,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = ?
AND event_kind = ?
AND (
json_extract(payload_json, '$.discriminatorHex') = ?
OR json_extract(payload_json, '$.discriminator_hex') = ?
OR json_extract(payload_json, '$.instructionDiscriminatorHex') = ?
OR json_extract(payload_json, '$.instruction_discriminator_hex') = ?
OR json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
OR json_extract(payload_json, '$.anchor_event_discriminator_hex') = ?
OR instr(lower(COALESCE(payload_json, '')), lower(?)) > 0
)
"#,
)
.bind(transaction_id)
.bind(protocol_name.to_string())
.bind(audit_event_kind.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete replaced instruction audit by discriminator on sqlite: {}",
error
)));
},
}
},
}
}
/// Deletes a Raydium Launchpad self-CPI audit row replaced by a direct decoded event.
pub async fn query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit(
database: &crate::Database,
transaction_id: i64,
anchor_self_cpi_log_selector_hex: &str,
anchor_event_discriminator_hex: &str,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = 'raydium_launchpad'
AND event_kind = 'raydium_launchpad.instruction_audit'
AND json_extract(payload_json, '$.anchorSelfCpiLog') = 1
AND json_extract(payload_json, '$.anchorSelfCpiLogSelectorHex') = ?
AND json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
"#,
)
.bind(transaction_id)
.bind(anchor_self_cpi_log_selector_hex.to_string())
.bind(anchor_event_discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete replaced Raydium Launchpad self-CPI instruction audit on sqlite: {}",
error
)));
},
}
},
}
}
/// Deletes Raydium Launchpad self-CPI audit rows once their direct decoded rows exist.
pub async fn query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits(
database: &crate::Database,
transaction_id: i64,
anchor_self_cpi_log_selector_hex: &str,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE id IN (
SELECT audit.id
FROM k_sol_dex_decoded_events audit
WHERE audit.transaction_id = ?
AND audit.protocol_name = 'raydium_launchpad'
AND audit.event_kind = 'raydium_launchpad.instruction_audit'
AND json_extract(audit.payload_json, '$.anchorSelfCpiLog') = 1
AND json_extract(audit.payload_json, '$.anchorSelfCpiLogSelectorHex') = ?
AND EXISTS (
SELECT 1
FROM k_sol_dex_decoded_events direct
WHERE direct.transaction_id = audit.transaction_id
AND direct.protocol_name = 'raydium_launchpad'
AND direct.event_kind IN (
'raydium_launchpad.trade_event',
'raydium_launchpad.pool_create_event',
'raydium_launchpad.claim_vested_event',
'raydium_launchpad.create_vesting_event'
)
AND json_extract(direct.payload_json, '$.anchorEventDiscriminatorHex') =
json_extract(audit.payload_json, '$.anchorEventDiscriminatorHex')
)
)
"#,
)
.bind(transaction_id)
.bind(anchor_self_cpi_log_selector_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot cleanup replaced Raydium Launchpad self-CPI instruction audits on sqlite: {}",
error
)));
},
}
},
}
}
#[cfg(test)]
mod tests {
async fn make_database() -> crate::Database {

View File

@@ -171,3 +171,218 @@ LIMIT ?
},
}
}
/// Deletes instruction observations for a set of transaction ids before rebuilding the technical index.
pub async fn query_instruction_observations_delete_by_transaction_ids(
database: &crate::Database,
transaction_ids: &[i64],
) -> Result<u64, crate::Error> {
if transaction_ids.is_empty() {
return Ok(0);
}
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let mut deleted_count: u64 = 0;
for transaction_id_chunk in transaction_ids.chunks(900) {
let mut query_builder = sqlx::QueryBuilder::<sqlx::Sqlite>::new(
"DELETE FROM k_sol_instruction_observations WHERE transaction_id IN (",
);
let mut separated = query_builder.separated(", ");
for transaction_id in transaction_id_chunk {
separated.push_bind(*transaction_id);
}
separated.push_unseparated(")");
let query = query_builder.build();
let query_result = query.execute(pool).await;
match query_result {
Ok(query_result) => {
deleted_count = deleted_count.saturating_add(query_result.rows_affected());
},
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete k_sol_instruction_observations by transaction ids on sqlite: {}",
error
)));
},
}
}
return Ok(deleted_count);
},
}
}
/// Lists instruction-observation source rows for one transaction signature.
pub async fn query_instruction_observation_source_rows_list_by_signature(
database: &crate::Database,
signature: &str,
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
WHERE tx.signature = ?
ORDER BY ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(signature.to_string())
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for signature '{}': {}",
signature, error
)));
},
}
},
}
}
/// Lists instruction-observation source rows for the local replay window.
pub async fn query_instruction_observation_source_rows_list_replay_window(
database: &crate::Database,
limit: std::option::Option<i64>,
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
let effective_limit = match limit {
Some(limit) => {
if limit <= 0 {
10_000
} else {
limit
}
},
None => 10_000,
};
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::InstructionObservationSourceRow>(
r#"
WITH replay_transactions AS (
SELECT id
FROM k_sol_chain_transactions
ORDER BY id ASC
LIMIT ?
)
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN replay_transactions replay_tx
ON replay_tx.id = ins.transaction_id
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY tx.id ASC, ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(effective_limit)
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for replay window: {}",
error
)));
},
}
},
}
}
/// Lists recent instruction-observation source rows.
pub async fn query_instruction_observation_source_rows_list_recent(
database: &crate::Database,
limit: u32,
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY ins.id DESC
LIMIT ?
"#,
)
.bind(i64::from(limit))
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list recent instruction observation source rows: {}",
error
)));
},
}
},
}
}

View File

@@ -0,0 +1,139 @@
// file: kb_lib/src/db/queries/launch_event.rs
//! Queries for `k_sol_launch_events`.
/// Inserts or updates one launch event by decoded event id.
pub async fn query_launch_events_upsert(
database: &crate::Database,
input: &crate::LaunchEventUpsertInput,
) -> Result<i64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let existing_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_launch_events
WHERE decoded_event_id = ?
LIMIT 1
"#,
)
.bind(input.decoded_event_id)
.fetch_optional(pool)
.await;
let existing_id = match existing_result {
Ok(existing_id) => existing_id,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_launch_events id for decoded_event_id '{}' on sqlite: {}",
input.decoded_event_id, error
)));
},
};
if let Some(existing_id) = existing_id {
let update_result = sqlx::query(
r#"
UPDATE k_sol_launch_events
SET
transaction_id = ?,
dex_id = ?,
pool_id = ?,
pair_id = ?,
signature = ?,
slot = ?,
protocol_name = ?,
program_id = ?,
event_kind = ?,
pool_account = ?,
actor_wallet = ?,
event_role = ?,
related_account = ?,
related_mint = ?,
payload_json = ?,
executed_at = ?
WHERE id = ?
"#,
)
.bind(input.transaction_id)
.bind(input.dex_id)
.bind(input.pool_id)
.bind(input.pair_id)
.bind(input.signature.clone())
.bind(input.slot)
.bind(input.protocol_name.clone())
.bind(input.program_id.clone())
.bind(input.event_kind.clone())
.bind(input.pool_account.clone())
.bind(input.actor_wallet.clone())
.bind(input.event_role.clone())
.bind(input.related_account.clone())
.bind(input.related_mint.clone())
.bind(input.payload_json.clone())
.bind(chrono::Utc::now().to_rfc3339())
.bind(existing_id)
.execute(pool)
.await;
if let Err(error) = update_result {
return Err(crate::Error::Db(format!(
"cannot update k_sol_launch_events id '{}' on sqlite: {}",
existing_id, error
)));
}
return Ok(existing_id);
}
let insert_result = sqlx::query(
r#"
INSERT INTO k_sol_launch_events (
transaction_id,
decoded_event_id,
dex_id,
pool_id,
pair_id,
signature,
slot,
protocol_name,
program_id,
event_kind,
pool_account,
actor_wallet,
event_role,
related_account,
related_mint,
payload_json,
executed_at,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(input.transaction_id)
.bind(input.decoded_event_id)
.bind(input.dex_id)
.bind(input.pool_id)
.bind(input.pair_id)
.bind(input.signature.clone())
.bind(input.slot)
.bind(input.protocol_name.clone())
.bind(input.program_id.clone())
.bind(input.event_kind.clone())
.bind(input.pool_account.clone())
.bind(input.actor_wallet.clone())
.bind(input.event_role.clone())
.bind(input.related_account.clone())
.bind(input.related_mint.clone())
.bind(input.payload_json.clone())
.bind(chrono::Utc::now().to_rfc3339())
.bind(chrono::Utc::now().to_rfc3339())
.execute(pool)
.await;
match insert_result {
Ok(result) => return Ok(result.last_insert_rowid()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot insert k_sol_launch_events on sqlite: {}",
error
)));
},
}
},
}
}

View File

@@ -291,3 +291,32 @@ LIMIT ?
},
}
}
/// Deletes a stale pool admin materialization by decoded event id.
pub async fn query_pool_admin_events_delete_by_decoded_event_id(
database: &crate::Database,
decoded_event_id: i64,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_pool_admin_events
WHERE decoded_event_id = ?
"#,
)
.bind(decoded_event_id)
.execute(pool)
.await;
match delete_result {
Ok(delete_result) => return Ok(delete_result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete k_sol_pool_admin_events for decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
}
},
}
}

View File

@@ -73,39 +73,12 @@ LIMIT ?
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct ProgramInstructionDiscriminatorSummaryKey {
program_id: std::string::String,
discriminator_hex: std::option::Option<std::string::String>,
accounts_count: u64,
stack_height: std::option::Option<u32>,
is_inner_instruction: bool,
}
#[derive(Debug, Clone)]
struct ProgramInstructionDiscriminatorSummaryAccumulator {
key: ProgramInstructionDiscriminatorSummaryKey,
known_instruction_name: std::option::Option<std::string::String>,
occurrence_count: u64,
decoded_event_count: u64,
transaction_signatures: std::collections::BTreeSet<std::string::String>,
latest_slot: std::option::Option<u64>,
latest_signature: std::string::String,
latest_instruction_id: i64,
latest_instruction_index: u32,
latest_inner_instruction_index: std::option::Option<u32>,
latest_parsed_type: std::option::Option<std::string::String>,
latest_decoded_event_kind: std::option::Option<std::string::String>,
latest_data_json_preview: std::option::Option<std::string::String>,
latest_accounts_json_preview: std::option::Option<std::string::String>,
}
fn build_program_instruction_discriminator_summaries(
rows: std::vec::Vec<crate::ProgramInstructionDiscriminatorRowEntity>,
) -> Result<std::vec::Vec<crate::ProgramInstructionDiscriminatorSummaryDto>, crate::Error> {
let mut grouped = std::collections::BTreeMap::<
ProgramInstructionDiscriminatorSummaryKey,
ProgramInstructionDiscriminatorSummaryAccumulator,
crate::db::dtos::ProgramInstructionDiscriminatorSummaryKey,
crate::db::dtos::ProgramInstructionDiscriminatorSummaryAccumulator,
>::new();
for row in rows {
let summary_row_result = build_summary_row_from_discriminator_entity(row);
@@ -189,7 +162,7 @@ fn build_program_instruction_discriminator_summaries(
fn build_summary_row_from_discriminator_entity(
row: crate::ProgramInstructionDiscriminatorRowEntity,
) -> Result<ProgramInstructionDiscriminatorSummaryAccumulator, crate::Error> {
) -> Result<crate::db::dtos::ProgramInstructionDiscriminatorSummaryAccumulator, crate::Error> {
let program_id = match row.program_id.clone() {
Some(program_id) => program_id,
None => "unknown".to_string(),
@@ -252,14 +225,14 @@ fn build_summary_row_from_discriminator_entity(
let mut transaction_signatures = std::collections::BTreeSet::new();
transaction_signatures.insert(row.signature.clone());
let decoded_event_count = if row.decoded_event_id.is_some() { 1_u64 } else { 0_u64 };
let key = ProgramInstructionDiscriminatorSummaryKey {
let key = crate::db::dtos::ProgramInstructionDiscriminatorSummaryKey {
program_id,
discriminator_hex,
accounts_count,
stack_height,
is_inner_instruction: row.parent_instruction_id.is_some(),
};
return Ok(ProgramInstructionDiscriminatorSummaryAccumulator {
return Ok(crate::db::dtos::ProgramInstructionDiscriminatorSummaryAccumulator {
key,
known_instruction_name,
occurrence_count: 1,

View File

@@ -70,6 +70,7 @@ pub use pump_swap::PumpSwapTradeDecoded;
pub use raydium_amm_v4::RaydiumAmmV4DecodedEvent;
pub use raydium_amm_v4::RaydiumAmmV4Decoder;
pub use raydium_amm_v4::RaydiumAmmV4Initialize2PoolDecoded;
pub use raydium_amm_v4::RaydiumAmmV4InstructionDecoded;
pub use raydium_amm_v4::RaydiumAmmV4SwapDecoded;
pub use raydium_clmm::RaydiumClmmCollectProtocolFeeDecoded;
pub use raydium_clmm::RaydiumClmmCreatePoolDecoded;

View File

@@ -40,6 +40,12 @@ pub struct RaydiumAmmV4SwapDecoded {
pub signature: std::string::String,
/// Program id.
pub program_id: std::string::String,
/// Local decoded event kind derived from the AMM v4 instruction discriminator.
pub event_kind: std::string::String,
/// Upstream instruction name derived from the AMM v4 instruction discriminator.
pub instruction_name: std::string::String,
/// One-byte Raydium AMM v4 instruction discriminator in hex form.
pub discriminator_hex: std::string::String,
/// AMM pool/state account.
pub pool_account: std::string::String,
/// Raydium AMM authority account.
@@ -78,6 +84,49 @@ pub struct RaydiumAmmV4SwapDecoded {
pub payload_json: serde_json::Value,
}
/// Decoded Raydium AmmV4 non-swap or decoded-only instruction event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RaydiumAmmV4InstructionDecoded {
/// Parent transaction id.
pub transaction_id: i64,
/// Parent instruction id.
pub instruction_id: i64,
/// Transaction signature.
pub signature: std::string::String,
/// Program id.
pub program_id: std::string::String,
/// Local decoded event kind derived from the AMM v4 instruction discriminator.
pub event_kind: std::string::String,
/// Upstream instruction name derived from the AMM v4 instruction discriminator.
pub instruction_name: std::string::String,
/// One-byte Raydium AMM v4 instruction discriminator in hex form.
pub discriminator_hex: std::string::String,
/// Optional AMM pool/state account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional Raydium AMM authority account.
pub authority: std::option::Option<std::string::String>,
/// Optional AMM open-orders account.
pub open_orders: std::option::Option<std::string::String>,
/// Optional AMM target-orders account.
pub target_orders: std::option::Option<std::string::String>,
/// Optional market program account.
pub market_program: std::option::Option<std::string::String>,
/// Optional market account.
pub market_account: std::option::Option<std::string::String>,
/// Optional LP mint account.
pub lp_mint: std::option::Option<std::string::String>,
/// Optional token A mint after best-effort account layout extraction.
pub token_a_mint: std::option::Option<std::string::String>,
/// Optional token B mint after best-effort account layout extraction.
pub token_b_mint: std::option::Option<std::string::String>,
/// Optional AMM coin/base vault account.
pub base_vault: std::option::Option<std::string::String>,
/// Optional AMM pc/quote vault account.
pub quote_vault: std::option::Option<std::string::String>,
/// Decoded payload.
pub payload_json: serde_json::Value,
}
/// Decoded Raydium AmmV4 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumAmmV4DecodedEvent {
@@ -85,6 +134,8 @@ pub enum RaydiumAmmV4DecodedEvent {
Initialize2Pool(std::boxed::Box<RaydiumAmmV4Initialize2PoolDecoded>),
/// Swap event decoded from a direct or inner Raydium AMM v4 instruction.
Swap(std::boxed::Box<RaydiumAmmV4SwapDecoded>),
/// Known Raydium AMM v4 instruction decoded without direct trade materialization.
Instruction(std::boxed::Box<RaydiumAmmV4InstructionDecoded>),
}
/// Raydium AmmV4 decoder.
@@ -182,11 +233,27 @@ impl RaydiumAmmV4Decoder {
token_balances.as_slice(),
);
match swap_result {
Ok(Some(swap)) => decoded_events
.push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap))),
Ok(Some(swap)) => {
decoded_events
.push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap)));
continue;
},
Ok(None) => {},
Err(error) => return Err(error),
}
let instruction_event = decode_known_instruction_event(
transaction,
transaction_id,
instruction,
instruction_id,
program_id,
accounts.as_slice(),
);
if let Some(instruction_event) = instruction_event {
decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Instruction(
std::boxed::Box::new(instruction_event),
));
}
}
return Ok(decoded_events);
}
@@ -209,9 +276,15 @@ fn decode_initialize2_event(
let token_a_mint = extract_account(accounts, 8);
let token_b_mint = extract_account(accounts, 9);
let market_account = extract_account(accounts, 16);
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
let discriminator_hex = raydium_amm_v4_instruction_discriminator_hex(data_base58.as_deref());
let payload_json = serde_json::json!({
"decoder": "raydium_amm_v4",
"eventKind": "initialize2_pool",
"instructionName": "initialize2",
"upstreamInstructionName": "initialize2",
"discriminatorHex": discriminator_hex,
"instructionDiscriminatorHex": discriminator_hex,
"signature": transaction.signature,
"instructionId": instruction_id,
"instructionIndex": instruction.instruction_index,
@@ -251,6 +324,11 @@ fn decode_swap_event(
if accounts.len() < 8 {
return Ok(None);
}
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
let swap_instruction = match raydium_amm_v4_swap_instruction(data_base58.as_deref()) {
Some(swap_instruction) => swap_instruction,
None => return Ok(None),
};
let pool_account = match extract_account(accounts, 1) {
Some(pool_account) => pool_account,
None => return Ok(None),
@@ -294,11 +372,14 @@ fn decode_swap_event(
normalized_pair.quote_vault.as_str(),
);
let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions);
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
let trade_candidate = base_amount_raw.is_some() && quote_amount_raw.is_some();
let payload_json = serde_json::json!({
"decoder": "raydium_amm_v4",
"eventKind": "swap",
"eventKind": swap_instruction.event_kind,
"instructionName": swap_instruction.instruction_name,
"upstreamInstructionName": swap_instruction.instruction_name,
"discriminatorHex": swap_instruction.discriminator_hex,
"instructionDiscriminatorHex": swap_instruction.discriminator_hex,
"signature": transaction.signature,
"instructionId": instruction_id,
"instructionIndex": instruction.instruction_index,
@@ -337,6 +418,9 @@ fn decode_swap_event(
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
event_kind: swap_instruction.event_kind.to_string(),
instruction_name: swap_instruction.instruction_name.to_string(),
discriminator_hex: swap_instruction.discriminator_hex.to_string(),
pool_account,
authority,
token_a_mint: normalized_pair.base_mint,
@@ -358,6 +442,439 @@ fn decode_swap_event(
}));
}
fn decode_known_instruction_event(
transaction: &crate::ChainTransactionDto,
transaction_id: i64,
instruction: &crate::ChainInstructionDto,
instruction_id: i64,
program_id: &str,
accounts: &[std::string::String],
) -> std::option::Option<crate::RaydiumAmmV4InstructionDecoded> {
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
let data_bytes = raydium_amm_v4_instruction_data_bytes(data_base58.as_deref());
let identity = match raydium_amm_v4_instruction_identity(data_base58.as_deref()) {
Some(identity) => identity,
None => return None,
};
let account_layout = raydium_amm_v4_account_layout(identity.discriminator_hex);
let pool_account = extract_account_by_index(accounts, account_layout.pool_account_index);
let authority = extract_account_by_index(accounts, account_layout.authority_index);
let open_orders = extract_account_by_index(accounts, account_layout.open_orders_index);
let target_orders = extract_account_by_index(accounts, account_layout.target_orders_index);
let market_program = extract_account_by_index(accounts, account_layout.market_program_index);
let market_account = extract_account_by_index(accounts, account_layout.market_account_index);
let lp_mint = extract_account_by_index(accounts, account_layout.lp_mint_index);
let token_a_mint = extract_account_by_index(accounts, account_layout.token_a_mint_index);
let token_b_mint = extract_account_by_index(accounts, account_layout.token_b_mint_index);
let base_vault = extract_account_by_index(accounts, account_layout.base_vault_index);
let quote_vault = extract_account_by_index(accounts, account_layout.quote_vault_index);
let mut payload_json = serde_json::json!({
"decoder": "raydium_amm_v4",
"eventKind": identity.event_kind,
"instructionName": identity.instruction_name,
"upstreamInstructionName": identity.instruction_name,
"discriminatorHex": identity.discriminator_hex,
"instructionDiscriminatorHex": identity.discriminator_hex,
"signature": transaction.signature,
"instructionId": instruction_id,
"instructionIndex": instruction.instruction_index,
"innerInstructionIndex": instruction.inner_instruction_index,
"innerInstruction": instruction.inner_instruction_index.is_some(),
"parentInstructionId": instruction.parent_instruction_id,
"programId": program_id,
"accounts": accounts,
"data": data_base58,
"instructionDataLength": data_bytes.as_ref().map(std::vec::Vec::len),
"poolAccount": pool_account.clone(),
"authority": authority.clone(),
"openOrders": open_orders.clone(),
"targetOrders": target_orders.clone(),
"marketProgram": market_program.clone(),
"marketAccount": market_account.clone(),
"lpMint": lp_mint.clone(),
"tokenAMint": token_a_mint.clone(),
"tokenBMint": token_b_mint.clone(),
"baseVault": base_vault.clone(),
"quoteVault": quote_vault.clone(),
"tradeCandidate": false,
"candleCandidate": false,
"skipTradeReason": "decoded_only_instruction",
"skipCandleReason": "decoded_only_instruction"
});
enrich_known_instruction_payload(&mut payload_json, identity.discriminator_hex, data_bytes.as_deref());
return Some(crate::RaydiumAmmV4InstructionDecoded {
transaction_id,
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
event_kind: identity.event_kind.to_string(),
instruction_name: identity.instruction_name.to_string(),
discriminator_hex: identity.discriminator_hex.to_string(),
pool_account,
authority,
open_orders,
target_orders,
market_program,
market_account,
lp_mint,
token_a_mint,
token_b_mint,
base_vault,
quote_vault,
payload_json,
});
}
#[derive(Clone, Copy)]
struct RaydiumAmmV4AccountLayout {
pool_account_index: std::option::Option<usize>,
authority_index: std::option::Option<usize>,
open_orders_index: std::option::Option<usize>,
target_orders_index: std::option::Option<usize>,
market_program_index: std::option::Option<usize>,
market_account_index: std::option::Option<usize>,
lp_mint_index: std::option::Option<usize>,
token_a_mint_index: std::option::Option<usize>,
token_b_mint_index: std::option::Option<usize>,
base_vault_index: std::option::Option<usize>,
quote_vault_index: std::option::Option<usize>,
}
fn raydium_amm_v4_empty_account_layout() -> RaydiumAmmV4AccountLayout {
return RaydiumAmmV4AccountLayout {
pool_account_index: None,
authority_index: None,
open_orders_index: None,
target_orders_index: None,
market_program_index: None,
market_account_index: None,
lp_mint_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
base_vault_index: None,
quote_vault_index: None,
};
}
fn raydium_amm_v4_account_layout(discriminator_hex: &str) -> RaydiumAmmV4AccountLayout {
let mut layout = raydium_amm_v4_empty_account_layout();
match discriminator_hex {
"00" => {
layout.pool_account_index = Some(3);
},
"01" => {
layout.pool_account_index = Some(4);
layout.authority_index = Some(5);
layout.open_orders_index = Some(6);
layout.lp_mint_index = Some(7);
layout.token_a_mint_index = Some(8);
layout.token_b_mint_index = Some(9);
layout.base_vault_index = Some(10);
layout.quote_vault_index = Some(11);
layout.target_orders_index = Some(12);
layout.market_program_index = Some(15);
layout.market_account_index = Some(16);
},
"02" => {
layout.pool_account_index = Some(3);
layout.authority_index = Some(4);
layout.open_orders_index = Some(5);
layout.target_orders_index = Some(6);
layout.base_vault_index = Some(7);
layout.quote_vault_index = Some(8);
layout.market_program_index = Some(9);
layout.market_account_index = Some(10);
},
"03" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(2);
layout.open_orders_index = Some(3);
layout.target_orders_index = Some(4);
layout.lp_mint_index = Some(5);
layout.base_vault_index = Some(6);
layout.quote_vault_index = Some(7);
layout.market_account_index = Some(8);
},
"04" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(2);
layout.open_orders_index = Some(3);
layout.target_orders_index = Some(4);
layout.lp_mint_index = Some(5);
layout.base_vault_index = Some(6);
layout.quote_vault_index = Some(7);
layout.market_program_index = Some(8);
layout.market_account_index = Some(9);
},
"05" => {
layout.pool_account_index = Some(3);
layout.authority_index = Some(4);
layout.open_orders_index = Some(5);
layout.base_vault_index = Some(6);
layout.quote_vault_index = Some(7);
layout.target_orders_index = Some(8);
layout.market_program_index = Some(9);
layout.market_account_index = Some(10);
},
"06" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(2);
layout.open_orders_index = Some(3);
layout.target_orders_index = Some(4);
layout.base_vault_index = Some(5);
layout.quote_vault_index = Some(6);
layout.market_program_index = Some(7);
layout.market_account_index = Some(8);
},
"07" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(3);
layout.open_orders_index = Some(4);
layout.base_vault_index = Some(5);
layout.quote_vault_index = Some(6);
layout.target_orders_index = Some(10);
layout.market_program_index = Some(11);
layout.market_account_index = Some(12);
},
"08" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(3);
},
"09" | "0b" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(2);
layout.open_orders_index = Some(3);
layout.target_orders_index = Some(4);
layout.base_vault_index = Some(5);
layout.quote_vault_index = Some(6);
layout.market_program_index = Some(7);
layout.market_account_index = Some(8);
},
"0a" => {
layout.pool_account_index = Some(4);
},
"0c" => {
layout.pool_account_index = Some(1);
},
"0d" => {
layout.pool_account_index = Some(1);
},
"10" | "11" => {
layout.pool_account_index = Some(1);
layout.authority_index = Some(2);
layout.base_vault_index = Some(3);
layout.quote_vault_index = Some(4);
},
_ => {},
}
return layout;
}
fn extract_account_by_index(
accounts: &[std::string::String],
index: std::option::Option<usize>,
) -> std::option::Option<std::string::String> {
let index = match index {
Some(index) => index,
None => return None,
};
return extract_account(accounts, index);
}
fn raydium_amm_v4_instruction_data_bytes(
data_base58: std::option::Option<&str>,
) -> std::option::Option<std::vec::Vec<u8>> {
let data_base58 = match data_base58 {
Some(data_base58) => data_base58,
None => return None,
};
let bytes_result = bs58::decode(data_base58).into_vec();
match bytes_result {
Ok(bytes) => return Some(bytes),
Err(_) => return None,
}
}
fn enrich_known_instruction_payload(
payload_json: &mut serde_json::Value,
discriminator_hex: &str,
data: std::option::Option<&[u8]>,
) {
let data = match data {
Some(data) => data,
None => return,
};
match discriminator_hex {
"00" => {
insert_u8(payload_json, "nonce", data, 1);
insert_u64_string(payload_json, "openTime", data, 2);
},
"01" => {
insert_u8(payload_json, "nonce", data, 1);
insert_u64_string(payload_json, "openTime", data, 2);
insert_u64_string(payload_json, "initPcAmount", data, 10);
insert_u64_string(payload_json, "initCoinAmount", data, 18);
insert_u64_string(payload_json, "tokenAAmount", data, 18);
insert_u64_string(payload_json, "tokenBAmount", data, 10);
},
"02" => {
insert_u16(payload_json, "planOrderLimit", data, 1);
insert_u16(payload_json, "placeOrderLimit", data, 3);
insert_u16(payload_json, "cancelOrderLimit", data, 5);
},
"03" => {
insert_u64_string(payload_json, "maxCoinAmount", data, 1);
insert_u64_string(payload_json, "maxPcAmount", data, 9);
insert_u64_string(payload_json, "baseSide", data, 17);
insert_u64_string(payload_json, "otherAmountMin", data, 25);
insert_u64_string(payload_json, "tokenAAmount", data, 1);
insert_u64_string(payload_json, "tokenBAmount", data, 9);
},
"04" => {
insert_u64_string(payload_json, "lpAmountRaw", data, 1);
insert_u64_string(payload_json, "liquidity", data, 1);
insert_u64_string(payload_json, "minCoinAmount", data, 9);
insert_u64_string(payload_json, "minPcAmount", data, 17);
},
"06" => {
insert_u8(payload_json, "configParam", data, 1);
insert_u64_string(payload_json, "configValue", data, 2);
insert_fixed_hex(payload_json, "configPubkeyHex", data, 2, 32);
insert_u64_string(payload_json, "lastOrderNumerator", data, 2);
insert_u64_string(payload_json, "lastOrderDenominator", data, 10);
},
"08" => {
insert_u64_string(payload_json, "amountRaw", data, 1);
},
"09" | "10" => {
insert_u64_string(payload_json, "amountIn", data, 1);
insert_u64_string(payload_json, "minimumAmountOut", data, 9);
},
"0a" => {
insert_u8(payload_json, "nonce", data, 1);
},
"0b" | "11" => {
insert_u64_string(payload_json, "maxAmountIn", data, 1);
insert_u64_string(payload_json, "amountOut", data, 9);
},
"0c" => {
insert_u8(payload_json, "simulateParam", data, 1);
insert_u64_string(payload_json, "amountIn", data, 2);
insert_u64_string(payload_json, "minimumAmountOut", data, 10);
insert_u64_string(payload_json, "maxAmountIn", data, 2);
insert_u64_string(payload_json, "amountOut", data, 10);
},
"0d" => {
insert_u16(payload_json, "orderCancelLimit", data, 1);
},
"0f" => {
insert_u8(payload_json, "configParam", data, 1);
insert_fixed_hex(payload_json, "configOwnerHex", data, 2, 32);
insert_u64_string(payload_json, "createPoolFee", data, 2);
},
_ => {},
}
}
fn insert_u8(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
let value = match read_u8(data, offset) {
Some(value) => value,
None => return,
};
insert_json_value(
payload_json,
key,
serde_json::Value::Number(serde_json::Number::from(value as u64)),
);
}
fn insert_u16(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
let value = match read_u16_le(data, offset) {
Some(value) => value,
None => return,
};
insert_json_value(
payload_json,
key,
serde_json::Value::Number(serde_json::Number::from(value as u64)),
);
}
fn insert_u64_string(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
let value = match read_u64_le(data, offset) {
Some(value) => value,
None => return,
};
insert_json_value(
payload_json,
key,
serde_json::Value::String(value.to_string()),
);
}
fn insert_fixed_hex(
payload_json: &mut serde_json::Value,
key: &str,
data: &[u8],
offset: usize,
len: usize,
) {
if data.len() < offset + len {
return;
}
let slice = &data[offset..offset + len];
insert_json_value(payload_json, key, serde_json::Value::String(bytes_to_hex(slice)));
}
fn insert_json_value(payload_json: &mut serde_json::Value, key: &str, value: serde_json::Value) {
let object_option = payload_json.as_object_mut();
let object = match object_option {
Some(object) => object,
None => return,
};
object.insert(key.to_string(), value);
}
fn read_u8(data: &[u8], offset: usize) -> std::option::Option<u8> {
if data.len() < offset + 1 {
return None;
}
return Some(data[offset]);
}
fn read_u16_le(data: &[u8], offset: usize) -> std::option::Option<u16> {
if data.len() < offset + 2 {
return None;
}
let bytes = [data[offset], data[offset + 1]];
return Some(u16::from_le_bytes(bytes));
}
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 {
return None;
}
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
];
return Some(u64::from_le_bytes(bytes));
}
fn bytes_to_hex(bytes: &[u8]) -> std::string::String {
let mut output = std::string::String::new();
for byte in bytes {
output.push_str(format!("{byte:02x}").as_str());
}
return output;
}
#[derive(Debug, Clone)]
struct TokenBalanceRecord {
account_address: std::option::Option<std::string::String>,
@@ -401,6 +918,183 @@ struct NormalizedVaultPair {
quote_mint: std::string::String,
}
#[derive(Clone, Copy)]
struct RaydiumAmmV4InstructionIdentity {
instruction_name: &'static str,
event_kind: &'static str,
discriminator_hex: &'static str,
}
fn raydium_amm_v4_swap_instruction(
data_base58: std::option::Option<&str>,
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
let identity = match raydium_amm_v4_instruction_identity(data_base58) {
Some(identity) => identity,
None => return None,
};
match identity.discriminator_hex {
"09" | "0b" | "10" | "11" => return Some(identity),
_ => return None,
}
}
fn raydium_amm_v4_instruction_identity(
data_base58: std::option::Option<&str>,
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
let discriminator_hex = match raydium_amm_v4_instruction_discriminator_hex(data_base58) {
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
match discriminator_hex.as_str() {
"00" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "initialize",
event_kind: "raydium_amm_v4.initialize",
discriminator_hex: "00",
});
},
"01" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "initialize2",
event_kind: "raydium_amm_v4.initialize2_pool",
discriminator_hex: "01",
});
},
"02" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "monitor_step",
event_kind: "raydium_amm_v4.monitor_step",
discriminator_hex: "02",
});
},
"03" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "deposit",
event_kind: "raydium_amm_v4.deposit",
discriminator_hex: "03",
});
},
"04" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "withdraw",
event_kind: "raydium_amm_v4.withdraw",
discriminator_hex: "04",
});
},
"05" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "migrate_to_open_book",
event_kind: "raydium_amm_v4.migrate_to_open_book",
discriminator_hex: "05",
});
},
"06" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "set_params",
event_kind: "raydium_amm_v4.set_params",
discriminator_hex: "06",
});
},
"07" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "withdraw_pnl",
event_kind: "raydium_amm_v4.withdraw_pnl",
discriminator_hex: "07",
});
},
"08" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "withdraw_srm",
event_kind: "raydium_amm_v4.withdraw_srm",
discriminator_hex: "08",
});
},
"09" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "swap_base_in",
event_kind: "raydium_amm_v4.swap_base_in",
discriminator_hex: "09",
});
},
"0a" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "pre_initialize",
event_kind: "raydium_amm_v4.pre_initialize",
discriminator_hex: "0a",
});
},
"0b" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "swap_base_out",
event_kind: "raydium_amm_v4.swap_base_out",
discriminator_hex: "0b",
});
},
"0c" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "simulate_info",
event_kind: "raydium_amm_v4.simulate_info",
discriminator_hex: "0c",
});
},
"0d" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "admin_cancel_orders",
event_kind: "raydium_amm_v4.admin_cancel_orders",
discriminator_hex: "0d",
});
},
"0e" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "create_config_account",
event_kind: "raydium_amm_v4.create_config_account",
discriminator_hex: "0e",
});
},
"0f" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "update_config_account",
event_kind: "raydium_amm_v4.update_config_account",
discriminator_hex: "0f",
});
},
"10" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "swap_base_in_v2",
event_kind: "raydium_amm_v4.swap_base_in_v2",
discriminator_hex: "10",
});
},
"11" => {
return Some(RaydiumAmmV4InstructionIdentity {
instruction_name: "swap_base_out_v2",
event_kind: "raydium_amm_v4.swap_base_out_v2",
discriminator_hex: "11",
});
},
_ => return None,
}
}
fn raydium_amm_v4_instruction_discriminator_hex(
data_base58: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let data_base58 = match data_base58 {
Some(data_base58) => data_base58,
None => return None,
};
let bytes_result = bs58::decode(data_base58).into_vec();
let bytes = match bytes_result {
Ok(bytes) => bytes,
Err(_) => return None,
};
let first_byte = match bytes.first() {
Some(first_byte) => first_byte,
None => return None,
};
return Some(format!("{first_byte:02x}"));
}
fn parse_transaction_meta_value(
transaction: &crate::ChainTransactionDto,
transaction_json: &serde_json::Value,
@@ -1209,6 +1903,14 @@ mod tests {
}
fn make_swap_instruction() -> crate::ChainInstructionDto {
return make_swap_instruction_with_data("63SfuT4qF7xK35jRTGqxuUT");
}
fn make_swap_v2_instruction() -> crate::ChainInstructionDto {
return make_swap_instruction_with_data("9rj8cBJgMm4L1xvzfy5AUsy");
}
fn make_swap_instruction_with_data(data_base58: &str) -> crate::ChainInstructionDto {
let mut dto = crate::ChainInstructionDto::new(
100,
Some(55),
@@ -1228,7 +1930,7 @@ mod tests {
"UserQuote111"
])
.to_string(),
Some(serde_json::json!("9rj8cBJgMm4L1xvzfy5AUsy").to_string()),
Some(serde_json::json!(data_base58).to_string()),
None,
None,
);
@@ -1305,6 +2007,9 @@ mod tests {
assert_eq!(event.transaction_id, 100);
assert_eq!(event.instruction_id, 200);
assert_eq!(event.pool_account, "Pool111".to_string());
assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in".to_string());
assert_eq!(event.instruction_name, "swap_base_in".to_string());
assert_eq!(event.discriminator_hex, "09".to_string());
assert_eq!(event.token_a_mint, "BaseMint111".to_string());
assert_eq!(event.token_b_mint, crate::WSOL_MINT_ID.to_string());
assert_eq!(event.base_vault, "BaseVault111".to_string());
@@ -1320,4 +2025,29 @@ mod tests {
_ => panic!("expected swap event"),
}
}
#[test]
fn raydium_amm_v4_swap_base_in_v2_is_decoded_from_inner_instruction_and_vault_deltas() {
let decoder = crate::RaydiumAmmV4Decoder::new();
let transaction = make_swap_transaction();
let instructions = vec![make_swap_v2_instruction()];
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
match &decoded[0] {
crate::RaydiumAmmV4DecodedEvent::Swap(event) => {
assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in_v2".to_string());
assert_eq!(event.instruction_name, "swap_base_in_v2".to_string());
assert_eq!(event.discriminator_hex, "10".to_string());
assert_eq!(event.pool_account, "Pool111".to_string());
assert_eq!(event.base_amount_raw, Some("100".to_string()));
assert_eq!(event.quote_amount_raw, Some("100".to_string()));
assert_eq!(event.trade_side, Some("BuyBase".to_string()));
},
_ => panic!("expected swap event"),
}
}
}

View File

@@ -523,43 +523,16 @@ impl DexDecodeService {
transaction_id: i64,
discriminator_hex: &str,
) -> Result<(), crate::Error> {
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = 'raydium_clmm'
AND event_kind = 'raydium_clmm.instruction_audit'
AND (
json_extract(payload_json, '$.discriminatorHex') = ?
OR json_extract(payload_json, '$.discriminator_hex') = ?
OR json_extract(payload_json, '$.instructionDiscriminatorHex') = ?
OR json_extract(payload_json, '$.instruction_discriminator_hex') = ?
OR json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
OR json_extract(payload_json, '$.anchor_event_discriminator_hex') = ?
)
"#,
)
.bind(transaction_id)
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete Raydium CLMM residual instruction audit '{}': {}",
discriminator_hex, error
)));
},
}
},
let delete_result =
crate::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator(
self.database.as_ref(),
transaction_id,
discriminator_hex,
)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
@@ -706,88 +679,18 @@ WHERE transaction_id = ?
Some(audit_event_kind) => audit_event_kind,
None => return Ok(()),
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let unlink_result = sqlx::query(
r#"
UPDATE k_sol_instruction_observations
SET decoded_event_id = NULL
WHERE decoded_event_id IN (
SELECT id
FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = ?
AND event_kind = ?
AND (
json_extract(payload_json, '$.discriminatorHex') = ?
OR json_extract(payload_json, '$.discriminator_hex') = ?
OR json_extract(payload_json, '$.instructionDiscriminatorHex') = ?
OR json_extract(payload_json, '$.instruction_discriminator_hex') = ?
OR json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
OR json_extract(payload_json, '$.anchor_event_discriminator_hex') = ?
OR instr(lower(COALESCE(payload_json, '')), lower(?)) > 0
)
)
"#,
)
.bind(transaction_id)
.bind(protocol_name.to_string())
.bind(audit_event_kind.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.execute(pool)
.await;
if let Err(error) = unlink_result {
return Err(crate::Error::Db(format!(
"cannot unlink replaced instruction audit observation by discriminator on sqlite: {}",
error
)));
}
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = ?
AND event_kind = ?
AND (
json_extract(payload_json, '$.discriminatorHex') = ?
OR json_extract(payload_json, '$.discriminator_hex') = ?
OR json_extract(payload_json, '$.instructionDiscriminatorHex') = ?
OR json_extract(payload_json, '$.instruction_discriminator_hex') = ?
OR json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
OR json_extract(payload_json, '$.anchor_event_discriminator_hex') = ?
OR instr(lower(COALESCE(payload_json, '')), lower(?)) > 0
)
"#,
)
.bind(transaction_id)
.bind(protocol_name.to_string())
.bind(audit_event_kind.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.bind(discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete replaced instruction audit by discriminator on sqlite: {}",
error
)));
},
}
},
let delete_result =
crate::query_dex_decoded_events_delete_instruction_audit_by_discriminator(
self.database.as_ref(),
transaction_id,
protocol_name,
audit_event_kind,
discriminator_hex,
)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
@@ -797,34 +700,17 @@ WHERE transaction_id = ?
_instruction_id: i64,
anchor_event_discriminator_hex: &str,
) -> Result<(), crate::Error> {
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE transaction_id = ?
AND protocol_name = 'raydium_launchpad'
AND event_kind = 'raydium_launchpad.instruction_audit'
AND json_extract(payload_json, '$.anchorSelfCpiLog') = 1
AND json_extract(payload_json, '$.anchorSelfCpiLogSelectorHex') = ?
AND json_extract(payload_json, '$.anchorEventDiscriminatorHex') = ?
"#,
)
.bind(transaction_id)
.bind(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX.to_string())
.bind(anchor_event_discriminator_hex.to_string())
.execute(pool)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete replaced Raydium Launchpad self-CPI instruction audit on sqlite: {}",
error
)));
},
}
},
let delete_result =
crate::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit(
self.database.as_ref(),
transaction_id,
METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX,
anchor_event_discriminator_hex,
)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
@@ -836,50 +722,16 @@ WHERE transaction_id = ?
Some(transaction_id) => transaction_id,
None => return Ok(()),
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE id IN (
SELECT audit.id
FROM k_sol_dex_decoded_events audit
WHERE audit.transaction_id = ?
AND audit.protocol_name = 'raydium_launchpad'
AND audit.event_kind = 'raydium_launchpad.instruction_audit'
AND json_extract(audit.payload_json, '$.anchorSelfCpiLog') = 1
AND json_extract(audit.payload_json, '$.anchorSelfCpiLogSelectorHex') = ?
AND EXISTS (
SELECT 1
FROM k_sol_dex_decoded_events direct
WHERE direct.transaction_id = audit.transaction_id
AND direct.protocol_name = 'raydium_launchpad'
AND direct.event_kind IN (
'raydium_launchpad.trade_event',
'raydium_launchpad.pool_create_event',
'raydium_launchpad.claim_vested_event',
'raydium_launchpad.create_vesting_event'
let cleanup_result =
crate::query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits(
self.database.as_ref(),
transaction_id,
METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX,
)
AND json_extract(direct.payload_json, '$.anchorEventDiscriminatorHex') =
json_extract(audit.payload_json, '$.anchorEventDiscriminatorHex')
)
)
"#,
)
.bind(transaction_id)
.bind(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX.to_string())
.execute(pool)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot cleanup replaced Raydium Launchpad self-CPI instruction audits for signature '{}': {}",
transaction.signature, error
)));
},
}
},
.await;
match cleanup_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
@@ -1518,7 +1370,7 @@ WHERE id IN (
event.instruction_id,
"raydium_amm_v4",
event.program_id.clone(),
"raydium_amm_v4.swap",
event.event_kind.as_str(),
Some(event.pool_account.clone()),
None,
Some(event.token_a_mint.clone()),
@@ -1528,6 +1380,24 @@ WHERE id IN (
)
.await;
},
crate::RaydiumAmmV4DecodedEvent::Instruction(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"raydium_amm_v4",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
event.market_account.clone(),
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
}
}
@@ -1978,7 +1848,11 @@ WHERE id IN (
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref());
let discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0);
let discriminator_hex = raydium_instruction_discriminator_hex(
audit_spec.protocol_name,
data_bytes.as_deref(),
0,
);
let anchor_event_spec = raydium_launchpad_anchor_self_cpi_event_spec(
audit_spec.protocol_name,
data_bytes.as_deref(),
@@ -2598,6 +2472,17 @@ enum RaydiumMappedNonTradeAmountLayout {
CpmmPoolStatus,
CpmmWithdraw,
LaunchpadInitialize,
AmmV4Initialize,
AmmV4Initialize2,
AmmV4MonitorStep,
AmmV4Deposit,
AmmV4Withdraw,
AmmV4SetParams,
AmmV4WithdrawSrm,
AmmV4PreInitialize,
AmmV4SimulateInfo,
AmmV4AdminCancelOrders,
AmmV4UpdateConfigAccount,
}
fn raydium_instruction_audit_spec(
@@ -2649,6 +2534,9 @@ fn raydium_mapped_non_trade_instruction_spec(
account_count,
);
}
if protocol_name == "raydium_amm_v4" {
return raydium_amm_v4_mapped_non_trade_instruction_spec(discriminator_hex, account_count);
}
if protocol_name == "raydium_clmm" {
if discriminator_hex == "e445a52e51cb9a1d" {
return Some(RaydiumMappedNonTradeInstructionSpec {
@@ -3173,6 +3061,198 @@ fn raydium_mapped_non_trade_instruction_spec(
return None;
}
fn raydium_amm_v4_mapped_non_trade_instruction_spec(
discriminator_hex: &str,
account_count: usize,
) -> std::option::Option<RaydiumMappedNonTradeInstructionSpec> {
match discriminator_hex {
"00" => {
if account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize",
event_kind: "raydium_amm_v4.initialize",
pool_account_index: Some(3),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Initialize,
});
}
},
"01" => {
if account_count >= 10 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize2",
event_kind: "raydium_amm_v4.initialize2_pool",
pool_account_index: Some(4),
token_a_mint_index: Some(8),
token_b_mint_index: Some(9),
lp_mint_index: Some(7),
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Initialize2,
});
}
},
"02" => {
if account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "monitor_step",
event_kind: "raydium_amm_v4.monitor_step",
pool_account_index: Some(3),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4MonitorStep,
});
}
},
"03" => {
if account_count >= 8 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "deposit",
event_kind: "raydium_amm_v4.deposit",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(5),
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Deposit,
});
}
},
"04" => {
if account_count >= 8 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_amm_v4.withdraw",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(5),
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4Withdraw,
});
}
},
"05" => {
if account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "migrate_to_open_book",
event_kind: "raydium_amm_v4.migrate_to_open_book",
pool_account_index: Some(3),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
},
"06" => {
if account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "set_params",
event_kind: "raydium_amm_v4.set_params",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4SetParams,
});
}
},
"07" => {
if account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw_pnl",
event_kind: "raydium_amm_v4.withdraw_pnl",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
},
"08" => {
if account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw_srm",
event_kind: "raydium_amm_v4.withdraw_srm",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4WithdrawSrm,
});
}
},
"0a" => {
if account_count >= 5 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "pre_initialize",
event_kind: "raydium_amm_v4.pre_initialize",
pool_account_index: Some(4),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4PreInitialize,
});
}
},
"0c" => {
if account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "simulate_info",
event_kind: "raydium_amm_v4.simulate_info",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4SimulateInfo,
});
}
},
"0d" => {
if account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "admin_cancel_orders",
event_kind: "raydium_amm_v4.admin_cancel_orders",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4AdminCancelOrders,
});
}
},
"0e" => {
if account_count >= 1 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_config_account",
event_kind: "raydium_amm_v4.create_config_account",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
},
"0f" => {
if account_count >= 1 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_config_account",
event_kind: "raydium_amm_v4.update_config_account",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AmmV4UpdateConfigAccount,
});
}
},
_ => {},
}
return None;
}
fn raydium_launchpad_mapped_non_trade_instruction_spec(
discriminator_hex: &str,
account_count: usize,
@@ -3497,6 +3577,189 @@ fn insert_raydium_mapped_amounts(
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4Initialize => {
if let Some(nonce) = read_u8_from_bytes(data, 1) {
object.insert(
"nonce".to_string(),
serde_json::Value::Number(serde_json::Number::from(nonce as u64)),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 2) {
object.insert("openTime".to_string(), serde_json::Value::String(open_time.to_string()));
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4Initialize2 => {
if let Some(nonce) = read_u8_from_bytes(data, 1) {
object.insert(
"nonce".to_string(),
serde_json::Value::Number(serde_json::Number::from(nonce as u64)),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 2) {
object.insert("openTime".to_string(), serde_json::Value::String(open_time.to_string()));
}
if let Some(init_pc_amount) = read_u64_le_from_bytes(data, 10) {
object.insert(
"initPcAmount".to_string(),
serde_json::Value::String(init_pc_amount.to_string()),
);
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(init_pc_amount.to_string()),
);
}
if let Some(init_coin_amount) = read_u64_le_from_bytes(data, 18) {
object.insert(
"initCoinAmount".to_string(),
serde_json::Value::String(init_coin_amount.to_string()),
);
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(init_coin_amount.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4MonitorStep => {
if let Some(plan_order_limit) = read_u16_le_from_bytes(data, 1) {
object.insert(
"planOrderLimit".to_string(),
serde_json::Value::Number(serde_json::Number::from(plan_order_limit as u64)),
);
}
if let Some(place_order_limit) = read_u16_le_from_bytes(data, 3) {
object.insert(
"placeOrderLimit".to_string(),
serde_json::Value::Number(serde_json::Number::from(place_order_limit as u64)),
);
}
if let Some(cancel_order_limit) = read_u16_le_from_bytes(data, 5) {
object.insert(
"cancelOrderLimit".to_string(),
serde_json::Value::Number(serde_json::Number::from(cancel_order_limit as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4Deposit => {
if let Some(max_coin_amount) = read_u64_le_from_bytes(data, 1) {
object.insert(
"maxCoinAmount".to_string(),
serde_json::Value::String(max_coin_amount.to_string()),
);
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(max_coin_amount.to_string()),
);
}
if let Some(max_pc_amount) = read_u64_le_from_bytes(data, 9) {
object.insert(
"maxPcAmount".to_string(),
serde_json::Value::String(max_pc_amount.to_string()),
);
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(max_pc_amount.to_string()),
);
}
if let Some(base_side) = read_u64_le_from_bytes(data, 17) {
object.insert("baseSide".to_string(), serde_json::Value::String(base_side.to_string()));
}
if let Some(other_amount_min) = read_u64_le_from_bytes(data, 25) {
object.insert(
"otherAmountMin".to_string(),
serde_json::Value::String(other_amount_min.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4Withdraw => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 1) {
object.insert("lpAmountRaw".to_string(), serde_json::Value::String(lp_amount.to_string()));
object.insert("liquidity".to_string(), serde_json::Value::String(lp_amount.to_string()));
}
if let Some(min_coin_amount) = read_u64_le_from_bytes(data, 9) {
object.insert(
"minCoinAmount".to_string(),
serde_json::Value::String(min_coin_amount.to_string()),
);
}
if let Some(min_pc_amount) = read_u64_le_from_bytes(data, 17) {
object.insert(
"minPcAmount".to_string(),
serde_json::Value::String(min_pc_amount.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4SetParams => {
if let Some(param) = read_u8_from_bytes(data, 1) {
object.insert(
"configParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(value) = read_u64_le_from_bytes(data, 2) {
object.insert("configValue".to_string(), serde_json::Value::String(value.to_string()));
}
if let Some(last_order_denominator) = read_u64_le_from_bytes(data, 10) {
object.insert(
"lastOrderDenominator".to_string(),
serde_json::Value::String(last_order_denominator.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4WithdrawSrm => {
if let Some(amount) = read_u64_le_from_bytes(data, 1) {
object.insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string()));
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4PreInitialize => {
object.insert("deprecatedInstruction".to_string(), serde_json::Value::Bool(true));
object.insert("partialLifecycle".to_string(), serde_json::Value::Bool(true));
object.insert(
"skipCatalogReason".to_string(),
serde_json::Value::String("missing_token_mints".to_string()),
);
if let Some(nonce) = read_u8_from_bytes(data, 1) {
object.insert(
"nonce".to_string(),
serde_json::Value::Number(serde_json::Number::from(nonce as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4SimulateInfo => {
if let Some(param) = read_u8_from_bytes(data, 1) {
object.insert(
"simulateParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(amount_in) = read_u64_le_from_bytes(data, 2) {
object.insert("amountIn".to_string(), serde_json::Value::String(amount_in.to_string()));
}
if let Some(amount_out) = read_u64_le_from_bytes(data, 10) {
object.insert("amountOutOrMinimumAmountOut".to_string(), serde_json::Value::String(amount_out.to_string()));
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4AdminCancelOrders => {
if let Some(limit) = read_u16_le_from_bytes(data, 1) {
object.insert(
"orderCancelLimit".to_string(),
serde_json::Value::Number(serde_json::Number::from(limit as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::AmmV4UpdateConfigAccount => {
if let Some(param) = read_u8_from_bytes(data, 1) {
object.insert(
"configParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(create_pool_fee) = read_u64_le_from_bytes(data, 2) {
object.insert(
"createPoolFee".to_string(),
serde_json::Value::String(create_pool_fee.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::LaunchpadInitialize => {
object.insert(
"poolKindHint".to_string(),
@@ -3555,6 +3818,19 @@ fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
return Some(data[offset]);
}
fn read_u16_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u16> {
if data.len() < offset + 2 {
return None;
}
let mut bytes = [0_u8; 2];
let mut index = 0_usize;
while index < 2 {
bytes[index] = data[offset + index];
index += 1;
}
return Some(u16::from_le_bytes(bytes));
}
fn read_i32_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<i32> {
if data.len() < offset + 4 {
return None;
@@ -3701,7 +3977,7 @@ fn build_meteora_instruction_audit_payload(
};
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref());
let discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0);
let discriminator_hex = raydium_instruction_discriminator_hex(protocol_name, data_bytes.as_deref(), 0);
let anchor_self_cpi_log =
discriminator_hex.as_deref() == Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX);
let anchor_event_discriminator_hex = if anchor_self_cpi_log {
@@ -4239,7 +4515,7 @@ fn build_raydium_instruction_audit_payload(
};
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let data_bytes = instruction_data_bytes_from_base58(data_base58.as_deref());
let discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0);
let discriminator_hex = raydium_instruction_discriminator_hex(protocol_name, data_bytes.as_deref(), 0);
let anchor_self_cpi_log =
discriminator_hex.as_deref() == Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX);
let anchor_event_discriminator_hex = if anchor_self_cpi_log {
@@ -4438,6 +4714,40 @@ fn discriminator_hex_from_base58(
return discriminator_hex_from_bytes(bytes.as_deref(), 0);
}
fn raydium_instruction_discriminator_hex(
protocol_name: &str,
bytes: std::option::Option<&[u8]>,
offset: usize,
) -> std::option::Option<std::string::String> {
if protocol_name == "raydium_amm_v4" {
return discriminator_hex_from_bytes_with_len(bytes, offset, 1);
}
return discriminator_hex_from_bytes(bytes, offset);
}
fn discriminator_hex_from_bytes_with_len(
bytes: std::option::Option<&[u8]>,
offset: usize,
length: usize,
) -> std::option::Option<std::string::String> {
let bytes = match bytes {
Some(bytes) => bytes,
None => return None,
};
if bytes.len() < offset + length {
return None;
}
let mut text = std::string::String::new();
let mut index = offset;
let end = offset + length;
while index < end {
let byte = bytes[index];
text.push_str(format!("{byte:02x}").as_str());
index += 1;
}
return Some(text);
}
fn discriminator_hex_from_bytes(
bytes: std::option::Option<&[u8]>,
offset: usize,

View File

@@ -49,7 +49,16 @@ pub(crate) fn dex_detection_route(
crate::dex_detection_route::DexDetectionRoute::RaydiumAmmV4Initialize2Pool,
);
},
("raydium_amm_v4", "raydium_amm_v4.swap") => {
("raydium_amm_v4", "raydium_amm_v4.swap_base_in") => {
return Some(crate::dex_detection_route::DexDetectionRoute::RaydiumAmmV4Trade);
},
("raydium_amm_v4", "raydium_amm_v4.swap_base_out") => {
return Some(crate::dex_detection_route::DexDetectionRoute::RaydiumAmmV4Trade);
},
("raydium_amm_v4", "raydium_amm_v4.swap_base_in_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::RaydiumAmmV4Trade);
},
("raydium_amm_v4", "raydium_amm_v4.swap_base_out_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::RaydiumAmmV4Trade);
},
("raydium_cpmm", "raydium_cpmm.swap_base_input") => {

View File

@@ -357,6 +357,12 @@ 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.contains(".withdraw_pnl") {
return false;
}
if event_kind.contains(".withdraw_srm") {
return false;
}
if event_kind.contains(".deposit") {
return true;
}
@@ -418,6 +424,12 @@ pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool {
/// Returns true for liquidity remove-like DEX events.
pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".withdraw_pnl") {
return false;
}
if event_kind.contains(".withdraw_srm") {
return false;
}
if event_kind.contains(".withdraw") {
return true;
}
@@ -487,6 +499,12 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.contains("withdraw_protocol_fees") {
return true;
}
if event_kind.contains(".withdraw_pnl") {
return true;
}
if event_kind.contains(".withdraw_srm") {
return true;
}
if event_kind.contains("partner_claim_fee") {
return true;
}
@@ -506,6 +524,15 @@ pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
/// Returns true for orderbook or limit-order events that must not become candles.
pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".monitor_step") {
return true;
}
if event_kind.contains(".migrate_to_open_book") {
return true;
}
if event_kind.contains(".admin_cancel_orders") {
return true;
}
if event_kind.contains(".order_place") {
return true;
}
@@ -538,6 +565,9 @@ pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
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") {
return true;
}
@@ -621,6 +651,12 @@ pub fn is_dex_token_burn_event_kind(event_kind: &str) -> bool {
/// Returns true for launch-surface or pool migration events.
pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_amm_v4.migrate_to_open_book" {
return false;
}
if event_kind.contains(".migrate_to_open_book") {
return false;
}
if event_kind.contains(".migrate") {
return true;
}
@@ -632,6 +668,9 @@ 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 == "raydium_amm_v4.pre_initialize" {
return true;
}
if event_kind.contains("amm_config") {
return false;
}
@@ -711,6 +750,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.contains(".admin_cancel_orders") {
return false;
}
if event_kind.contains(".monitor_step") {
return false;
}
if event_kind.contains(".migrate_to_open_book") {
return false;
}
if event_kind.contains(".close_platform_global_access") {
return true;
}
@@ -1114,6 +1162,10 @@ mod tests {
super::classify_dex_event_category_code("raydium_cpmm.initialize"),
"pool_lifecycle"
);
assert_eq!(
super::classify_dex_event_category_code("raydium_amm_v4.pre_initialize"),
"pool_lifecycle"
);
}
#[test]
@@ -1122,7 +1174,32 @@ mod tests {
super::classify_dex_event_lifecycle_kind_code("raydium_cpmm.initialize"),
"pool_creation"
);
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("raydium_amm_v4.pre_initialize"),
"pool_creation"
);
assert_eq!(super::classify_dex_event_lifecycle_kind_code("pump_fun.create"), "launch");
assert_eq!(
crate::dex_event_classification::classify_dex_event_category_code(
"raydium_amm_v4.migrate_to_open_book",
),
"unknown",
);
assert_eq!(
crate::dex_event_classification::classify_dex_event_lifecycle_kind_code(
"raydium_amm_v4.migrate_to_open_book",
),
"unknown",
);
assert!(crate::dex_event_classification::is_dex_orderbook_event_kind(
"raydium_amm_v4.migrate_to_open_book",
));
assert!(!crate::dex_event_classification::is_dex_pool_lifecycle_event_kind(
"raydium_amm_v4.migrate_to_open_book",
));
assert!(!crate::dex_event_classification::is_dex_migration_event_kind(
"raydium_amm_v4.migrate_to_open_book",
));
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("meteora_dbc.migrate"),
"migration"

View File

@@ -220,6 +220,46 @@ fn infer_expected_db_target_for_entry(
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
if decoder_code == "raydium_amm_v4" {
if entry_name == "swap_base_in"
|| entry_name == "swap_base_out"
|| entry_name == "swap_base_in_v2"
|| entry_name == "swap_base_out_v2"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if entry_name == "initialize"
|| entry_name == "initialize2"
|| entry_name == "pre_initialize"
{
return Some(
crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string(),
);
}
if entry_name == "deposit" || entry_name == "withdraw" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string());
}
if entry_name == "withdraw_pnl" || entry_name == "withdraw_srm" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string());
}
if entry_name == "admin_cancel_orders" || entry_name == "migrate_to_open_book" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS.to_string());
}
if entry_name == "monitor_step" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_ORDERBOOK_EVENTS.to_string());
}
if entry_name == "create_config_account"
|| entry_name == "update_config_account"
|| entry_name == "set_params"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string());
}
if entry_name == "simulate_info" {
return Some(
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
);
}
}
if decoder_code == "raydium_clmm" {
if entry_name == "initialize_reward" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS.to_string());
@@ -356,6 +396,9 @@ fn infer_event_family_for_entry(
if decoder_code == "raydium_launchpad" {
return infer_raydium_launchpad_event_family(entry_name, entry_kind);
}
if decoder_code == "raydium_amm_v4" {
return infer_raydium_amm_v4_event_family(entry_name, entry_kind);
}
if decoder_code == "raydium_clmm" {
return infer_raydium_clmm_event_family(entry_name, entry_kind);
}
@@ -365,6 +408,36 @@ fn infer_event_family_for_entry(
return infer_event_family(entry_name, entry_kind);
}
fn infer_raydium_amm_v4_event_family(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
match entry_name {
"swap_base_in" => return Some("swap".to_string()),
"swap_base_out" => return Some("swap".to_string()),
"swap_base_in_v2" => return Some("swap".to_string()),
"swap_base_out_v2" => return Some("swap".to_string()),
"initialize" => return Some("pool_create".to_string()),
"initialize2" => return Some("pool_create".to_string()),
"pre_initialize" => return Some("pool_create".to_string()),
"deposit" => return Some("liquidity_add".to_string()),
"withdraw" => return Some("liquidity_remove".to_string()),
"withdraw_pnl" => return Some("fee".to_string()),
"withdraw_srm" => return Some("fee".to_string()),
"admin_cancel_orders" => return Some("order_cancel".to_string()),
"migrate_to_open_book" => return Some("order_place".to_string()),
"create_config_account" => return Some("admin_config".to_string()),
"update_config_account" => return Some("admin_config".to_string()),
"set_params" => return Some("admin_config".to_string()),
"monitor_step" => return Some("order_place".to_string()),
"simulate_info" => return Some("cpi_transport".to_string()),
_ => return infer_event_family(entry_name, entry_kind),
}
}
fn infer_raydium_cpmm_event_family(
entry_name: &str,
entry_kind: &str,
@@ -628,10 +701,37 @@ fn raydium_launchpad_local_entry_is_known(entry_name: &str) -> bool {
}
}
fn raydium_amm_v4_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
match entry_name {
"swap_base_in" => return Some("raydium_amm_v4.swap_base_in".to_string()),
"swap_base_out" => return Some("raydium_amm_v4.swap_base_out".to_string()),
"swap_base_in_v2" => return Some("raydium_amm_v4.swap_base_in_v2".to_string()),
"swap_base_out_v2" => return Some("raydium_amm_v4.swap_base_out_v2".to_string()),
"initialize" => return Some("raydium_amm_v4.initialize".to_string()),
"initialize2" => return Some("raydium_amm_v4.initialize2_pool".to_string()),
"pre_initialize" => return Some("raydium_amm_v4.pre_initialize".to_string()),
"deposit" => return Some("raydium_amm_v4.deposit".to_string()),
"withdraw" => return Some("raydium_amm_v4.withdraw".to_string()),
"withdraw_pnl" => return Some("raydium_amm_v4.withdraw_pnl".to_string()),
"withdraw_srm" => return Some("raydium_amm_v4.withdraw_srm".to_string()),
"admin_cancel_orders" => return Some("raydium_amm_v4.admin_cancel_orders".to_string()),
"migrate_to_open_book" => return Some("raydium_amm_v4.migrate_to_open_book".to_string()),
"create_config_account" => return Some("raydium_amm_v4.create_config_account".to_string()),
"update_config_account" => return Some("raydium_amm_v4.update_config_account".to_string()),
"set_params" => return Some("raydium_amm_v4.set_params".to_string()),
"monitor_step" => return Some("raydium_amm_v4.monitor_step".to_string()),
"simulate_info" => return Some("raydium_amm_v4.simulate_info".to_string()),
_ => return None,
}
}
pub(crate) fn known_local_event_kind(
decoder_code: &str,
entry_name: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "raydium_amm_v4" {
return raydium_amm_v4_local_event_kind(entry_name);
}
if decoder_code == "raydium_launchpad" && raydium_launchpad_local_entry_is_known(entry_name) {
return Some(format!("raydium_launchpad.{}", entry_name));
}

View File

@@ -6,25 +6,6 @@
//! aid used to find local corpus evidence by program, decoder, instruction
//! discriminator and instruction name.
#[derive(Debug, Clone, sqlx::FromRow)]
struct InstructionObservationSourceRow {
transaction_id: i64,
signature: std::string::String,
slot: std::option::Option<i64>,
block_time: std::option::Option<i64>,
err_json: std::option::Option<std::string::String>,
instruction_id: i64,
parent_instruction_id: std::option::Option<i64>,
instruction_index: i64,
inner_instruction_index: std::option::Option<i64>,
program_id: std::option::Option<std::string::String>,
accounts_json: std::string::String,
data_json: std::option::Option<std::string::String>,
pool_account: std::option::Option<std::string::String>,
decoded_event_kind: std::option::Option<std::string::String>,
decoded_event_id: std::option::Option<i64>,
}
/// Result of refreshing the instruction-observation index.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -88,8 +69,23 @@ impl InstructionObservationIndexService {
async fn upsert_source_rows(
&self,
rows: std::vec::Vec<InstructionObservationSourceRow>,
rows: std::vec::Vec<crate::InstructionObservationSourceRow>,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let mut transaction_ids = std::vec::Vec::<i64>::new();
for row in &rows {
if transaction_ids.contains(&row.transaction_id) {
continue;
}
transaction_ids.push(row.transaction_id);
}
let delete_result = crate::query_instruction_observations_delete_by_transaction_ids(
self.database.as_ref(),
transaction_ids.as_slice(),
)
.await;
if let Err(error) = delete_result {
return Err(error);
}
let mut result = crate::InstructionObservationIndexRefreshResult::default();
for row in rows {
result.scanned_instruction_count += 1;
@@ -111,182 +107,46 @@ impl InstructionObservationIndexService {
async fn list_source_rows_by_signature(
&self,
signature: &str,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
WHERE tx.signature = ?
ORDER BY ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(signature.to_string())
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for signature '{}': {}",
signature, error
)));
},
}
},
}
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
return crate::query_instruction_observation_source_rows_list_by_signature(
self.database.as_ref(),
signature,
)
.await;
}
async fn list_replay_window_source_rows(
&self,
limit: std::option::Option<i64>,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
let effective_limit = match limit {
Some(limit) => {
if limit <= 0 {
10_000
} else {
limit
}
},
None => 10_000,
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
WITH replay_transactions AS (
SELECT id
FROM k_sol_chain_transactions
ORDER BY id ASC
LIMIT ?
)
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN replay_transactions replay_tx
ON replay_tx.id = ins.transaction_id
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY tx.id ASC, ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(effective_limit)
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for replay window: {}",
error
)));
},
}
},
}
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
return crate::query_instruction_observation_source_rows_list_replay_window(
self.database.as_ref(),
limit,
)
.await;
}
async fn list_recent_source_rows(
&self,
limit: u32,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY ins.id DESC
LIMIT ?
"#,
)
.bind(i64::from(limit))
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list recent instruction observation source rows: {}",
error
)));
},
}
},
}
) -> Result<std::vec::Vec<crate::InstructionObservationSourceRow>, crate::Error> {
return crate::query_instruction_observation_source_rows_list_recent(
self.database.as_ref(),
limit,
)
.await;
}
}
fn build_instruction_observation_dto(
row: InstructionObservationSourceRow,
row: crate::InstructionObservationSourceRow,
) -> std::option::Option<crate::InstructionObservationDto> {
let program_id = match row.program_id.clone() {
Some(program_id) => program_id,
None => return None,
};
let discriminator_hex = discriminator_hex_from_data_json(row.data_json.as_ref());
let discriminator_hex =
discriminator_hex_from_data_json(row.data_json.as_ref(), program_id.as_str());
let decoder_code = resolve_decoder_code(program_id.as_str());
let instruction_name = resolve_instruction_name(
program_id.as_str(),
@@ -340,6 +200,31 @@ fn resolve_instruction_name(
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
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",
"01" => "raydium_amm_v4.initialize2_pool",
"02" => "raydium_amm_v4.monitor_step",
"03" => "raydium_amm_v4.deposit",
"04" => "raydium_amm_v4.withdraw",
"05" => "raydium_amm_v4.migrate_to_open_book",
"06" => "raydium_amm_v4.set_params",
"07" => "raydium_amm_v4.withdraw_pnl",
"08" => "raydium_amm_v4.withdraw_srm",
"09" => "raydium_amm_v4.swap_base_in",
"0a" => "raydium_amm_v4.pre_initialize",
"0b" => "raydium_amm_v4.swap_base_out",
"0c" => "raydium_amm_v4.simulate_info",
"0d" => "raydium_amm_v4.admin_cancel_orders",
"0e" => "raydium_amm_v4.create_config_account",
"0f" => "raydium_amm_v4.update_config_account",
"10" => "raydium_amm_v4.swap_base_in_v2",
"11" => "raydium_amm_v4.swap_base_out_v2",
_ => return None,
};
return Some(name.to_string());
}
if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID || decoder_code == Some("raydium_cpmm") {
let name = match discriminator_hex {
"9c5420764587467b" => "raydium_cpmm.close_permission_pda",
@@ -418,15 +303,21 @@ fn resolve_instruction_name(
fn discriminator_hex_from_data_json(
data_json: std::option::Option<&std::string::String>,
program_id: &str,
) -> std::option::Option<std::string::String> {
let decoded = match decode_data_json_as_bytes(data_json) {
Some(decoded) => decoded,
None => return None,
};
if decoded.len() < 8 {
let discriminator_len = if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID {
1_usize
} else {
8_usize
};
if decoded.len() < discriminator_len {
return None;
}
return Some(bytes_to_hex(&decoded[0..8]));
return Some(bytes_to_hex(&decoded[0..discriminator_len]));
}
fn decode_data_json_as_bytes(

View File

@@ -750,14 +750,24 @@ pub use db::query_dex_decode_replay_ledger_get_by_signature;
pub use db::query_dex_decode_replay_ledger_get_by_transaction;
/// Inserts or updates one DEX decode replay ledger row.
pub use db::query_dex_decode_replay_ledger_upsert;
/// Cleans Raydium Launchpad self-CPI audit rows replaced by direct decoded rows.
pub use db::query_dex_decoded_events_cleanup_raydium_launchpad_anchor_self_cpi_audits;
/// Deletes one decoded DEX event row by its natural key.
pub use db::query_dex_decoded_events_delete_by_key;
/// Deletes an instruction-audit row by discriminator for one protocol.
pub use db::query_dex_decoded_events_delete_instruction_audit_by_discriminator;
/// Deletes local DEX decoded rows and linked materialization rows for one replayed transaction.
pub use db::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id;
/// Deletes upstream registry instruction-match rows already covered by specialized local decoders.
pub use db::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
/// Deletes Meteora DLMM Anchor self-CPI swap audit rows already covered by decoded swaps.
pub use db::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
/// Deletes decoded DEX instruction audit rows related to one decoded instruction.
pub use db::query_dex_decoded_events_delete_related_instruction_audit;
/// Deletes one Raydium CLMM instruction-audit row by discriminator.
pub use db::query_dex_decoded_events_delete_raydium_clmm_instruction_audit_by_discriminator;
/// Deletes one Raydium Launchpad self-CPI audit row by discriminator.
pub use db::query_dex_decoded_events_delete_raydium_launchpad_anchor_self_cpi_audit;
/// Deletes Raydium CLMM instruction-audit rows for locally mapped CLMM instructions.
pub use db::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
/// Deletes Raydium CPMM instruction-audit rows already covered by local named rows.
@@ -770,6 +780,8 @@ pub use db::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use db::query_dex_decoded_events_list_by_transaction_id;
/// Inserts or updates one decoded DEX event row.
pub use db::query_dex_decoded_events_upsert;
/// Updates the persisted payload of one decoded DEX event row.
pub use db::query_dex_decoded_events_update_payload_json_by_id;
/// Deletes DEX event coverage entries for one decoder.
pub use db::query_dex_event_coverage_entries_delete_by_decoder;
/// Lists DEX event coverage entries for one decoder.
@@ -795,8 +807,20 @@ 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.
/// 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.
pub use db::query_instruction_observation_source_rows_list_recent;
/// Lists instruction-observation source rows for the local replay window.
pub use db::query_instruction_observation_source_rows_list_replay_window;
/// Deletes instruction observations for a set of transaction ids before rebuilding the technical index.
pub use db::query_instruction_observations_delete_by_transaction_ids;
/// Lists instruction observations by optional filters.
pub use db::query_instruction_observations_list_by_filter;
/// Upserts one instruction observation row.
pub use db::query_instruction_observations_upsert;
/// Raw source row used to rebuild the technical instruction-observation index.
pub use db::InstructionObservationSourceRow;
/// Reads one known HTTP endpoint by name.
pub use db::query_known_http_endpoints_get;
/// Lists all known HTTP endpoints.
@@ -815,6 +839,10 @@ pub use db::query_launch_attributions_get_by_decoded_event_id;
pub use db::query_launch_attributions_list_by_pool_id;
/// Inserts or updates one launch attribution row and returns its stable internal id.
pub use db::query_launch_attributions_upsert;
/// Inserts or updates one launch event row.
pub use db::query_launch_events_upsert;
/// Input used to upsert one launch event row.
pub use db::LaunchEventUpsertInput;
/// Returns one launch-surface matching key identified by its kind and value, if it exists.
pub use db::query_launch_surface_keys_get_by_match;
/// Lists all launch-surface matching keys attached to one launch surface id.
@@ -913,6 +941,8 @@ pub use db::query_pairs_list;
pub use db::query_pairs_update_symbol;
/// Inserts or updates one normalized pair row by pool id.
pub use db::query_pairs_upsert;
/// Deletes one stale pool administration event by decoded-event id.
pub use db::query_pool_admin_events_delete_by_decoded_event_id;
/// Returns one pool administration event by decoded-event id.
pub use db::query_pool_admin_events_get_by_decoded_event_id;
/// Lists recent pool administration events ordered from newest to oldest.
@@ -1161,6 +1191,8 @@ pub use dex::RaydiumAmmV4DecodedEvent;
pub use dex::RaydiumAmmV4Decoder;
/// Decoded Raydium AmmV4 initialize2 pool event.
pub use dex::RaydiumAmmV4Initialize2PoolDecoded;
/// Decoded Raydium AmmV4 non-swap or decoded-only instruction event.
pub use dex::RaydiumAmmV4InstructionDecoded;
/// Decoded Raydium AMM v4 swap event.
pub use dex::RaydiumAmmV4SwapDecoded;
/// Decoded Raydium CLMM collect_protocol_fee instruction.

View File

@@ -25,7 +25,7 @@ impl LocalPipelineDiagnosticsService {
pub async fn diagnose_for_validation(
&self,
) -> Result<crate::LocalPipelineDiagnosticSummaryDto, crate::Error> {
let counters_result = query_lightweight_validation_counters(self.database.as_ref()).await;
let counters_result = crate::query_local_pipeline_diagnostic_get_counters(self.database.as_ref()).await;
let counters = match counters_result {
Ok(counters) => counters,
Err(error) => return Err(error),
@@ -364,475 +364,21 @@ impl LocalPipelineDiagnosticsService {
}
}
async fn query_lightweight_validation_counters(
database: &crate::Database,
) -> Result<crate::LocalPipelineDiagnosticCountersDto, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let transaction_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_chain_transactions",
"transaction_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let ok_transaction_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NULL",
"ok_transaction_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let failed_transaction_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_chain_transactions WHERE err_json IS NOT NULL",
"failed_transaction_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_dex_decoded_events",
"decoded_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_trade_candidate_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.tradeCandidate') = 1", "decoded_trade_candidate_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_candle_candidate_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE json_extract(payload_json, '$.candleCandidate') = 1", "decoded_candle_candidate_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_non_trade_useful_event_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.nonTradeUseful'), 0) = 1 OR COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_trade_useful'", "decoded_non_trade_useful_event_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_non_actionable_trade_event_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_actionable_trade' OR (COALESCE(json_extract(payload_json, '$.eventActionability'), '') = '' AND COALESCE(json_extract(payload_json, '$.eventCategory'), '') = 'trade' AND COALESCE(json_extract(payload_json, '$.tradeCandidate'), 0) = 0 AND COALESCE(json_extract(payload_json, '$.transactionFailed'), 0) = 0)", "decoded_non_actionable_trade_event_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_unknown_event_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events WHERE COALESCE(json_extract(payload_json, '$.eventCategory'), 'unknown') = 'unknown'", "decoded_unknown_event_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let liquidity_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_liquidity_events",
"liquidity_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pool_lifecycle_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_pool_lifecycle_events",
"pool_lifecycle_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let fee_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_fee_events",
"fee_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let reward_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_reward_events",
"reward_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pool_admin_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_pool_admin_events",
"pool_admin_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let missing_trade_event_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)", "missing_trade_event_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_trade_candidate_without_trade_event_count = missing_trade_event_count;
let decoded_trade_candidate_without_trade_event_on_ok_transaction_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NULL AND dde.pool_account IS NOT NULL AND dde.token_a_mint IS NOT NULL AND dde.token_b_mint IS NOT NULL AND EXISTS (SELECT 1 FROM k_sol_pools p JOIN k_sol_pairs pair ON pair.pool_id = p.id WHERE p.address = dde.pool_account)", "decoded_trade_candidate_without_trade_event_on_ok_transaction_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let decoded_trade_candidate_without_trade_event_on_failed_transaction_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ct.err_json IS NOT NULL", "decoded_trade_candidate_without_trade_event_on_failed_transaction_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let actionable_missing_trade_event_count =
decoded_trade_candidate_without_trade_event_on_ok_transaction_count;
let ignored_failed_transaction_trade_candidate_count =
decoded_trade_candidate_without_trade_event_on_failed_transaction_count;
let decoded_trade_candidate_without_amount_payload_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_dex_decoded_events dde WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id) AND ((json_extract(dde.payload_json, '$.baseAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.base_amount_raw') IS NULL) OR (json_extract(dde.payload_json, '$.quoteAmountRaw') IS NULL AND json_extract(dde.payload_json, '$.quote_amount_raw') IS NULL))", "decoded_trade_candidate_without_amount_payload_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let trade_event_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_trade_events",
"trade_event_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let invalid_trade_event_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_trade_events WHERE base_amount_raw IS NULL OR quote_amount_raw IS NULL OR price_quote_per_base IS NULL OR CAST(base_amount_raw AS INTEGER) <= 0 OR CAST(quote_amount_raw AS INTEGER) <= 0 OR price_quote_per_base <= 0", "invalid_trade_event_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pair_candle_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(*) FROM k_sol_pair_candles",
"pair_candle_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let duplicate_decoded_event_trade_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT decoded_event_id FROM k_sol_trade_events WHERE decoded_event_id IS NOT NULL GROUP BY decoded_event_id HAVING COUNT(*) > 1)", "duplicate_decoded_event_trade_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let multi_trade_signature_pair_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT signature, pair_id FROM k_sol_trade_events GROUP BY signature, pair_id HAVING COUNT(*) > 1)", "multi_trade_signature_pair_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let duplicate_candle_bucket_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM (SELECT pair_id, timeframe_seconds, bucket_start_unix FROM k_sol_pair_candles GROUP BY pair_id, timeframe_seconds, bucket_start_unix HAVING COUNT(*) > 1)", "duplicate_candle_bucket_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let token_count = {
let counter_result =
query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens", "token_count")
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let token_metadata_missing_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_tokens WHERE symbol IS NULL OR TRIM(symbol) = '' OR name IS NULL OR TRIM(name) = ''", "token_metadata_missing_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let tradable_token_metadata_missing_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT token.id) FROM k_sol_tokens token JOIN (SELECT pair.base_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id UNION SELECT pair.quote_token_id AS token_id FROM k_sol_pairs pair JOIN k_sol_trade_events te ON te.pair_id = pair.id) tradable_pair_token ON tradable_pair_token.token_id = token.id WHERE token.symbol IS NULL OR TRIM(token.symbol) = '' OR token.name IS NULL OR TRIM(token.name) = ''", "tradable_token_metadata_missing_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let quote_token_metadata_missing_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(DISTINCT quote_token.id) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.symbol IS NULL OR TRIM(quote_token.symbol) = '' OR quote_token.name IS NULL OR TRIM(quote_token.name) = ''", "quote_token_metadata_missing_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pair_symbol_fallback_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NULL OR TRIM(pair.symbol) = '' OR pair.symbol = base_token.mint || '/' || quote_token.mint OR instr(pair.symbol, base_token.mint) > 0 OR instr(pair.symbol, quote_token.mint) > 0", "pair_symbol_fallback_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pair_symbol_resolved_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE pair.symbol IS NOT NULL AND TRIM(pair.symbol) != '' AND pair.symbol != base_token.mint || '/' || quote_token.mint AND instr(pair.symbol, base_token.mint) = 0 AND instr(pair.symbol, quote_token.mint) = 0", "pair_symbol_resolved_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let wsol_quote_pair_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint = 'So11111111111111111111111111111111111111112'", "wsol_quote_pair_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let stable_quote_pair_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id WHERE quote_token.mint IN ('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB', 'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD')", "stable_quote_pair_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pool_count = {
let counter_result =
query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pools", "pool_count")
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let pair_count = {
let counter_result =
query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs", "pair_count")
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let literal_pair_without_trade_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "literal_pair_without_trade_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let literal_pair_without_candle_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair WHERE NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "literal_pair_without_candle_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let trade_materialized_pair_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(DISTINCT pair_id) FROM k_sol_trade_events",
"trade_materialized_pair_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let candle_materialized_pair_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(DISTINCT pair_id) FROM k_sol_pair_candles",
"candle_materialized_pair_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let actionable_pair_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL)", "actionable_pair_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let candle_bucket_timeframe_count = {
let counter_result = query_validation_i64(
pool,
"SELECT COUNT(DISTINCT timeframe_seconds) FROM k_sol_pair_candles",
"candle_bucket_timeframe_count",
)
.await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let non_actionable_pair_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id)) AND NOT EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.decoded_event_id = dde.id))", "non_actionable_pair_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let blocking_pair_without_trade_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.tradeCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_trade_events te WHERE te.pair_id = pair.id)", "blocking_pair_without_trade_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
let blocking_pair_without_candle_count = {
let counter_result = query_validation_i64(pool, "SELECT COUNT(*) FROM k_sol_pairs pair JOIN k_sol_pools p ON p.id = pair.pool_id WHERE EXISTS (SELECT 1 FROM k_sol_dex_decoded_events dde JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id WHERE dde.pool_account = p.address AND json_extract(dde.payload_json, '$.candleCandidate') = 1 AND ct.err_json IS NULL) AND NOT EXISTS (SELECT 1 FROM k_sol_pair_candles pc WHERE pc.pair_id = pair.id)", "blocking_pair_without_candle_count").await;
match counter_result {
Ok(value) => value,
Err(error) => return Err(error),
}
};
return Ok(crate::LocalPipelineDiagnosticCountersDto {
transaction_count,
ok_transaction_count,
failed_transaction_count,
decoded_event_count,
decoded_trade_candidate_count,
decoded_candle_candidate_count,
decoded_non_trade_useful_event_count,
decoded_non_actionable_trade_event_count,
decoded_unknown_event_count,
liquidity_event_count,
pool_lifecycle_event_count,
fee_event_count,
reward_event_count,
pool_admin_event_count,
missing_trade_event_count,
decoded_trade_candidate_without_trade_event_count,
decoded_trade_candidate_without_trade_event_on_ok_transaction_count,
decoded_trade_candidate_without_trade_event_on_failed_transaction_count,
actionable_missing_trade_event_count,
ignored_failed_transaction_trade_candidate_count,
decoded_trade_candidate_without_amount_payload_count,
trade_event_count,
invalid_trade_event_count,
pair_candle_count,
duplicate_decoded_event_trade_count,
multi_trade_signature_pair_count,
duplicate_candle_bucket_count,
token_count,
token_metadata_missing_count,
tradable_token_metadata_missing_count,
quote_token_metadata_missing_count,
pair_symbol_fallback_count,
pair_symbol_resolved_count,
wsol_quote_pair_count,
stable_quote_pair_count,
pool_count,
pair_count,
literal_pair_without_trade_count,
literal_pair_without_candle_count,
trade_materialized_pair_count,
candle_materialized_pair_count,
actionable_pair_count,
candle_bucket_timeframe_count,
non_actionable_pair_count,
blocking_pair_without_trade_count,
blocking_pair_without_candle_count,
pair_without_trade_count: blocking_pair_without_trade_count,
pair_without_candle_count: blocking_pair_without_candle_count,
});
},
}
}
async fn query_validation_i64(
pool: &sqlx::Pool<sqlx::Sqlite>,
sql: &'static str,
counter_name: &str,
) -> Result<i64, crate::Error> {
let result = sqlx::query_scalar::<sqlx::Sqlite, i64>(sql).fetch_one(pool).await;
match result {
Ok(value) => return Ok(value),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot read local pipeline validation counter '{}' on sqlite: {}",
counter_name, error
)));
},
}
}
async fn load_event_coverage_summaries(
database: &crate::Database,
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
let coverage_service =
crate::DexEventCoverageService::new(std::sync::Arc::new(database.clone()));
let refresh_result = coverage_service.refresh_local_counts(None).await;
let refresh_result = match refresh_result {
Ok(refresh_result) => refresh_result,
let refresh_result =
crate::query_dex_event_coverage_entries_refresh_local_counts(database).await;
match refresh_result {
Ok(_) => {},
Err(error) => return Err(error),
};
return Ok(refresh_result.summaries);
}
let summaries_result =
crate::query_dex_event_coverage_entries_list_summary_by_decoder(database).await;
match summaries_result {
Ok(summaries) => return Ok(summaries),
Err(error) => return Err(error),
}
}
#[derive(Debug, Clone)]
@@ -871,23 +417,41 @@ fn aggregate_event_coverage_summaries(
upstream_git_local_corpus_materialized_entry_count: 0,
};
for summary in summaries {
aggregate.listed_entry_count += summary.listed_entry_count;
aggregate.decoded_entry_count += summary.decoded_entry_count;
aggregate.observed_entry_count += summary.observed_entry_count;
aggregate.materialized_entry_count += summary.materialized_entry_count;
aggregate.total_observed_count += summary.total_observed_count;
aggregate.total_materialized_count += summary.total_materialized_count;
aggregate.trade_count += summary.trade_count;
aggregate.audit_only_entry_count += summary.audit_only_entry_count;
aggregate.missing_db_target_entry_count += summary.missing_db_target_entry_count;
aggregate.upstream_git_unverified_entry_count +=
summary.upstream_git_unverified_entry_count;
aggregate.upstream_git_mapped_unverified_entry_count +=
summary.upstream_git_mapped_unverified_entry_count;
aggregate.upstream_git_local_corpus_observed_entry_count +=
summary.upstream_git_local_corpus_observed_entry_count;
aggregate.upstream_git_local_corpus_materialized_entry_count +=
summary.upstream_git_local_corpus_materialized_entry_count;
aggregate.listed_entry_count =
aggregate.listed_entry_count.saturating_add(summary.listed_entry_count);
aggregate.decoded_entry_count =
aggregate.decoded_entry_count.saturating_add(summary.decoded_entry_count);
aggregate.observed_entry_count = aggregate
.observed_entry_count
.saturating_add(summary.observed_entry_count);
aggregate.materialized_entry_count = aggregate
.materialized_entry_count
.saturating_add(summary.materialized_entry_count);
aggregate.total_observed_count = aggregate
.total_observed_count
.saturating_add(summary.total_observed_count);
aggregate.total_materialized_count = aggregate
.total_materialized_count
.saturating_add(summary.total_materialized_count);
aggregate.trade_count = aggregate.trade_count.saturating_add(summary.trade_count);
aggregate.audit_only_entry_count = aggregate
.audit_only_entry_count
.saturating_add(summary.audit_only_entry_count);
aggregate.missing_db_target_entry_count = aggregate
.missing_db_target_entry_count
.saturating_add(summary.missing_db_target_entry_count);
aggregate.upstream_git_unverified_entry_count = aggregate
.upstream_git_unverified_entry_count
.saturating_add(summary.upstream_git_unverified_entry_count);
aggregate.upstream_git_mapped_unverified_entry_count = aggregate
.upstream_git_mapped_unverified_entry_count
.saturating_add(summary.upstream_git_mapped_unverified_entry_count);
aggregate.upstream_git_local_corpus_observed_entry_count = aggregate
.upstream_git_local_corpus_observed_entry_count
.saturating_add(summary.upstream_git_local_corpus_observed_entry_count);
aggregate.upstream_git_local_corpus_materialized_entry_count = aggregate
.upstream_git_local_corpus_materialized_entry_count
.saturating_add(summary.upstream_git_local_corpus_materialized_entry_count);
}
return aggregate;
}

View File

@@ -7,7 +7,7 @@
//! deterministic local pipeline over their signatures.
const LOCAL_PIPELINE_DEX_DECODER_SCOPE: &str = "dex_decode.local_pipeline";
const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = "dex_decode.v0.7.46.damm_v1_events1";
const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = "dex_decode.v0.7.51.raydium_amm_v4_max_decoder";
fn default_skip_certified_dex_decode() -> bool {
return true;
@@ -280,6 +280,28 @@ impl LocalPipelineReplayService {
);
},
None => {
let replay_scope_delete_result =
crate::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
match replay_scope_delete_result {
Ok(deleted_count) => {
result.reset_market_materialization_deleted_count = result
.reset_market_materialization_deleted_count
.saturating_add(deleted_count);
if deleted_count > 0 {
tracing::debug!(
signature = %signature,
transaction_id,
deleted_count,
"local pipeline replay deleted stale local DEX replay scope before decode"
);
}
},
Err(error) => return Err(error),
}
let decode_result =
dex_decode.decode_transaction_by_signature(signature.as_str()).await;
match decode_result {

View File

@@ -482,38 +482,24 @@ impl NonTradeEventMaterializationService {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_pool_admin_events
WHERE decoded_event_id = ?
"#,
)
.bind(decoded_event_id)
.execute(pool)
.await;
let delete_result = match delete_result {
Ok(delete_result) => delete_result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete stale k_sol_pool_admin_events for lifecycle decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
};
let deleted_count = delete_result.rows_affected();
if deleted_count > 0 {
tracing::debug!(
decoded_event_id = decoded_event_id,
event_kind = %decoded_event.event_kind,
deleted_count = deleted_count,
"removed stale pool admin materialization for lifecycle event"
);
}
return Ok(());
},
let delete_result = crate::query_pool_admin_events_delete_by_decoded_event_id(
self.database.as_ref(),
decoded_event_id,
)
.await;
let deleted_count = match delete_result {
Ok(deleted_count) => deleted_count,
Err(error) => return Err(error),
};
if deleted_count > 0 {
tracing::debug!(
decoded_event_id = decoded_event_id,
event_kind = %decoded_event.event_kind,
deleted_count = deleted_count,
"removed stale pool admin materialization for lifecycle event"
);
}
return Ok(());
}
async fn materialize_pool_admin_event(
@@ -712,132 +698,28 @@ WHERE decoded_event_id = ?
},
None => None,
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let existing_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_launch_events
WHERE decoded_event_id = ?
LIMIT 1
"#,
)
.bind(decoded_event_id)
.fetch_optional(pool)
.await;
let existing_id = match existing_result {
Ok(existing_id) => existing_id,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_launch_events id for decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
};
if let Some(existing_id) = existing_id {
let update_result = sqlx::query(
r#"
UPDATE k_sol_launch_events
SET
transaction_id = ?,
dex_id = ?,
pool_id = ?,
pair_id = ?,
signature = ?,
slot = ?,
protocol_name = ?,
program_id = ?,
event_kind = ?,
pool_account = ?,
actor_wallet = ?,
event_role = ?,
related_account = ?,
related_mint = ?,
payload_json = ?,
executed_at = ?
WHERE id = ?
"#,
)
.bind(transaction_id)
.bind(context.dex_id)
.bind(context.pool_id)
.bind(context.pair_id)
.bind(transaction.signature.clone())
.bind(slot_i64)
.bind(decoded_event.protocol_name.clone())
.bind(decoded_event.program_id.clone())
.bind(decoded_event.event_kind.clone())
.bind(decoded_event.pool_account.clone())
.bind(actor_wallet.clone())
.bind(event_role.clone())
.bind(related_account.clone())
.bind(related_mint.clone())
.bind(decoded_event.payload_json.clone())
.bind(chrono::Utc::now().to_rfc3339())
.bind(existing_id)
.execute(pool)
.await;
if let Err(error) = update_result {
return Err(crate::Error::Db(format!(
"cannot update k_sol_launch_events id '{}' on sqlite: {}",
existing_id, error
)));
}
return Ok(true);
}
let insert_result = sqlx::query(
r#"
INSERT INTO k_sol_launch_events (
transaction_id,
decoded_event_id,
dex_id,
pool_id,
pair_id,
signature,
slot,
protocol_name,
program_id,
event_kind,
pool_account,
actor_wallet,
event_role,
related_account,
related_mint,
payload_json,
executed_at,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(transaction_id)
.bind(decoded_event_id)
.bind(context.dex_id)
.bind(context.pool_id)
.bind(context.pair_id)
.bind(transaction.signature.clone())
.bind(slot_i64)
.bind(decoded_event.protocol_name.clone())
.bind(decoded_event.program_id.clone())
.bind(decoded_event.event_kind.clone())
.bind(decoded_event.pool_account.clone())
.bind(actor_wallet)
.bind(event_role)
.bind(related_account)
.bind(related_mint)
.bind(decoded_event.payload_json.clone())
.bind(chrono::Utc::now().to_rfc3339())
.bind(chrono::Utc::now().to_rfc3339())
.execute(pool)
.await;
if let Err(error) = insert_result {
return Err(crate::Error::Db(format!(
"cannot insert k_sol_launch_events on sqlite: {}",
error
)));
}
return Ok(true);
},
let input = crate::LaunchEventUpsertInput {
transaction_id,
decoded_event_id,
dex_id: context.dex_id,
pool_id: context.pool_id,
pair_id: context.pair_id,
signature: transaction.signature.clone(),
slot: slot_i64,
protocol_name: decoded_event.protocol_name.clone(),
program_id: decoded_event.program_id.clone(),
event_kind: decoded_event.event_kind.clone(),
pool_account: decoded_event.pool_account.clone(),
actor_wallet,
event_role,
related_account,
related_mint,
payload_json: payload.clone(),
};
let upsert_result = crate::query_launch_events_upsert(self.database.as_ref(), &input).await;
match upsert_result {
Ok(_) => return Ok(true),
Err(error) => return Err(error),
}
}
@@ -861,15 +743,51 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
};
let dex_id = match context.dex_id {
Some(dex_id) => dex_id,
None => return Ok(false),
None => {
let annotate_result = self
.annotate_decoded_event_payload(
decoded_event,
"skipLiquidityReason",
"missing_dex_catalog",
)
.await;
if let Err(error) = annotate_result {
return Err(error);
}
return Ok(false);
},
};
let pool_id = match context.pool_id {
Some(pool_id) => pool_id,
None => return Ok(false),
None => {
let annotate_result = self
.annotate_decoded_event_payload(
decoded_event,
"skipLiquidityReason",
"missing_pool_catalog",
)
.await;
if let Err(error) = annotate_result {
return Err(error);
}
return Ok(false);
},
};
let pair = match context.pair {
Some(pair) => pair,
None => return Ok(false),
None => {
let annotate_result = self
.annotate_decoded_event_payload(
decoded_event,
"skipLiquidityReason",
"missing_pair_catalog",
)
.await;
if let Err(error) = annotate_result {
return Err(error);
}
return Ok(false);
},
};
let pair_id = match pair.id {
Some(pair_id) => Some(pair_id),
@@ -1087,6 +1005,57 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
}
}
async fn annotate_decoded_event_payload(
&self,
decoded_event: &crate::DexDecodedEventDto,
reason_key: &str,
reason_value: &str,
) -> Result<(), crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
let payload_result = serde_json::from_str::<serde_json::Value>(
decoded_event.payload_json.as_str(),
);
let mut object = match payload_result {
Ok(serde_json::Value::Object(object)) => object,
Ok(other) => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
Err(_) => serde_json::Map::new(),
};
let existing_reason = match object.get(reason_key).and_then(serde_json::Value::as_str) {
Some(existing_reason) => existing_reason.trim().to_string(),
None => std::string::String::new(),
};
if existing_reason.is_empty() {
object.insert(
reason_key.to_string(),
serde_json::Value::String(reason_value.to_string()),
);
}
if reason_key == "skipLiquidityReason" {
object.insert(
"skipCatalogReason".to_string(),
serde_json::Value::String(reason_value.to_string()),
);
}
let payload_json = serde_json::Value::Object(object).to_string();
let update_result = crate::query_dex_decoded_events_update_payload_json_by_id(
self.database.as_ref(),
decoded_event_id,
payload_json.as_str(),
)
.await;
match update_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn resolve_liquidity_context(
&self,
transaction: &crate::ChainTransactionDto,