This commit is contained in:
2026-05-29 07:38:24 +02:00
parent 96b6209482
commit ffa4acbccb
15 changed files with 1982 additions and 107 deletions

View File

@@ -2,6 +2,8 @@
//! Persistence-oriented DEX decoding service.
const METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX: &str = "e445a52e51cb9a1d";
/// DEX decode service.
#[derive(Debug, Clone)]
pub struct DexDecodeService {
@@ -206,7 +208,7 @@ impl DexDecodeService {
Err(error) => return Err(error),
};
let cleanup_result = self
.delete_replaced_raydium_instruction_audit(
.delete_replaced_instruction_audit(
transaction_id,
instruction_id,
protocol_name,
@@ -219,7 +221,7 @@ impl DexDecodeService {
return Ok(materialized);
}
async fn delete_replaced_raydium_instruction_audit(
async fn delete_replaced_instruction_audit(
&self,
transaction_id: i64,
instruction_id: i64,
@@ -229,15 +231,14 @@ impl DexDecodeService {
if event_kind.ends_with(".instruction_audit") {
return Ok(());
}
let audit_event_kind = match raydium_instruction_audit_event_kind_by_protocol(protocol_name)
{
let audit_event_kind = match instruction_audit_event_kind_by_protocol(protocol_name) {
Some(audit_event_kind) => audit_event_kind,
None => return Ok(()),
};
let delete_result = crate::query_dex_decoded_events_delete_by_key(
let delete_result = crate::query_dex_decoded_events_delete_related_instruction_audit(
self.database.as_ref(),
transaction_id,
Some(instruction_id),
instruction_id,
audit_event_kind,
)
.await;
@@ -505,6 +506,42 @@ impl DexDecodeService {
)
.await;
},
crate::MeteoraDlmmDecodedEvent::Fee(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dlmm",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
None,
None,
None,
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDlmmDecodedEvent::Reward(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dlmm",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
None,
None,
None,
event.payload_json.clone(),
)
.await;
},
}
}
@@ -1167,6 +1204,13 @@ impl DexDecodeService {
if decoded_instruction_ids.contains(&instruction_id) {
continue;
}
if is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap(
audit_spec.protocol_name,
instruction,
decoded_events.as_slice(),
) {
continue;
}
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
let payload = build_meteora_instruction_audit_payload(
transaction,
@@ -1773,6 +1817,35 @@ fn candidate_meteora_audit_pool_account(
return accounts.get(index).cloned();
}
fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap(
protocol_name: &str,
instruction: &crate::ChainInstructionDto,
decoded_events: &[crate::DexDecodedEventDto],
) -> bool {
if protocol_name != "meteora_dlmm" {
return false;
}
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 selector_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 0);
if selector_hex.as_deref() != Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) {
return false;
}
let event_discriminator_hex = discriminator_hex_from_bytes(data_bytes.as_deref(), 8);
match event_discriminator_hex.as_deref() {
Some("516ce3becdd00ac4") | Some("2e7452d7941b544d") => {},
_ => return false,
}
for decoded_event in decoded_events {
if decoded_event.protocol_name == "meteora_dlmm"
&& decoded_event.event_kind == "meteora_dlmm.swap"
{
return true;
}
}
return false;
}
fn build_meteora_instruction_audit_payload(
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
@@ -1786,10 +1859,36 @@ fn build_meteora_instruction_audit_payload(
None => 0,
};
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let discriminator_hex = discriminator_hex_from_base58(data_base58.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 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 {
discriminator_hex_from_bytes(data_bytes.as_deref(), 8)
} else {
None
};
let anchor_event_payload_size = if anchor_self_cpi_log {
match data_bytes.as_ref() {
Some(data_bytes) => data_bytes.len().checked_sub(8),
None => None,
}
} else {
None
};
let data_prefix = data_base58
.as_ref()
.map(|value| return value.chars().take(16).collect::<std::string::String>());
let audit_reason = if anchor_self_cpi_log {
"meteora_anchor_self_cpi_log_not_decoded_by_specific_event_decoder"
} else {
"meteora_instruction_not_decoded_by_specific_decoder"
};
let proof_status = if anchor_self_cpi_log {
"observed_local_corpus_anchor_self_cpi_log"
} else {
"unclassified_local_corpus_instruction"
};
return serde_json::json!({
"decoder": protocol_name,
"eventKind": event_kind,
@@ -1806,8 +1905,12 @@ fn build_meteora_instruction_audit_payload(
"data": data_base58,
"dataPrefix": data_prefix,
"discriminatorHex": discriminator_hex,
"auditReason": "meteora_instruction_not_decoded_by_specific_decoder",
"proofStatus": "unclassified_local_corpus_instruction",
"anchorSelfCpiLog": anchor_self_cpi_log,
"anchorSelfCpiLogSelectorHex": if anchor_self_cpi_log { Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) } else { None },
"anchorEventDiscriminatorHex": anchor_event_discriminator_hex,
"anchorEventPayloadSize": anchor_event_payload_size,
"auditReason": audit_reason,
"proofStatus": proof_status,
"tradeCandidate": false,
"candleCandidate": false,
"nonTradeUseful": false,
@@ -1816,13 +1919,14 @@ fn build_meteora_instruction_audit_payload(
});
}
fn raydium_instruction_audit_event_kind_by_protocol(
fn instruction_audit_event_kind_by_protocol(
protocol_name: &str,
) -> std::option::Option<&'static str> {
match protocol_name {
"raydium_amm_v4" => return Some("raydium_amm_v4.instruction_audit"),
"raydium_clmm" => return Some("raydium_clmm.instruction_audit"),
"raydium_cpmm" => return Some("raydium_cpmm.instruction_audit"),
"meteora_dlmm" => return Some("meteora_dlmm.instruction_audit"),
_ => return None,
}
}
@@ -1932,21 +2036,28 @@ fn parse_instruction_data_base58(
fn discriminator_hex_from_base58(
data_base58: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let data_base58 = match data_base58 {
Some(data_base58) => data_base58,
let bytes = instruction_data_bytes_from_base58(data_base58);
return discriminator_hex_from_bytes(bytes.as_deref(), 0);
}
fn discriminator_hex_from_bytes(
bytes: std::option::Option<&[u8]>,
offset: usize,
) -> std::option::Option<std::string::String> {
let bytes = match bytes {
Some(bytes) => bytes,
None => return None,
};
let bytes_result = bs58::decode(data_base58).into_vec();
let bytes = match bytes_result {
Ok(bytes) => bytes,
Err(_) => return None,
};
if bytes.len() < 8 {
if bytes.len() < offset + 8 {
return None;
}
let mut text = std::string::String::new();
for byte in bytes.iter().take(8) {
let mut index = offset;
let end = offset + 8;
while index < end {
let byte = bytes[index];
text.push_str(format!("{byte:02x}").as_str());
index += 1;
}
return Some(text);
}
@@ -3012,4 +3123,17 @@ mod tests {
};
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
}
#[test]
fn maps_instruction_audit_event_kind_for_raydium_and_meteora_dlmm_protocols() {
assert_eq!(
super::instruction_audit_event_kind_by_protocol("raydium_clmm"),
Some("raydium_clmm.instruction_audit")
);
assert_eq!(
super::instruction_audit_event_kind_by_protocol("meteora_dlmm"),
Some("meteora_dlmm.instruction_audit")
);
assert_eq!(super::instruction_audit_event_kind_by_protocol("unknown"), None);
}
}