Files
khadhroony-bobobot/kb_lib/src/dex_decode.rs
2026-06-11 17:22:55 +02:00

6493 lines
255 KiB
Rust

// file: kb_lib/src/dex_decode.rs
//! Persistence-oriented DEX decoding service.
const METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX: &str = "e445a52e51cb9a1d";
/// DEX decode service.
#[derive(Debug, Clone)]
pub struct DexDecodeService {
database: std::sync::Arc<crate::Database>,
persistence: crate::DetectionPersistenceService,
raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder,
raydium_clmm_decoder: crate::RaydiumClmmDecoder,
raydium_stable_swap_decoder: crate::RaydiumStableSwapDecoder,
pump_fun_decoder: crate::PumpFunDecoder,
pump_swap_decoder: crate::PumpSwapDecoder,
orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder,
meteora_dbc_decoder: crate::MeteoraDbcDecoder,
meteora_dlmm_decoder: crate::MeteoraDlmmDecoder,
meteora_damm_v1_decoder: crate::MeteoraDammV1Decoder,
meteora_damm_v2_decoder: crate::MeteoraDammV2Decoder,
fluxbeam_decoder: crate::FluxbeamDecoder,
dexlab_decoder: crate::DexlabDecoder,
openbook_v2_decoder: crate::OpenBookV2Decoder,
phoenix_v1_decoder: crate::PhoenixV1Decoder,
}
impl DexDecodeService {
/// Creates a new DEX decode service.
pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
let persistence = crate::DetectionPersistenceService::new(database.clone());
return Self {
database,
persistence,
raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder::new(),
raydium_clmm_decoder: crate::RaydiumClmmDecoder::new(),
raydium_stable_swap_decoder: crate::RaydiumStableSwapDecoder::new(),
pump_fun_decoder: crate::PumpFunDecoder::new(),
pump_swap_decoder: crate::PumpSwapDecoder::new(),
orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder::new(),
meteora_dbc_decoder: crate::MeteoraDbcDecoder::new(),
meteora_dlmm_decoder: crate::MeteoraDlmmDecoder::new(),
meteora_damm_v1_decoder: crate::MeteoraDammV1Decoder::new(),
meteora_damm_v2_decoder: crate::MeteoraDammV2Decoder::new(),
fluxbeam_decoder: crate::FluxbeamDecoder::new(),
dexlab_decoder: crate::DexlabDecoder::new(),
openbook_v2_decoder: crate::OpenBookV2Decoder::new(),
phoenix_v1_decoder: crate::PhoenixV1Decoder::new(),
};
}
/// Decodes one projected transaction and persists the decoded events.
pub async fn decode_transaction_by_signature(
&self,
signature: &str,
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let context_result = crate::dex_decode_context::load_dex_decode_transaction_context(
self.database.as_ref(),
signature,
)
.await;
let context = match context_result {
Ok(context) => context,
Err(error) => return Err(error),
};
let transaction = context.transaction;
let instructions = context.instructions;
let mut persisted = std::vec::Vec::new();
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_raydium_amm_v4_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_raydium_cpmm_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_raydium_stable_swap_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_raydium_clmm_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.preserve_unmatched_raydium_instruction_audits(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let cleanup_result =
self.cleanup_replaced_raydium_cpmm_instruction_audits(&transaction).await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result =
self.cleanup_replaced_raydium_clmm_instruction_audits(&transaction).await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result = self
.cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits(&transaction)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_pump_fun_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_pump_swap_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_meteora_dbc_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_meteora_dlmm_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_meteora_damm_v1_events(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_meteora_damm_v2_events(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.preserve_unmatched_meteora_instruction_audits(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_orca_whirlpools_events(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_fluxbeam_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_dexlab_events(&transaction, &instructions).await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_openbook_v2_audit_events(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_phoenix_v1_audit_events(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let decoded_instruction_ids = decoded_instruction_ids_from_persisted_events(&persisted);
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_upstream_registry_matches(
&transaction,
&instructions,
&decoded_instruction_ids,
)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let cleanup_result =
self.cleanup_replaced_raydium_cpmm_instruction_audits(&transaction).await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result =
self.cleanup_replaced_raydium_clmm_instruction_audits(&transaction).await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result = self
.cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits(&transaction)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
let reconcile_result =
self.reconcile_raydium_clmm_confirmed_non_trade_events(&transaction).await;
if let Err(error) = reconcile_result {
return Err(error);
}
return Ok(persisted);
}
async fn cleanup_replaced_raydium_cpmm_instruction_audits(
&self,
transaction: &crate::ChainTransactionDto,
) -> Result<(), crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => return Ok(()),
};
let cleanup_result =
crate::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits(
self.database.as_ref(),
Some(transaction_id),
)
.await;
match cleanup_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn cleanup_replaced_raydium_clmm_instruction_audits(
&self,
transaction: &crate::ChainTransactionDto,
) -> Result<(), crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => return Ok(()),
};
let cleanup_result =
crate::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits(
self.database.as_ref(),
Some(transaction_id),
)
.await;
match cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::debug!(
signature = %transaction.signature,
deleted_count,
"cleaned replaced Raydium CLMM instruction audits"
);
}
return Ok(());
},
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot cleanup replaced Raydium CLMM instruction audits for signature '{}': {}",
transaction.signature, error
)));
},
}
}
async fn reconcile_raydium_clmm_confirmed_non_trade_events(
&self,
transaction: &crate::ChainTransactionDto,
) -> Result<(), crate::Error> {
if dex_decode_transaction_has_effective_error(transaction) {
return Ok(());
}
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let decoded_events = match decoded_events_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut delete_create_pool_audit = false;
let mut delete_collect_protocol_fee_audit = false;
for decoded_event in &decoded_events {
if decoded_event.protocol_name != "raydium_clmm" {
continue;
}
if decoded_event.event_kind == "raydium_clmm.create_pool" {
let materialize_result = self
.materialize_raydium_clmm_create_pool_lifecycle(transaction, decoded_event)
.await;
if let Err(error) = materialize_result {
return Err(error);
}
delete_create_pool_audit = true;
continue;
}
if decoded_event.event_kind == "raydium_clmm.collect_protocol_fee" {
let materialize_result = self
.materialize_raydium_clmm_collect_protocol_fee(transaction, decoded_event)
.await;
if let Err(error) = materialize_result {
return Err(error);
}
delete_collect_protocol_fee_audit = true;
}
}
if delete_create_pool_audit {
let delete_result = self
.delete_raydium_clmm_instruction_audit_by_discriminator(
transaction_id,
"e992d18ecf6840bc",
)
.await;
if let Err(error) = delete_result {
return Err(error);
}
}
if delete_collect_protocol_fee_audit {
let delete_result = self
.delete_raydium_clmm_instruction_audit_by_discriminator(
transaction_id,
"8888fcddc2427e59",
)
.await;
if let Err(error) = delete_result {
return Err(error);
}
}
return Ok(());
}
async fn materialize_raydium_clmm_create_pool_lifecycle(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<(), crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
let context_result = self.resolve_decoded_event_db_context(decoded_event).await;
let context = match context_result {
Ok(context) => context,
Err(error) => return Err(error),
};
let dto = crate::PoolLifecycleEventDto::new(
decoded_event.transaction_id,
Some(decoded_event_id),
context.0,
context.1,
context.2,
transaction.signature.clone(),
transaction.slot,
decoded_event.protocol_name.clone(),
decoded_event.program_id.clone(),
decoded_event.event_kind.clone(),
decoded_event.pool_account.clone(),
decoded_event.token_a_mint.clone(),
decoded_event.token_b_mint.clone(),
decoded_event.payload_json.clone(),
);
let upsert_result =
crate::query_pool_lifecycle_events_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn materialize_raydium_clmm_collect_protocol_fee(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<(), crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
let payload = dex_decode_payload_value(decoded_event.payload_json.as_str());
let context_result = self.resolve_decoded_event_db_context(decoded_event).await;
let context = match context_result {
Ok(context) => context,
Err(error) => return Err(error),
};
let actor_wallet = dex_decode_extract_first_string(
&payload,
&["authority", "actorWallet", "actor_wallet", "owner", "payer", "user"],
);
let fee_token_mint = dex_decode_extract_first_string(
&payload,
&[
"vault_0_mint",
"vault0Mint",
"feeTokenMint",
"fee_token_mint",
"tokenMint",
"token_mint",
"mint",
],
);
let fee_amount_raw = dex_decode_extract_first_amount_string(
&payload,
&[
"amount0RequestedRaw",
"amount_0_requested_raw",
"tokenAAmount",
"token_a_amount",
"feeAmountRaw",
"fee_amount_raw",
"protocolFeeAmount",
"protocol_fee_amount",
"amount",
],
);
let dto = crate::FeeEventDto::new(
decoded_event.transaction_id,
Some(decoded_event_id),
context.0,
context.1,
context.2,
transaction.signature.clone(),
transaction.slot,
decoded_event.protocol_name.clone(),
decoded_event.program_id.clone(),
decoded_event.event_kind.clone(),
decoded_event.pool_account.clone(),
actor_wallet,
fee_token_mint,
fee_amount_raw,
decoded_event.payload_json.clone(),
);
let upsert_result = crate::query_fee_events_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn resolve_decoded_event_db_context(
&self,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<
(std::option::Option<i64>, std::option::Option<i64>, std::option::Option<i64>),
crate::Error,
> {
let dex_result = crate::query_dexs_get_by_code(
self.database.as_ref(),
decoded_event.protocol_name.as_str(),
)
.await;
let dex_id = match dex_result {
Ok(Some(dex)) => dex.id,
Ok(None) => None,
Err(error) => return Err(error),
};
let pool_account = match decoded_event.pool_account.as_ref() {
Some(pool_account) => pool_account,
None => return Ok((dex_id, None, None)),
};
let pool_result =
crate::query_pools_get_by_address(self.database.as_ref(), pool_account.as_str()).await;
let pool = match pool_result {
Ok(Some(pool)) => pool,
Ok(None) => return Ok((dex_id, None, None)),
Err(error) => return Err(error),
};
let pool_id = match pool.id {
Some(pool_id) => pool_id,
None => return Ok((dex_id, None, None)),
};
let pair_result = crate::query_pairs_get_by_pool_id(self.database.as_ref(), pool_id).await;
let pair = match pair_result {
Ok(pair) => pair,
Err(error) => return Err(error),
};
let pair_id = match pair {
Some(pair) => pair.id,
None => None,
};
return Ok((dex_id, Some(pool_id), pair_id));
}
async fn delete_raydium_clmm_instruction_audit_by_discriminator(
&self,
transaction_id: i64,
discriminator_hex: &str,
) -> Result<(), crate::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),
}
}
async fn materialize_named_dex_event(
&self,
transaction: &crate::ChainTransactionDto,
transaction_id: i64,
instruction_id: i64,
protocol_name: &str,
program_id: std::string::String,
event_kind: &str,
pool_account: std::option::Option<std::string::String>,
market_account: std::option::Option<std::string::String>,
token_a_mint: std::option::Option<std::string::String>,
token_b_mint: std::option::Option<std::string::String>,
lp_mint: std::option::Option<std::string::String>,
payload_json: serde_json::Value,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let payload_json_for_cleanup = payload_json.clone();
let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
database: self.database.as_ref(),
persistence: &self.persistence,
transaction,
transaction_id,
instruction_id: Some(instruction_id),
protocol_name: protocol_name.to_string(),
program_id,
event_kind: event_kind.to_string(),
pool_account,
market_account,
token_a_mint,
token_b_mint,
lp_mint,
enrichment_payload_json: payload_json.clone(),
observation_payload_json: payload_json,
observation_kind: format!("dex.{event_kind}"),
signal_kind: format!("signal.dex.{event_kind}"),
missing_after_upsert_message: "decoded event disappeared after upsert".to_string(),
};
let materialized_result =
crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input).await;
let materialized = match materialized_result {
Ok(materialized) => materialized,
Err(error) => return Err(error),
};
let cleanup_result = self
.delete_replaced_instruction_audit(
transaction_id,
instruction_id,
protocol_name,
event_kind,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result = self
.delete_replaced_instruction_audit_by_discriminator(
transaction_id,
protocol_name,
event_kind,
&payload_json_for_cleanup,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
let cleanup_result = self
.delete_replaced_upstream_registry_match(
transaction_id,
instruction_id,
protocol_name,
event_kind,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
let non_trade_result = self
.materialize_direct_decoded_non_trade_if_needed(transaction, &materialized)
.await;
if let Err(error) = non_trade_result {
return Err(error);
}
return Ok(materialized);
}
async fn materialize_direct_decoded_non_trade_if_needed(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<(), crate::Error> {
if dex_decode_transaction_has_effective_error(transaction) {
return Ok(());
}
if !should_immediately_materialize_decoded_non_trade_event(
decoded_event.event_kind.as_str(),
) {
return Ok(());
}
if decoded_event.event_kind == "raydium_clmm.create_pool" {
return self
.materialize_raydium_clmm_create_pool_lifecycle(transaction, decoded_event)
.await;
}
if decoded_event.event_kind == "raydium_clmm.collect_protocol_fee" {
return self
.materialize_raydium_clmm_collect_protocol_fee(transaction, decoded_event)
.await;
}
return Ok(());
}
async fn delete_replaced_instruction_audit_by_discriminator(
&self,
transaction_id: i64,
protocol_name: &str,
event_kind: &str,
payload_json: &serde_json::Value,
) -> Result<(), crate::Error> {
if event_kind.ends_with(".instruction_audit") {
return Ok(());
}
let discriminator_hex = match instruction_discriminator_hex_from_payload(payload_json) {
Some(discriminator_hex) => discriminator_hex,
None => return Ok(()),
};
return self
.delete_replaced_instruction_audit_by_discriminator_hex(
transaction_id,
protocol_name,
discriminator_hex.as_str(),
)
.await;
}
async fn delete_replaced_instruction_audit_by_discriminator_hex(
&self,
transaction_id: i64,
protocol_name: &str,
discriminator_hex: &str,
) -> Result<(), crate::Error> {
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_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),
}
}
async fn delete_replaced_raydium_launchpad_anchor_self_cpi_audit(
&self,
transaction_id: i64,
_instruction_id: i64,
anchor_event_discriminator_hex: &str,
) -> Result<(), crate::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),
}
}
async fn cleanup_replaced_raydium_launchpad_anchor_self_cpi_audits(
&self,
transaction: &crate::ChainTransactionDto,
) -> Result<(), crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => return Ok(()),
};
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,
)
.await;
match cleanup_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn delete_replaced_upstream_registry_match(
&self,
transaction_id: i64,
instruction_id: i64,
protocol_name: &str,
event_kind: &str,
) -> Result<(), crate::Error> {
if protocol_name == crate::UPSTREAM_REGISTRY_PROTOCOL_NAME {
return Ok(());
}
if event_kind == crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND {
return Ok(());
}
let delete_result = crate::query_dex_decoded_events_delete_by_key(
self.database.as_ref(),
transaction_id,
Some(instruction_id),
crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND,
)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn delete_replaced_instruction_audit(
&self,
transaction_id: i64,
instruction_id: i64,
protocol_name: &str,
event_kind: &str,
) -> Result<(), crate::Error> {
if event_kind.ends_with(".instruction_audit") {
return Ok(());
}
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_related_instruction_audit(
self.database.as_ref(),
transaction_id,
instruction_id,
audit_event_kind,
)
.await;
match delete_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
async fn decode_and_persist_upstream_registry_matches(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
already_decoded_instruction_ids: &std::collections::HashSet<i64>,
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let decoded_events = match decoded_events_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut decoded_instruction_ids = already_decoded_instruction_ids.clone();
for decoded_event in &decoded_events {
let instruction_id = match decoded_event.instruction_id {
Some(instruction_id) => instruction_id,
None => continue,
};
decoded_instruction_ids.insert(instruction_id);
}
let mut persisted = std::vec::Vec::new();
for instruction in instructions {
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
if decoded_instruction_ids.contains(&instruction_id) {
continue;
}
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let registry_match =
crate::upstream_registry_match::upstream_registry_match_instruction_data(
program_id.as_str(),
data_base58.as_deref(),
);
let registry_match = match registry_match {
Some(registry_match) => registry_match,
None => continue,
};
if upstream_registry_instruction_match_is_locally_covered(&registry_match) {
continue;
}
let payload = build_upstream_registry_instruction_match_payload(
transaction,
instruction,
&registry_match,
data_base58.as_deref(),
);
let persist_result = self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
crate::UPSTREAM_REGISTRY_PROTOCOL_NAME,
program_id.clone(),
crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND,
None,
None,
None,
None,
None,
payload,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn persist_dexlab_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::DexlabDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::DexlabDecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"dexlab",
event.program_id.clone(),
"dexlab.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
crate::DexlabDecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"dexlab",
event.program_id.clone(),
"dexlab.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_fluxbeam_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::FluxbeamDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::FluxbeamDecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"fluxbeam",
event.program_id.clone(),
"fluxbeam.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
crate::FluxbeamDecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"fluxbeam",
event.program_id.clone(),
"fluxbeam.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_orca_whirlpools_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::OrcaWhirlpoolsDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::OrcaWhirlpoolsDecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"orca_whirlpools",
event.program_id.clone(),
"orca_whirlpools.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.config_account.clone(),
event.payload_json.clone(),
)
.await;
},
crate::OrcaWhirlpoolsDecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"orca_whirlpools",
event.program_id.clone(),
"orca_whirlpools.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_meteora_dbc_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::MeteoraDbcDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::MeteoraDbcDecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dbc",
event.program_id.clone(),
"meteora_dbc.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.config_account.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDbcDecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dbc",
event.program_id.clone(),
"meteora_dbc.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_meteora_dlmm_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::MeteoraDlmmDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::MeteoraDlmmDecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dlmm",
event.program_id.clone(),
"meteora_dlmm.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.config_account.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDlmmDecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_dlmm",
event.program_id.clone(),
"meteora_dlmm.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDlmmDecodedEvent::Liquidity(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,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDlmmDecodedEvent::PoolLifecycle(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,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.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;
},
}
}
async fn persist_meteora_damm_v1_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::MeteoraDammV1DecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::MeteoraDammV1DecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
"meteora_damm_v1.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDammV1DecodedEvent::Swap(event) => {
let enrichment_payload_json =
prepare_meteora_damm_v1_swap_payload_for_classification(event);
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
"meteora_damm_v1.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
enrichment_payload_json,
)
.await;
},
crate::MeteoraDammV1DecodedEvent::Liquidity(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDammV1DecodedEvent::Fee(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
None,
None,
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDammV1DecodedEvent::PoolLifecycle(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.lp_mint.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDammV1DecodedEvent::PoolAdmin(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v1",
event.program_id.clone(),
event.event_kind.as_str(),
event.pool_account.clone(),
None,
None,
None,
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_meteora_damm_v2_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::MeteoraDammV2DecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::MeteoraDammV2DecodedEvent::CreatePool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v2",
event.program_id.clone(),
"meteora_damm_v2.create_pool",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
event.config_account.clone(),
event.payload_json.clone(),
)
.await;
},
crate::MeteoraDammV2DecodedEvent::Swap(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"meteora_damm_v2",
event.program_id.clone(),
"meteora_damm_v2.swap",
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn persist_raydium_amm_v4_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::RaydiumAmmV4DecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"raydium_amm_v4",
event.program_id.clone(),
"raydium_amm_v4.initialize2_pool",
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;
},
crate::RaydiumAmmV4DecodedEvent::Swap(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(),
Some(event.pool_account.clone()),
None,
Some(event.token_a_mint.clone()),
Some(event.token_b_mint.clone()),
None,
event.payload_json.clone(),
)
.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;
},
}
}
async fn persist_raydium_clmm_event(
&self,
transaction: &crate::ChainTransactionDto,
instruction_id: i64,
decoded_event: &crate::RaydiumClmmDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let event_kind = decoded_event.event_kind().to_string();
let raw_payload_json = match decoded_event.to_payload_json() {
Some(payload_json) => payload_json,
None => {
return Err(crate::Error::Json(
"cannot serialize decoded raydium clmm payload".to_string(),
));
},
};
let payload_value_result = enriched_raydium_payload_value(
"raydium_clmm",
event_kind.as_str(),
raw_payload_json.as_str(),
);
let payload_value = match payload_value_result {
Ok(payload_value) => payload_value,
Err(error) => return Err(error),
};
return self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
"raydium_clmm",
crate::RAYDIUM_CLMM_PROGRAM_ID.to_string(),
event_kind.as_str(),
decoded_event.pool_account_option().map(|value| return value.to_string()),
None,
decoded_event.base_mint_option().map(|value| return value.to_string()),
decoded_event.quote_mint_option().map(|value| return value.to_string()),
None,
payload_value,
)
.await;
}
async fn persist_raydium_cpmm_event(
&self,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
decoded_event: &crate::RaydiumCpmmDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"raydium cpmm instruction for transaction '{}' has no internal id",
transaction.signature
)));
},
};
let event_kind = decoded_event.event_kind().to_string();
let raw_payload_json = match decoded_event.to_payload_json() {
Some(payload_json) => payload_json,
None => {
return Err(crate::Error::Json(
"cannot serialize decoded raydium cpmm payload".to_string(),
));
},
};
let payload_value_result = enriched_raydium_payload_value(
"raydium_cpmm",
event_kind.as_str(),
raw_payload_json.as_str(),
);
let payload_value = match payload_value_result {
Ok(payload_value) => payload_value,
Err(error) => return Err(error),
};
return self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
"raydium_cpmm",
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
event_kind.as_str(),
decoded_event.pool_account().map(|value| return value.to_string()),
None,
decoded_event.base_mint().map(|value| return value.to_string()),
decoded_event.quote_mint().map(|value| return value.to_string()),
decoded_event.lp_mint().map(|value| return value.to_string()),
payload_value,
)
.await;
}
async fn persist_raydium_stable_swap_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::RaydiumStableSwapDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let instruction_id = match decoded_event.instruction_id() {
Some(instruction_id) => instruction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"raydium stable swap decoded event for transaction '{}' has no instruction id",
transaction.signature
)));
},
};
let event_kind = decoded_event.event_kind().to_string();
let raw_payload_json = match decoded_event.to_payload_json() {
Some(payload_json) => payload_json,
None => {
return Err(crate::Error::Json(
"cannot serialize decoded raydium stable swap payload".to_string(),
));
},
};
let payload_value_result = enriched_raydium_payload_value(
"raydium_stable_swap",
event_kind.as_str(),
raw_payload_json.as_str(),
);
let payload_value = match payload_value_result {
Ok(payload_value) => payload_value,
Err(error) => return Err(error),
};
return self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
"raydium_stable_swap",
crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID.to_string(),
event_kind.as_str(),
decoded_event.pool_account().map(|value| return value.to_string()),
decoded_event.market_account().map(|value| return value.to_string()),
decoded_event.base_mint().map(|value| return value.to_string()),
decoded_event.quote_mint().map(|value| return value.to_string()),
decoded_event.lp_mint().map(|value| return value.to_string()),
payload_value,
)
.await;
}
async fn persist_pump_fun_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::PumpFunDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::PumpFunDecodedEvent::CreateV2Token(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"pump_fun",
event.program_id.clone(),
"pump_fun.create_v2_token",
event.bonding_curve.clone(),
None,
event.mint.clone(),
Some(crate::WSOL_MINT_ID.to_string()),
event.associated_bonding_curve.clone(),
event.payload_json.clone(),
)
.await;
},
crate::PumpFunDecodedEvent::BuyTrade(event) => {
return self
.persist_pump_fun_trade_event(
transaction,
event,
"pump_fun.buy",
"signal.dex.pump_fun.buy",
"dex.pump_fun.buy",
)
.await;
},
crate::PumpFunDecodedEvent::SellTrade(event) => {
return self
.persist_pump_fun_trade_event(
transaction,
event,
"pump_fun.sell",
"signal.dex.pump_fun.sell",
"dex.pump_fun.sell",
)
.await;
},
}
}
async fn persist_pump_fun_trade_event(
&self,
transaction: &crate::ChainTransactionDto,
event: &crate::PumpFunTradeDecoded,
event_kind: &str,
signal_kind: &str,
observation_kind: &str,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
database: self.database.as_ref(),
persistence: &self.persistence,
transaction,
transaction_id: event.transaction_id,
instruction_id: Some(event.instruction_id),
protocol_name: "pump_fun".to_string(),
program_id: event.program_id.clone(),
event_kind: event_kind.to_string(),
pool_account: event.bonding_curve.clone(),
market_account: None,
token_a_mint: event.mint.clone(),
token_b_mint: Some(crate::WSOL_MINT_ID.to_string()),
lp_mint: event.associated_bonding_curve.clone(),
enrichment_payload_json: event.payload_json.clone(),
observation_payload_json: event.payload_json.clone(),
observation_kind: observation_kind.to_string(),
signal_kind: signal_kind.to_string(),
missing_after_upsert_message: "decoded pump.fun trade event disappeared after upsert"
.to_string(),
};
return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
.await;
}
async fn persist_pump_swap_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::PumpSwapDecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::PumpSwapDecodedEvent::BuyTrade(event) => {
return self
.persist_pump_swap_trade_event(
transaction,
event,
"pump_swap.buy",
"signal.dex.pump_swap.buy",
"dex.pump_swap.buy",
)
.await;
},
crate::PumpSwapDecodedEvent::SellTrade(event) => {
return self
.persist_pump_swap_trade_event(
transaction,
event,
"pump_swap.sell",
"signal.dex.pump_swap.sell",
"dex.pump_swap.sell",
)
.await;
},
}
}
async fn persist_pump_swap_trade_event(
&self,
transaction: &crate::ChainTransactionDto,
event: &crate::PumpSwapTradeDecoded,
event_kind: &str,
signal_kind: &str,
observation_kind: &str,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
let enrichment_payload_json = prepare_pump_swap_trade_payload_for_classification(event);
let input = crate::dex_decoded_event_materialization::DexDecodedEventMaterializationInput {
database: self.database.as_ref(),
persistence: &self.persistence,
transaction,
transaction_id: event.transaction_id,
instruction_id: Some(event.instruction_id),
protocol_name: "pump_swap".to_string(),
program_id: event.program_id.clone(),
event_kind: event_kind.to_string(),
pool_account: event.pool_account.clone(),
market_account: None,
token_a_mint: event.token_a_mint.clone(),
token_b_mint: event.token_b_mint.clone(),
lp_mint: event.pool_v2.clone(),
enrichment_payload_json,
observation_payload_json: event.payload_json.clone(),
observation_kind: observation_kind.to_string(),
signal_kind: signal_kind.to_string(),
missing_after_upsert_message: "decoded event disappeared after upsert".to_string(),
};
return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
.await;
}
async fn decode_and_persist_raydium_cpmm_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let mut persisted = std::vec::Vec::new();
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
if program_id.as_str() != crate::RAYDIUM_CPMM_PROGRAM_ID {
continue;
}
let data_json = match instruction.data_json.as_ref() {
Some(data_json) => data_json,
None => continue,
};
let instruction_kind =
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
let decoded_events = crate::decode_raydium_cpmm_instruction(
instruction.accounts_json.as_str(),
data_json.as_str(),
);
for decoded_event in &decoded_events {
let persist_result =
self.persist_raydium_cpmm_event(transaction, instruction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event(
self,
transaction,
instruction,
instruction_kind,
&mut program_data_events,
&mut persisted,
)
.await;
if let Err(error) = program_data_persist_result {
return Err(error);
}
}
return Ok(persisted);
}
async fn decode_and_persist_raydium_stable_swap_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self
.raydium_stable_swap_decoder
.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self
.persist_raydium_stable_swap_event(transaction, decoded_event)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_raydium_clmm_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.raydium_clmm_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self
.persist_raydium_clmm_event(
transaction,
decoded_event.instruction_id,
&decoded_event.decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
let mut program_data_events = collect_raydium_clmm_program_data_events(transaction);
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
if program_id.as_str() != crate::RAYDIUM_CLMM_PROGRAM_ID {
continue;
}
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref());
let persist_result = persist_matching_raydium_clmm_program_data_events(
self,
transaction,
instruction,
discriminator_hex.as_deref(),
&mut program_data_events,
&mut persisted,
)
.await;
if let Err(error) = persist_result {
return Err(error);
}
}
return Ok(persisted);
}
async fn decode_and_persist_raydium_amm_v4_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.raydium_amm_v4_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result =
self.persist_raydium_amm_v4_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn preserve_unmatched_raydium_instruction_audits(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let decoded_events = match decoded_events_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut decoded_instruction_ids = std::collections::HashSet::<i64>::new();
let mut decoded_discriminator_keys =
std::collections::HashSet::<std::string::String>::new();
for decoded_event in &decoded_events {
if !decoded_event.protocol_name.starts_with("raydium_") {
continue;
}
if decoded_event.event_kind.ends_with(".instruction_audit") {
continue;
}
if let Some(instruction_id) = decoded_event.instruction_id {
decoded_instruction_ids.insert(instruction_id);
}
let discriminator =
instruction_discriminator_hex_from_payload_str(decoded_event.payload_json.as_str());
if let Some(discriminator) = discriminator {
decoded_discriminator_keys.insert(raydium_decoded_discriminator_key(
decoded_event.protocol_name.as_str(),
discriminator.as_str(),
));
}
}
let mut persisted = std::vec::Vec::new();
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
let audit_spec = match raydium_instruction_audit_spec(program_id.as_str()) {
Some(audit_spec) => audit_spec,
None => continue,
};
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
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 = 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(),
);
let dedupe_discriminator_hex = match anchor_event_spec {
Some(anchor_event_spec) => Some(anchor_event_spec.discriminator_hex.to_string()),
None => discriminator_hex.clone(),
};
if decoded_instruction_ids.contains(&instruction_id) && anchor_event_spec.is_none() {
if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() {
let cleanup_result = self
.delete_replaced_instruction_audit_by_discriminator_hex(
transaction_id,
audit_spec.protocol_name,
discriminator_hex,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
}
continue;
}
if anchor_event_spec.is_none()
&& raydium_instruction_already_decoded_by_discriminator(
&decoded_discriminator_keys,
audit_spec.protocol_name,
dedupe_discriminator_hex.as_deref(),
)
{
if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() {
let cleanup_result = self
.delete_replaced_instruction_audit_by_discriminator_hex(
transaction_id,
audit_spec.protocol_name,
discriminator_hex,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
}
continue;
}
let mapped_spec = if anchor_event_spec.is_some() {
None
} else {
raydium_mapped_non_trade_instruction_spec(
audit_spec.protocol_name,
discriminator_hex.as_deref(),
accounts.len(),
)
};
if let Some(mapped_spec) = mapped_spec {
if raydium_mapped_event_kind_already_decoded(
decoded_events.as_slice(),
audit_spec.protocol_name,
mapped_spec.event_kind,
) {
if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() {
let cleanup_result = self
.delete_replaced_instruction_audit_by_discriminator_hex(
transaction_id,
audit_spec.protocol_name,
discriminator_hex,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
}
continue;
}
}
let event_kind = match anchor_event_spec {
Some(anchor_event_spec) => anchor_event_spec.event_kind,
None => match mapped_spec {
Some(mapped_spec) => mapped_spec.event_kind,
None => audit_spec.event_kind,
},
};
let mut payload = build_raydium_instruction_audit_payload(
transaction,
instruction,
audit_spec.protocol_name,
event_kind,
program_id.as_str(),
);
if let Some(anchor_event_spec) = anchor_event_spec {
payload = enrich_raydium_launchpad_anchor_self_cpi_payload(
payload,
anchor_event_spec,
data_bytes.as_deref(),
);
}
if let Some(mapped_spec) = mapped_spec {
payload = enrich_raydium_mapped_non_trade_payload(
payload,
mapped_spec,
data_base58.as_deref(),
);
}
let pool_account = match anchor_event_spec {
Some(anchor_event_spec) => raydium_launchpad_anchor_self_cpi_pool_account(
anchor_event_spec,
data_bytes.as_deref(),
),
None => candidate_raydium_mapped_pool_account(
mapped_spec,
accounts.as_slice(),
audit_spec.protocol_name,
instruction.accounts_json.as_str(),
),
};
let token_a_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| return spec.token_a_mint_index),
accounts.as_slice(),
);
let token_b_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| return spec.token_b_mint_index),
accounts.as_slice(),
);
let lp_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| return spec.lp_mint_index),
accounts.as_slice(),
);
let persist_result = self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
audit_spec.protocol_name,
program_id.clone(),
event_kind,
pool_account,
None,
token_a_mint,
token_b_mint,
lp_mint,
payload,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
if let Some(anchor_event_spec) = anchor_event_spec {
let cleanup_result = self
.delete_replaced_raydium_launchpad_anchor_self_cpi_audit(
transaction_id,
instruction_id,
anchor_event_spec.discriminator_hex,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
}
if anchor_event_spec.is_none() {
if let Some(discriminator_hex) = dedupe_discriminator_hex.as_deref() {
let cleanup_result = self
.delete_replaced_instruction_audit_by_discriminator_hex(
transaction_id,
audit_spec.protocol_name,
discriminator_hex,
)
.await;
if let Err(error) = cleanup_result {
return Err(error);
}
}
}
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn preserve_unmatched_meteora_instruction_audits(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let decoded_events = match decoded_events_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut decoded_instruction_ids = std::collections::HashSet::<i64>::new();
for decoded_event in &decoded_events {
if !decoded_event.protocol_name.starts_with("meteora_") {
continue;
}
if decoded_event.event_kind.ends_with(".instruction_audit") {
continue;
}
let instruction_id = match decoded_event.instruction_id {
Some(instruction_id) => instruction_id,
None => continue,
};
decoded_instruction_ids.insert(instruction_id);
}
let mut persisted = std::vec::Vec::new();
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
let audit_spec = match meteora_instruction_audit_spec(program_id.as_str()) {
Some(audit_spec) => audit_spec,
None => continue,
};
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
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,
instruction,
audit_spec.protocol_name,
audit_spec.event_kind,
program_id.as_str(),
);
let pool_account =
candidate_meteora_audit_pool_account(audit_spec, accounts.as_slice());
let persist_result = self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
audit_spec.protocol_name,
program_id.clone(),
audit_spec.event_kind,
pool_account,
None,
None,
None,
None,
payload,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_pump_fun_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.pump_fun_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_pump_fun_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_pump_swap_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.pump_swap_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_pump_swap_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_meteora_dbc_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.meteora_dbc_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_meteora_dbc_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_meteora_dlmm_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.meteora_dlmm_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_meteora_dlmm_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_meteora_damm_v1_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.meteora_damm_v1_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result =
self.persist_meteora_damm_v1_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_meteora_damm_v2_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.meteora_damm_v2_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result =
self.persist_meteora_damm_v2_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_orca_whirlpools_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result =
self.orca_whirlpools_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result =
self.persist_orca_whirlpools_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn persist_openbook_v2_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::OpenBookV2DecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::OpenBookV2DecodedEvent::Audit(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"openbook_v2",
event.program_id.clone(),
event.event_kind.as_str(),
None,
event.market_account.clone(),
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn decode_and_persist_openbook_v2_audit_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.openbook_v2_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_openbook_v2_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn persist_phoenix_v1_event(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::PhoenixV1DecodedEvent,
) -> Result<crate::DexDecodedEventDto, crate::Error> {
match decoded_event {
crate::PhoenixV1DecodedEvent::Audit(event) => {
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"phoenix_v1",
event.program_id.clone(),
event.event_kind.as_str(),
None,
event.market_account.clone(),
event.token_a_mint.clone(),
event.token_b_mint.clone(),
None,
event.payload_json.clone(),
)
.await;
},
}
}
async fn decode_and_persist_phoenix_v1_audit_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.phoenix_v1_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_phoenix_v1_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_fluxbeam_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.fluxbeam_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_fluxbeam_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_dexlab_events(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let decoded_result = self.dexlab_decoder.decode_transaction(transaction, instructions);
let decoded_events = match decoded_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
let persist_result = self.persist_dexlab_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
}
struct RaydiumInstructionAuditSpec {
protocol_name: &'static str,
event_kind: &'static str,
candidate_pool_account_index: usize,
}
#[derive(Clone, Copy)]
struct MeteoraInstructionAuditSpec {
protocol_name: &'static str,
event_kind: &'static str,
candidate_pool_account_index: std::option::Option<usize>,
}
#[derive(Clone, Copy)]
struct RaydiumMappedNonTradeInstructionSpec {
instruction_name: &'static str,
event_kind: &'static str,
pool_account_index: std::option::Option<usize>,
token_a_mint_index: std::option::Option<usize>,
token_b_mint_index: std::option::Option<usize>,
lp_mint_index: std::option::Option<usize>,
amount_layout: RaydiumMappedNonTradeAmountLayout,
}
#[derive(Clone, Copy)]
enum RaydiumMappedNonTradeAmountLayout {
None,
ClmmCreatePool,
ClmmFeePair,
ClmmLiquidityV2,
ClmmOpenLimitOrder,
ClmmIncreaseLimitOrder,
ClmmDecreaseLimitOrder,
AnchorIdl,
CpmmAmmConfig,
CpmmDeposit,
CpmmFeePair,
CpmmInitialize,
CpmmPoolStatus,
CpmmWithdraw,
LaunchpadInitialize,
AmmV4Initialize,
AmmV4Initialize2,
AmmV4MonitorStep,
AmmV4Deposit,
AmmV4Withdraw,
AmmV4SetParams,
AmmV4WithdrawSrm,
AmmV4PreInitialize,
AmmV4SimulateInfo,
AmmV4AdminCancelOrders,
AmmV4UpdateConfigAccount,
}
fn raydium_instruction_audit_spec(
program_id: &str,
) -> std::option::Option<RaydiumInstructionAuditSpec> {
if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID {
return Some(RaydiumInstructionAuditSpec {
protocol_name: "raydium_amm_v4",
event_kind: "raydium_amm_v4.instruction_audit",
candidate_pool_account_index: 1,
});
}
if program_id == crate::RAYDIUM_CLMM_PROGRAM_ID {
return Some(RaydiumInstructionAuditSpec {
protocol_name: "raydium_clmm",
event_kind: "raydium_clmm.instruction_audit",
candidate_pool_account_index: 2,
});
}
if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID {
return Some(RaydiumInstructionAuditSpec {
protocol_name: "raydium_cpmm",
event_kind: "raydium_cpmm.instruction_audit",
candidate_pool_account_index: 3,
});
}
if program_id == crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID {
return Some(RaydiumInstructionAuditSpec {
protocol_name: "raydium_stable_swap",
event_kind: "raydium_stable_swap.instruction_audit",
candidate_pool_account_index: 1,
});
}
if program_id == crate::RAYDIUM_LAUNCHPAD_PROGRAM_ID {
return Some(RaydiumInstructionAuditSpec {
protocol_name: "raydium_launchpad",
event_kind: "raydium_launchpad.instruction_audit",
candidate_pool_account_index: 4,
});
}
return None;
}
fn raydium_mapped_non_trade_instruction_spec(
protocol_name: &str,
discriminator_hex: std::option::Option<&str>,
account_count: usize,
) -> std::option::Option<RaydiumMappedNonTradeInstructionSpec> {
let discriminator_hex = match discriminator_hex {
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
if protocol_name == "raydium_launchpad" {
return raydium_launchpad_mapped_non_trade_instruction_spec(
discriminator_hex,
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 {
instruction_name: "cpi_event",
event_kind: "raydium_clmm.cpi_event",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "4c7c800fd55725fa" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "close_limit_order",
event_kind: "raydium_clmm.close_limit_order",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "9d20dab7471d1293" && account_count >= 11 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "open_limit_order",
event_kind: "raydium_clmm.open_limit_order",
pool_account_index: Some(1),
token_a_mint_index: Some(7),
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmOpenLimitOrder,
});
}
if discriminator_hex == "b19059ecfaba7d63" && account_count >= 8 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "increase_limit_order",
event_kind: "raydium_clmm.increase_limit_order",
pool_account_index: Some(1),
token_a_mint_index: Some(6),
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmIncreaseLimitOrder,
});
}
if discriminator_hex == "759d3c674231a300" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "decrease_limit_order",
event_kind: "raydium_clmm.decrease_limit_order",
pool_account_index: Some(1),
token_a_mint_index: Some(8),
token_b_mint_index: Some(9),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmDecreaseLimitOrder,
});
}
if discriminator_hex == "c975989055556cb2" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "close_protocol_position",
event_kind: "raydium_clmm.close_protocol_position",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 7 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_fund_fee",
event_kind: "raydium_clmm.collect_fund_fee",
pool_account_index: Some(1),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmFeePair,
});
}
if discriminator_hex == "8888fcddc2427e59" && account_count >= 7 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_protocol_fee",
event_kind: "raydium_clmm.collect_protocol_fee",
pool_account_index: Some(1),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmFeePair,
});
}
if discriminator_hex == "e992d18ecf6840bc" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_pool",
event_kind: "raydium_clmm.create_pool",
pool_account_index: Some(2),
token_a_mint_index: Some(3),
token_b_mint_index: Some(4),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmCreatePool,
});
}
if discriminator_hex == "12eda6c52210d590" && account_count >= 5 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_remaining_rewards",
event_kind: "raydium_clmm.collect_remaining_rewards",
pool_account_index: Some(2),
token_a_mint_index: Some(4),
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_amm_config",
event_kind: "raydium_clmm.create_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "2b44d4a7592fa401" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_customizable_pool",
event_kind: "raydium_clmm.create_customizable_pool",
pool_account_index: Some(2),
token_a_mint_index: Some(3),
token_b_mint_index: Some(4),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "bd0eb5785576e33e" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_dynamic_fee_config",
event_kind: "raydium_clmm.create_dynamic_fee_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "3f5794216d230868" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_operation_account",
event_kind: "raydium_clmm.create_operation_account",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "11fb415c88f20ea9" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_support_mint_associated",
event_kind: "raydium_clmm.create_support_mint_associated",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a026d06f685b2c01" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "decrease_liquidity",
event_kind: "raydium_clmm.decrease_liquidity",
pool_account_index: Some(3),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(1),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "2e9cf3760dcdfbb2" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "increase_liquidity",
event_kind: "raydium_clmm.increase_liquidity",
pool_account_index: Some(2),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(1),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "5f87c0c4f281e644" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize_reward",
event_kind: "raydium_clmm.initialize_reward",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "87802f4d0f98f031" && account_count >= 6 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "open_position",
event_kind: "raydium_clmm.open_position",
pool_account_index: Some(5),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(2),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "4db84ad67056f1c7" && account_count >= 6 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "open_position_v2",
event_kind: "raydium_clmm.open_position_v2",
pool_account_index: Some(5),
token_a_mint_index: if account_count >= 22 { Some(20) } else { None },
token_b_mint_index: if account_count >= 22 { Some(21) } else { None },
lp_mint_index: Some(2),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "7034a74b20c9d389" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "set_reward_params",
event_kind: "raydium_clmm.set_reward_params",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "cd4e74215c691a60" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "settle_limit_order",
event_kind: "raydium_clmm.settle_limit_order",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "457d73daf5baf2c4" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "swap_router_base_in",
event_kind: "raydium_clmm.swap_router_base_in",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "07160c53f22b3079" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "transfer_reward_owner",
event_kind: "raydium_clmm.transfer_reward_owner",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "313cae889a1c74c8" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_amm_config",
event_kind: "raydium_clmm.update_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "0707500802c784f0" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_dynamic_fee_config",
event_kind: "raydium_clmm.update_dynamic_fee_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "7f467728bce33d07" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_operation_account",
event_kind: "raydium_clmm.update_operation_account",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "82576c062ee0757b" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_pool_status",
event_kind: "raydium_clmm.update_pool_status",
pool_account_index: Some(0),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a3ace0340b9a6adf" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_reward_infos",
event_kind: "raydium_clmm.update_reward_infos",
pool_account_index: Some(0),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "3a7fbc3e4f52c460" && account_count >= 16 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "decrease_liquidity_v2",
event_kind: "raydium_clmm.decrease_liquidity_v2",
pool_account_index: Some(3),
token_a_mint_index: Some(14),
token_b_mint_index: Some(15),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2,
});
}
if discriminator_hex == "851d59df45eeb00a" && account_count >= 15 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "increase_liquidity_v2",
event_kind: "raydium_clmm.increase_liquidity_v2",
pool_account_index: Some(2),
token_a_mint_index: Some(13),
token_b_mint_index: Some(14),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2,
});
}
if discriminator_hex == "4dffae527d1dc92e" && account_count >= 20 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "open_position_with_token22_nft",
event_kind: "raydium_clmm.open_position_with_token22_nft",
pool_account_index: Some(4),
token_a_mint_index: Some(18),
token_b_mint_index: Some(19),
lp_mint_index: Some(2),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "7b86510031446262" && account_count >= 6 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "close_position",
event_kind: "raydium_clmm.close_position",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: Some(1),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
}
if protocol_name == "raydium_cpmm" {
if discriminator_hex == "40f4bc78a7e9690a" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "anchor_idl_instruction",
event_kind: "raydium_cpmm.anchor_idl_instruction",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::AnchorIdl,
});
}
if discriminator_hex == "e445a52e51cb9a1d" {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "cpi_event",
event_kind: "raydium_cpmm.cpi_event",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "close_permission_pda",
event_kind: "raydium_cpmm.close_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_fund_fee",
event_kind: "raydium_cpmm.collect_fund_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_protocol_fee",
event_kind: "raydium_cpmm.collect_protocol_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_amm_config",
event_kind: "raydium_cpmm.create_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_permission_pda",
event_kind: "raydium_cpmm.create_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "deposit",
event_kind: "raydium_cpmm.deposit",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit,
});
}
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize",
event_kind: "raydium_cpmm.initialize",
pool_account_index: Some(3),
token_a_mint_index: Some(4),
token_b_mint_index: Some(5),
lp_mint_index: Some(6),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize_with_permission",
event_kind: "raydium_cpmm.initialize_with_permission",
pool_account_index: Some(4),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: Some(7),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_amm_config",
event_kind: "raydium_cpmm.update_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "82576c062ee0757b" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_pool_status",
event_kind: "raydium_cpmm.update_pool_status",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
});
}
}
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,
) -> std::option::Option<RaydiumMappedNonTradeInstructionSpec> {
let layout =
match crate::dex::raydium_launchpad::account_layout(discriminator_hex, account_count) {
Some(layout) => layout,
None => return None,
};
let amount_layout = if layout.creates_pool {
RaydiumMappedNonTradeAmountLayout::LaunchpadInitialize
} else {
RaydiumMappedNonTradeAmountLayout::None
};
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: layout.instruction_name,
event_kind: layout.event_kind,
pool_account_index: layout.pool_account_index,
token_a_mint_index: layout.base_mint_index,
token_b_mint_index: layout.quote_mint_index,
lp_mint_index: None,
amount_layout,
});
}
fn candidate_raydium_mapped_pool_account(
mapped_spec: std::option::Option<RaydiumMappedNonTradeInstructionSpec>,
accounts: &[std::string::String],
protocol_name: &str,
accounts_json: &str,
) -> std::option::Option<std::string::String> {
if let Some(mapped_spec) = mapped_spec {
if let Some(pool_account_index) = mapped_spec.pool_account_index {
return candidate_raydium_mapped_account(Some(pool_account_index), accounts);
}
return None;
}
return candidate_raydium_audit_pool_account(protocol_name, accounts_json);
}
fn candidate_raydium_mapped_account(
index: std::option::Option<usize>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let index = match index {
Some(index) => index,
None => return None,
};
return accounts.get(index).cloned();
}
fn enrich_raydium_mapped_non_trade_payload(
payload: serde_json::Value,
mapped_spec: RaydiumMappedNonTradeInstructionSpec,
data_base58: std::option::Option<&str>,
) -> serde_json::Value {
let mut object = match payload {
serde_json::Value::Object(object) => object,
other => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
};
object.remove("tradeCandidate");
object.remove("candleCandidate");
object.remove("nonTradeUseful");
object.remove("skipTradeReason");
object.remove("skipCandleReason");
object.insert(
"instructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert(
"upstreamInstructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
object.insert(
"adminAction".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
object.insert(
"auditReason".to_string(),
serde_json::Value::String("raydium_non_swap_instruction_mapped_from_corpus".to_string()),
);
object.insert(
"proofSource".to_string(),
serde_json::Value::String(
"local_corpus_discriminator_and_raydium_idl_instruction_name".to_string(),
),
);
let data_bytes = instruction_data_bytes_from_base58(data_base58);
if let Some(data_bytes) = data_bytes {
insert_raydium_mapped_amounts(
&mut object,
mapped_spec.amount_layout,
data_bytes.as_slice(),
);
}
return serde_json::Value::Object(object);
}
fn insert_raydium_mapped_amounts(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
amount_layout: RaydiumMappedNonTradeAmountLayout,
data: &[u8],
) {
match amount_layout {
RaydiumMappedNonTradeAmountLayout::None => return,
RaydiumMappedNonTradeAmountLayout::AnchorIdl => {
let payload_len = data.len();
object.insert("idlManagementInstruction".to_string(), serde_json::Value::Bool(true));
object.insert(
"instructionDataLength".to_string(),
serde_json::Value::Number(serde_json::Number::from(payload_len as u64)),
);
if payload_len >= 8 {
object.insert(
"idlManagementDiscriminatorHex".to_string(),
serde_json::Value::String("40f4bc78a7e9690a".to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmCreatePool => {
if let Some(sqrt_price_x64) = read_u128_le_from_bytes(data, 8) {
object.insert(
"sqrtPriceX64".to_string(),
serde_json::Value::String(sqrt_price_x64.to_string()),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
object.insert(
"openTime".to_string(),
serde_json::Value::String(open_time.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmFeePair => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
object.insert(
"amount0RequestedRaw".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
object.insert(
"amount1RequestedRaw".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2 => {
if let Some(liquidity) = read_u128_le_from_bytes(data, 8) {
object.insert(
"liquidity".to_string(),
serde_json::Value::String(liquidity.to_string()),
);
object.insert(
"lpAmountRaw".to_string(),
serde_json::Value::String(liquidity.to_string()),
);
}
if let Some(amount_0) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 32) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmOpenLimitOrder => {
if let Some(nonce_index) = read_u8_from_bytes(data, 8) {
object.insert(
"nonceIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(nonce_index as u64)),
);
}
if let Some(zero_for_one) = read_u8_from_bytes(data, 9) {
object.insert("zeroForOne".to_string(), serde_json::Value::Bool(zero_for_one != 0));
}
if let Some(tick_index) = read_i32_le_from_bytes(data, 10) {
object.insert(
"tickIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(tick_index as i64)),
);
}
if let Some(amount) = read_u64_le_from_bytes(data, 14) {
object
.insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string()));
object.insert(
"orderAmountRaw".to_string(),
serde_json::Value::String(amount.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmIncreaseLimitOrder => {
if let Some(amount) = read_u64_le_from_bytes(data, 8) {
object
.insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string()));
object.insert(
"increasedAmountRaw".to_string(),
serde_json::Value::String(amount.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::ClmmDecreaseLimitOrder => {
if let Some(amount) = read_u64_le_from_bytes(data, 8) {
object
.insert("amountRaw".to_string(), serde_json::Value::String(amount.to_string()));
object.insert(
"decreasedAmountRaw".to_string(),
serde_json::Value::String(amount.to_string()),
);
}
if let Some(amount_min) = read_u64_le_from_bytes(data, 16) {
object.insert(
"amountMinRaw".to_string(),
serde_json::Value::String(amount_min.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => {
if let Some(param) = read_u8_from_bytes(data, 8) {
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, 9) {
object.insert(
"configValue".to_string(),
serde_json::Value::String(value.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmDeposit => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
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(amount_0) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmFeePair => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
object.insert(
"amount0RequestedRaw".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
object.insert(
"amount1RequestedRaw".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmInitialize => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
object.insert(
"openTime".to_string(),
serde_json::Value::String(open_time.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => {
if let Some(status) = read_u8_from_bytes(data, 8) {
object.insert(
"poolStatus".to_string(),
serde_json::Value::Number(serde_json::Number::from(status as u64)),
);
}
},
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(),
serde_json::Value::String("bonding_curve".to_string()),
);
object.insert(
"poolStatusHint".to_string(),
serde_json::Value::String("pending".to_string()),
);
},
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
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(amount_0) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
}
}
fn instruction_data_bytes_from_base58(
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 read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
if data.len() < offset + 1 {
return None;
}
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;
}
let mut bytes = [0_u8; 4];
let mut index = 0_usize;
while index < 4 {
bytes[index] = data[offset + index];
index += 1;
}
return Some(i32::from_le_bytes(bytes));
}
fn read_pubkey_base58_from_bytes(
data: std::option::Option<&[u8]>,
offset: usize,
) -> std::option::Option<std::string::String> {
let data = match data {
Some(data) => data,
None => return None,
};
let end = offset.checked_add(32_usize);
let end = match end {
Some(end) => end,
None => return None,
};
if data.len() < end {
return None;
}
return Some(bs58::encode(&data[offset..end]).into_string());
}
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 {
return None;
}
let mut bytes = [0_u8; 8];
let mut index = 0_usize;
while index < 8 {
bytes[index] = data[offset + index];
index += 1;
}
return Some(u64::from_le_bytes(bytes));
}
fn read_u128_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u128> {
if data.len() < offset + 16 {
return None;
}
let mut bytes = [0_u8; 16];
let mut index = 0_usize;
while index < 16 {
bytes[index] = data[offset + index];
index += 1;
}
return Some(u128::from_le_bytes(bytes));
}
fn meteora_instruction_audit_spec(
program_id: &str,
) -> std::option::Option<MeteoraInstructionAuditSpec> {
if program_id == crate::METEORA_DBC_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_dbc",
event_kind: "meteora_dbc.instruction_audit",
candidate_pool_account_index: Some(1),
});
}
if program_id == crate::METEORA_DLMM_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_dlmm",
event_kind: "meteora_dlmm.instruction_audit",
candidate_pool_account_index: Some(0),
});
}
if program_id == crate::METEORA_DAMM_V1_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_damm_v1",
event_kind: "meteora_damm_v1.instruction_audit",
candidate_pool_account_index: Some(0),
});
}
if program_id == crate::METEORA_DAMM_V2_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_damm_v2",
event_kind: "meteora_damm_v2.instruction_audit",
candidate_pool_account_index: Some(1),
});
}
return None;
}
fn candidate_meteora_audit_pool_account(
audit_spec: MeteoraInstructionAuditSpec,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let index = match audit_spec.candidate_pool_account_index {
Some(index) => index,
None => return None,
};
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,
protocol_name: &str,
event_kind: &str,
program_id: &str,
) -> serde_json::Value {
let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str());
let account_count = match accounts.as_array() {
Some(items) => items.len(),
None => 0,
};
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 = 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 {
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,
"signature": transaction.signature.clone(),
"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,
"programFamily": "meteora",
"accounts": accounts,
"accountCount": account_count,
"data": data_base58,
"dataPrefix": data_prefix,
"discriminatorHex": discriminator_hex,
"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,
"skipTradeReason": "instruction_audit_only",
"skipCandleReason": "instruction_audit_only"
});
}
fn instruction_discriminator_hex_from_payload(
payload_json: &serde_json::Value,
) -> std::option::Option<std::string::String> {
let candidates = [
"instructionDiscriminatorHex",
"instruction_discriminator_hex",
"discriminatorHex",
"discriminator_hex",
"anchorEventDiscriminatorHex",
"anchor_event_discriminator_hex",
];
for candidate in candidates {
let value = payload_json.get(candidate).and_then(serde_json::Value::as_str);
let value = match value {
Some(value) => value.trim(),
None => continue,
};
if !value.is_empty() {
return Some(value.to_string());
}
}
return None;
}
fn instruction_discriminator_hex_from_payload_str(
payload_json: &str,
) -> std::option::Option<std::string::String> {
let parsed = serde_json::from_str::<serde_json::Value>(payload_json);
let parsed = match parsed {
Ok(parsed) => parsed,
Err(_) => return None,
};
return instruction_discriminator_hex_from_payload(&parsed);
}
fn raydium_decoded_discriminator_key(
protocol_name: &str,
discriminator_hex: &str,
) -> std::string::String {
return format!("{}:{}", protocol_name, discriminator_hex);
}
fn raydium_instruction_already_decoded_by_discriminator(
decoded_discriminator_keys: &std::collections::HashSet<std::string::String>,
protocol_name: &str,
discriminator_hex: std::option::Option<&str>,
) -> bool {
let discriminator_hex = match discriminator_hex {
Some(discriminator_hex) => discriminator_hex,
None => return false,
};
let key = raydium_decoded_discriminator_key(protocol_name, discriminator_hex);
return decoded_discriminator_keys.contains(&key);
}
fn raydium_mapped_event_kind_already_decoded(
decoded_events: &[crate::DexDecodedEventDto],
protocol_name: &str,
event_kind: &str,
) -> bool {
for decoded_event in decoded_events {
if decoded_event.protocol_name != protocol_name {
continue;
}
if decoded_event.event_kind == event_kind {
return true;
}
}
return false;
}
fn should_immediately_materialize_decoded_non_trade_event(event_kind: &str) -> bool {
if event_kind == "raydium_clmm.create_pool" {
return true;
}
if event_kind == "raydium_clmm.collect_protocol_fee" {
return true;
}
return false;
}
fn dex_decode_transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {
Some(err_json) => err_json.trim(),
None => return false,
};
if err_json.is_empty() {
return false;
}
if err_json == "null" {
return false;
}
return true;
}
fn dex_decode_payload_value(payload_json: &str) -> serde_json::Value {
let parsed = serde_json::from_str::<serde_json::Value>(payload_json);
match parsed {
Ok(parsed) => return parsed,
Err(_) => return serde_json::Value::Object(serde_json::Map::new()),
}
}
fn dex_decode_extract_first_amount_string(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
let text = dex_decode_extract_first_string(value, candidate_keys);
if text.is_some() {
return text;
}
return dex_decode_extract_first_number_as_string(value, candidate_keys);
}
fn dex_decode_extract_first_string(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
let candidate_value = match candidate_value {
Some(candidate_value) => candidate_value,
None => continue,
};
if let Some(text) = candidate_value.as_str() {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
for nested_value in object.values() {
let nested = dex_decode_extract_first_string(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
return None;
}
if let Some(array) = value.as_array() {
for nested_value in array {
let nested = dex_decode_extract_first_string(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn dex_decode_extract_first_number_as_string(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
let candidate_value = match candidate_value {
Some(candidate_value) => candidate_value,
None => continue,
};
if let Some(number) = candidate_value.as_i64() {
return Some(number.to_string());
}
if let Some(number) = candidate_value.as_u64() {
return Some(number.to_string());
}
if let Some(number) = candidate_value.as_f64() {
return Some(number.to_string());
}
}
for nested_value in object.values() {
let nested = dex_decode_extract_first_number_as_string(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
return None;
}
if let Some(array) = value.as_array() {
for nested_value in array {
let nested = dex_decode_extract_first_number_as_string(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
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"),
"raydium_stable_swap" => return Some("raydium_stable_swap.instruction_audit"),
"raydium_launchpad" => return Some("raydium_launchpad.instruction_audit"),
"meteora_dlmm" => return Some("meteora_dlmm.instruction_audit"),
"meteora_damm_v1" => return Some("meteora_damm_v1.instruction_audit"),
"meteora_damm_v2" => return Some("meteora_damm_v2.instruction_audit"),
"meteora_dbc" => return Some("meteora_dbc.instruction_audit"),
_ => return None,
}
}
#[derive(Clone, Copy)]
struct RaydiumAnchorSelfCpiEventSpec {
entry_name: &'static str,
event_kind: &'static str,
event_family: &'static str,
discriminator_hex: &'static str,
}
fn raydium_launchpad_anchor_self_cpi_event_spec(
protocol_name: &str,
data_bytes: std::option::Option<&[u8]>,
) -> std::option::Option<RaydiumAnchorSelfCpiEventSpec> {
if protocol_name != "raydium_launchpad" {
return None;
}
let selector_hex = discriminator_hex_from_bytes(data_bytes, 0);
if selector_hex.as_deref() != Some(METEORA_ANCHOR_SELF_CPI_LOG_SELECTOR_HEX) {
return None;
}
let event_discriminator_hex = discriminator_hex_from_bytes(data_bytes, 8);
let event_discriminator_hex = match event_discriminator_hex.as_deref() {
Some(event_discriminator_hex) => event_discriminator_hex,
None => return None,
};
match event_discriminator_hex {
"bddb7fd34ee661ee" => {
return Some(RaydiumAnchorSelfCpiEventSpec {
entry_name: "trade_event",
event_kind: "raydium_launchpad.trade_event",
event_family: "swap",
discriminator_hex: "bddb7fd34ee661ee",
});
},
"97d7e20976a173ae" => {
return Some(RaydiumAnchorSelfCpiEventSpec {
entry_name: "pool_create_event",
event_kind: "raydium_launchpad.pool_create_event",
event_family: "pool_create",
discriminator_hex: "97d7e20976a173ae",
});
},
"15c2725778d3e220" => {
return Some(RaydiumAnchorSelfCpiEventSpec {
entry_name: "claim_vested_event",
event_kind: "raydium_launchpad.claim_vested_event",
event_family: "vesting",
discriminator_hex: "15c2725778d3e220",
});
},
"96980bb334d2bf7d" => {
return Some(RaydiumAnchorSelfCpiEventSpec {
entry_name: "create_vesting_event",
event_kind: "raydium_launchpad.create_vesting_event",
event_family: "vesting",
discriminator_hex: "96980bb334d2bf7d",
});
},
_ => return None,
}
}
fn raydium_launchpad_anchor_self_cpi_pool_account(
spec: RaydiumAnchorSelfCpiEventSpec,
data_bytes: std::option::Option<&[u8]>,
) -> std::option::Option<std::string::String> {
match spec.entry_name {
"trade_event" => return read_pubkey_base58_from_bytes(data_bytes, 16),
"pool_create_event" => return read_pubkey_base58_from_bytes(data_bytes, 16),
_ => return None,
}
}
fn enrich_raydium_launchpad_anchor_self_cpi_payload(
payload: serde_json::Value,
spec: RaydiumAnchorSelfCpiEventSpec,
data_bytes: std::option::Option<&[u8]>,
) -> serde_json::Value {
let mut object = match payload {
serde_json::Value::Object(object) => object,
other => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
};
object.insert(
"entryKind".to_string(),
serde_json::Value::String(crate::ENTRY_KIND_EVENT.to_string()),
);
object.insert("entryName".to_string(), serde_json::Value::String(spec.entry_name.to_string()));
object.insert(
"eventFamily".to_string(),
serde_json::Value::String(spec.event_family.to_string()),
);
object.insert("eventName".to_string(), serde_json::Value::String(spec.entry_name.to_string()));
object.insert(
"upstreamEventName".to_string(),
serde_json::Value::String(spec.entry_name.to_string()),
);
object.insert(
"upstreamEntryName".to_string(),
serde_json::Value::String(spec.entry_name.to_string()),
);
object.insert(
"upstreamEntryKind".to_string(),
serde_json::Value::String(crate::ENTRY_KIND_EVENT.to_string()),
);
object.insert(
"upstreamDiscriminatorHex".to_string(),
serde_json::Value::String(spec.discriminator_hex.to_string()),
);
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
object.insert("decodedFromAnchorSelfCpiLog".to_string(), serde_json::Value::Bool(true));
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(false));
object.insert(
"auditReason".to_string(),
serde_json::Value::String("raydium_launchpad_anchor_self_cpi_event_decoded".to_string()),
);
object.insert(
"proofSource".to_string(),
serde_json::Value::String(
"local_corpus_anchor_self_cpi_and_raydium_launchpad_event_discriminator".to_string(),
),
);
if let Some(data_bytes) = data_bytes {
object.insert(
"anchorSelfCpiDataLength".to_string(),
serde_json::Value::Number(serde_json::Number::from(data_bytes.len() as u64)),
);
if data_bytes.len() >= 16 {
object.insert(
"anchorEventPayloadSize".to_string(),
serde_json::Value::Number(serde_json::Number::from((data_bytes.len() - 16) as u64)),
);
}
}
if let Some(pool_state) = raydium_launchpad_anchor_self_cpi_pool_account(spec, data_bytes) {
object.insert("poolState".to_string(), serde_json::Value::String(pool_state.clone()));
object.insert("poolAccount".to_string(), serde_json::Value::String(pool_state));
}
if spec.entry_name == "trade_event" {
insert_raydium_launchpad_trade_event_amounts(&mut object, data_bytes);
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(true));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(true));
object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(false));
object.remove("skipTradeReason");
object.remove("skipCandleReason");
} else if spec.entry_name == "pool_create_event" {
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(true));
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("pool_lifecycle_event".to_string()),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String("pool_lifecycle_event".to_string()),
);
} else {
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(false));
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("launchpad_event_audit_only".to_string()),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String("launchpad_event_audit_only".to_string()),
);
}
return serde_json::Value::Object(object);
}
fn insert_raydium_launchpad_trade_event_amounts(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
data_bytes: std::option::Option<&[u8]>,
) {
let data = match data_bytes {
Some(data) => data,
None => return,
};
let fields = [
("totalBaseSellRaw", 48_usize),
("virtualBaseRaw", 56_usize),
("virtualQuoteRaw", 64_usize),
("realBaseBeforeRaw", 72_usize),
("realQuoteBeforeRaw", 80_usize),
("realBaseAfterRaw", 88_usize),
("realQuoteAfterRaw", 96_usize),
("amountInRaw", 104_usize),
("amountOutRaw", 112_usize),
("protocolFeeRaw", 120_usize),
("platformFeeRaw", 128_usize),
("creatorFeeRaw", 136_usize),
("shareFeeRaw", 144_usize),
];
for field in fields {
if let Some(value) = read_u64_le_from_bytes(data, field.1) {
object.insert(field.0.to_string(), serde_json::Value::String(value.to_string()));
}
}
if let Some(direction) = read_u8_from_bytes(data, 152) {
object.insert(
"tradeDirectionRaw".to_string(),
serde_json::Value::Number(serde_json::Number::from(direction as u64)),
);
}
if let Some(pool_status) = read_u8_from_bytes(data, 153) {
object.insert(
"poolStatusRaw".to_string(),
serde_json::Value::Number(serde_json::Number::from(pool_status as u64)),
);
}
if let Some(exact_in) = read_u8_from_bytes(data, 154) {
object.insert("exactIn".to_string(), serde_json::Value::Bool(exact_in != 0));
}
insert_raydium_launchpad_normalized_trade_amounts(object);
}
fn insert_raydium_launchpad_normalized_trade_amounts(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
) {
let direction = object.get("tradeDirectionRaw").and_then(serde_json::Value::as_u64);
let amount_in = object
.get("amountInRaw")
.and_then(serde_json::Value::as_str)
.map(str::to_string);
let amount_out = object
.get("amountOutRaw")
.and_then(serde_json::Value::as_str)
.map(str::to_string);
match direction {
Some(0) => {
object
.insert("tradeSide".to_string(), serde_json::Value::String("BuyBase".to_string()));
if let Some(amount_out) = amount_out {
object.insert("baseAmountRaw".to_string(), serde_json::Value::String(amount_out));
}
if let Some(amount_in) = amount_in {
object.insert("quoteAmountRaw".to_string(), serde_json::Value::String(amount_in));
}
},
Some(1) => {
object
.insert("tradeSide".to_string(), serde_json::Value::String("SellBase".to_string()));
if let Some(amount_in) = amount_in {
object.insert("baseAmountRaw".to_string(), serde_json::Value::String(amount_in));
}
if let Some(amount_out) = amount_out {
object.insert("quoteAmountRaw".to_string(), serde_json::Value::String(amount_out));
}
},
_ => {},
}
}
fn build_raydium_instruction_audit_payload(
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
protocol_name: &str,
event_kind: &str,
program_id: &str,
) -> serde_json::Value {
let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str());
let account_count = match accounts.as_array() {
Some(items) => items.len(),
None => 0,
};
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 = 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 {
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 audit_reason = if anchor_self_cpi_log {
"raydium_anchor_self_cpi_log_not_decoded_by_specific_event_decoder"
} else {
"raydium_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,
"signature": transaction.signature.clone(),
"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,
"accountCount": account_count,
"data": data_base58,
"discriminatorHex": discriminator_hex,
"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,
"skipTradeReason": "instruction_audit_only",
"skipCandleReason": "instruction_audit_only"
});
}
fn candidate_raydium_audit_pool_account(
protocol_name: &str,
accounts_json: &str,
) -> std::option::Option<std::string::String> {
let spec = match protocol_name {
"raydium_amm_v4" => RaydiumInstructionAuditSpec {
protocol_name: "raydium_amm_v4",
event_kind: "raydium_amm_v4.instruction_audit",
candidate_pool_account_index: 1,
},
"raydium_clmm" => RaydiumInstructionAuditSpec {
protocol_name: "raydium_clmm",
event_kind: "raydium_clmm.instruction_audit",
candidate_pool_account_index: 2,
},
"raydium_cpmm" => RaydiumInstructionAuditSpec {
protocol_name: "raydium_cpmm",
event_kind: "raydium_cpmm.instruction_audit",
candidate_pool_account_index: 3,
},
"raydium_launchpad" => RaydiumInstructionAuditSpec {
protocol_name: "raydium_launchpad",
event_kind: "raydium_launchpad.instruction_audit",
candidate_pool_account_index: 4,
},
_ => return None,
};
let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
let accounts = match accounts_result {
Ok(accounts) => accounts,
Err(_) => return None,
};
return accounts.get(spec.candidate_pool_account_index).cloned();
}
fn upstream_registry_instruction_match_is_locally_covered(
registry_match: &crate::UpstreamRegistryEntryDto,
) -> bool {
if registry_match.entry_kind != crate::ENTRY_KIND_INSTRUCTION {
return false;
}
let local_event_kind = crate::dex_event_coverage::known_local_event_kind(
registry_match.decoder_code.as_str(),
registry_match.entry_name.as_str(),
);
match local_event_kind {
Some(_) => return true,
None => return false,
}
}
fn build_upstream_registry_instruction_match_payload(
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
registry_match: &crate::UpstreamRegistryEntryDto,
data_base58: std::option::Option<&str>,
) -> serde_json::Value {
let data_bytes = instruction_data_bytes_from_base58(data_base58);
let data_byte_len = match data_bytes.as_ref() {
Some(data_bytes) => {
let len_result = u64::try_from(data_bytes.len());
match len_result {
Ok(len) => serde_json::Value::Number(serde_json::Number::from(len)),
Err(_) => serde_json::Value::Null,
}
},
None => serde_json::Value::Null,
};
return serde_json::json!({
"decoder": crate::UPSTREAM_REGISTRY_PROTOCOL_NAME,
"matchKind": "instruction_discriminator",
"signature": transaction.signature.clone(),
"slot": transaction.slot,
"instructionId": instruction.id,
"instructionIndex": instruction.instruction_index,
"innerInstructionIndex": instruction.inner_instruction_index,
"parentInstructionId": instruction.parent_instruction_id,
"stackHeight": instruction.stack_height,
"programId": instruction.program_id.clone(),
"programName": instruction.program_name.clone(),
"accounts": parse_instruction_accounts_value(instruction.accounts_json.as_str()),
"accountCount": parse_instruction_accounts_vec(instruction.accounts_json.as_str()).len(),
"dataBase58": data_base58,
"dataByteLen": data_byte_len,
"upstreamSourceRepo": registry_match.source_repo.clone(),
"upstreamSourcePath": registry_match.source_path.clone(),
"upstreamDecoderCode": registry_match.decoder_code.clone(),
"upstreamProgramFamily": registry_match.program_family.clone(),
"upstreamSurfaceKind": registry_match.surface_kind.clone(),
"upstreamEntryKind": registry_match.entry_kind.clone(),
"upstreamEntryName": registry_match.entry_name.clone(),
"upstreamDiscriminatorHex": registry_match.discriminator_hex.clone(),
"upstreamDiscriminatorLen": registry_match.discriminator_len,
"upstreamProofStatus": registry_match.proof_status.clone(),
"upstreamNotes": registry_match.notes.clone(),
"tradeCandidate": false,
"candleCandidate": false,
"nonTradeUseful": false,
"skipTradeReason": "upstream_git_unverified_registry_match",
"skipCandleReason": "upstream_git_unverified_registry_match"
});
}
fn parse_instruction_accounts_vec(accounts_json: &str) -> std::vec::Vec<std::string::String> {
let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
match accounts_result {
Ok(accounts) => return accounts,
Err(_) => return std::vec::Vec::new(),
}
}
fn parse_instruction_accounts_value(accounts_json: &str) -> serde_json::Value {
let accounts_result = serde_json::from_str::<serde_json::Value>(accounts_json);
match accounts_result {
Ok(accounts) => return accounts,
Err(_) => return serde_json::Value::Array(std::vec::Vec::new()),
}
}
fn parse_instruction_data_base58(
data_json: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return None,
};
let data_result = serde_json::from_str::<std::string::String>(data_json);
match data_result {
Ok(data) => return Some(data),
Err(_) => {
if data_json.trim().is_empty() {
return None;
}
return Some(data_json.to_string());
},
}
}
fn discriminator_hex_from_base58(
data_base58: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let bytes = instruction_data_bytes_from_base58(data_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" || protocol_name == "raydium_stable_swap" {
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,
) -> std::option::Option<std::string::String> {
let bytes = match bytes {
Some(bytes) => bytes,
None => return None,
};
if bytes.len() < offset + 8 {
return None;
}
let mut text = std::string::String::new();
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);
}
fn append_persisted_events(
target: &mut std::vec::Vec<crate::DexDecodedEventDto>,
source: std::vec::Vec<crate::DexDecodedEventDto>,
) {
for persisted_event in source {
target.push(persisted_event);
}
}
#[derive(Clone, Debug)]
struct RaydiumClmmProgramDataEventCandidate {
decoded_event: crate::RaydiumClmmDecodedEvent,
consumed: bool,
}
fn collect_raydium_clmm_program_data_events(
transaction: &crate::ChainTransactionDto,
) -> std::vec::Vec<RaydiumClmmProgramDataEventCandidate> {
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
let mut events = std::vec::Vec::new();
let mut clmm_stack_depth = 0_u32;
for log_message in logs {
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CLMM_PROGRAM_ID) {
clmm_stack_depth += 1;
continue;
}
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CLMM_PROGRAM_ID) {
clmm_stack_depth = clmm_stack_depth.saturating_sub(1);
continue;
}
if clmm_stack_depth == 0 {
continue;
}
let data_base64 = match log_message.strip_prefix("Program data: ") {
Some(data_base64) => data_base64.trim(),
None => continue,
};
if data_base64.is_empty() {
continue;
}
let decoded_event = crate::decode_raydium_clmm_program_data_event(data_base64);
if let Some(decoded_event) = decoded_event {
events.push(RaydiumClmmProgramDataEventCandidate { decoded_event, consumed: false });
}
}
return events;
}
async fn persist_matching_raydium_clmm_program_data_events(
service: &DexDecodeService,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
instruction_discriminator_hex: std::option::Option<&str>,
program_data_events: &mut [RaydiumClmmProgramDataEventCandidate],
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
) -> Result<(), crate::Error> {
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => return Ok(()),
};
let expected_event_kinds =
raydium_clmm_program_data_event_kinds_for_instruction(instruction_discriminator_hex);
if expected_event_kinds.is_empty() {
return Ok(());
}
let mut index = 0_usize;
while index < program_data_events.len() {
if program_data_events[index].consumed {
index += 1;
continue;
}
let event_kind = program_data_events[index].decoded_event.event_kind();
if !string_slice_contains(expected_event_kinds.as_slice(), event_kind) {
index += 1;
continue;
}
program_data_events[index].consumed = true;
let persist_result = service
.persist_raydium_clmm_event(
transaction,
instruction_id,
&program_data_events[index].decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
index += 1;
}
return Ok(());
}
fn raydium_clmm_program_data_event_kinds_for_instruction(
instruction_discriminator_hex: std::option::Option<&str>,
) -> std::vec::Vec<&'static str> {
let discriminator = match instruction_discriminator_hex {
Some(discriminator) => discriminator,
None => return std::vec::Vec::new(),
};
match discriminator {
"e992d18ecf6840bc" | "2b44d4a7592fa401" => {
return vec!["raydium_clmm.pool_created_event"];
},
"8888fcddc2427e59" => {
return vec!["raydium_clmm.collect_protocol_fee_event"];
},
"f8c69e91e17587c8" | "2b04ed0b1ac91e62" | "457d73daf5baf2c4" => {
return vec!["raydium_clmm.swap_event"];
},
"87802f4d0f98f031" | "4db84ad67056f1c7" | "4dffae527d1dc92e" => {
return vec![
"raydium_clmm.liquidity_calculate_event",
"raydium_clmm.create_personal_position_event",
"raydium_clmm.increase_liquidity_event",
"raydium_clmm.liquidity_change_event",
];
},
"2e9cf3760dcdfbb2" | "851d59df45eeb00a" => {
return vec![
"raydium_clmm.liquidity_calculate_event",
"raydium_clmm.increase_liquidity_event",
"raydium_clmm.liquidity_change_event",
];
},
"a026d06f685b2c01" | "3a7fbc3e4f52c460" => {
return vec![
"raydium_clmm.liquidity_calculate_event",
"raydium_clmm.decrease_liquidity_event",
"raydium_clmm.liquidity_change_event",
];
},
"7b86510031446262" | "c975989055556cb2" => {
return vec![
"raydium_clmm.decrease_liquidity_event",
"raydium_clmm.collect_personal_fee_event",
"raydium_clmm.liquidity_change_event",
];
},
"8934edd4d7756c68" | "313cae889a1c74c8" | "bd0eb5785576e33e" => {
return vec!["raydium_clmm.config_change_event"];
},
"5f87c0c4f281e644" | "7034a74b20c9d389" | "a3ace0340b9a6adf" => {
return vec!["raydium_clmm.update_reward_infos_event"];
},
_ => return std::vec::Vec::new(),
}
}
fn string_slice_contains(values: &[&'static str], expected: &str) -> bool {
for value in values {
if *value == expected {
return true;
}
}
return false;
}
#[derive(Clone, Debug)]
struct RaydiumCpmmProgramDataEventCandidate {
decoded_event: crate::RaydiumCpmmDecodedEvent,
consumed: bool,
}
fn collect_raydium_cpmm_program_data_events(
transaction: &crate::ChainTransactionDto,
) -> std::vec::Vec<RaydiumCpmmProgramDataEventCandidate> {
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
let mut events = std::vec::Vec::new();
let mut cpmm_stack_depth = 0_u32;
for log_message in logs {
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth += 1;
continue;
}
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1);
continue;
}
if cpmm_stack_depth == 0 {
continue;
}
let data_base64 = match log_message.strip_prefix("Program data: ") {
Some(data_base64) => data_base64.trim(),
None => continue,
};
if data_base64.is_empty() {
continue;
}
let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64);
if let Some(decoded_event) = decoded_event {
events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false });
}
}
return events;
}
async fn persist_matching_raydium_cpmm_program_data_event(
service: &DexDecodeService,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
instruction_kind: std::option::Option<&str>,
program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate],
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
) -> Result<(), crate::Error> {
let expected_event_kind = match instruction_kind {
Some("swap_base_input") => Some("swap_event"),
Some("swap_base_output") => Some("swap_event"),
Some("deposit") => Some("lp_change_event"),
Some("withdraw") => Some("lp_change_event"),
_ => None,
};
let expected_event_kind = match expected_event_kind {
Some(expected_event_kind) => expected_event_kind,
None => return Ok(()),
};
let mut index = 0_usize;
while index < program_data_events.len() {
if program_data_events[index].consumed {
index += 1;
continue;
}
let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) {
(crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true,
(crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true,
_ => false,
};
if !event_matches {
index += 1;
continue;
}
program_data_events[index].consumed = true;
let persist_result = service
.persist_raydium_cpmm_event(
transaction,
instruction,
&program_data_events[index].decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
return Ok(());
}
return Ok(());
}
fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec<std::string::String> {
let value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
let value = match value_result {
Ok(value) => value,
Err(_) => return std::vec::Vec::new(),
};
let meta = match value.get("meta") {
Some(meta) => meta,
None => return std::vec::Vec::new(),
};
let logs = match meta.get("logMessages") {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let logs = match logs.as_array() {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for log in logs {
if let Some(log) = log.as_str() {
output.push(log.to_string());
}
}
return output;
}
fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(" invoke [") {
return false;
}
return log_message.contains(program_id);
}
fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(program_id) {
return false;
}
if log_message.ends_with(" success") {
return true;
}
if log_message.contains(" failed: ") {
return true;
}
return false;
}
fn decoded_instruction_ids_from_persisted_events(
persisted: &[crate::DexDecodedEventDto],
) -> std::collections::HashSet<i64> {
let mut decoded_instruction_ids = std::collections::HashSet::<i64>::new();
for decoded_event in persisted {
let instruction_id = match decoded_event.instruction_id {
Some(instruction_id) => instruction_id,
None => continue,
};
decoded_instruction_ids.insert(instruction_id);
}
return decoded_instruction_ids;
}
fn append_persisted_events_result(
target: &mut std::vec::Vec<crate::DexDecodedEventDto>,
source_result: Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error>,
) -> Result<(), crate::Error> {
let source = match source_result {
Ok(source) => source,
Err(error) => return Err(error),
};
append_persisted_events(target, source);
return Ok(());
}
fn enriched_raydium_payload_value(
protocol_name: &str,
event_kind: &str,
raw_payload_json: &str,
) -> Result<serde_json::Value, crate::Error> {
let payload_value_result = serde_json::from_str::<serde_json::Value>(raw_payload_json);
let payload_value = match payload_value_result {
Ok(payload_value) => payload_value,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse decoded {} payload for '{}': {}",
protocol_name, event_kind, error
)));
},
};
return Ok(crate::enrich_dex_decoded_payload(protocol_name, event_kind, payload_value));
}
// Marks Meteora DAMM v1 swaps without direct amounts as non-materializable candidates
// before generic classification metadata is inserted.
fn prepare_meteora_damm_v1_swap_payload_for_classification(
event: &crate::MeteoraDammV1SwapDecoded,
) -> serde_json::Value {
let mut object = match event.payload_json.clone() {
serde_json::Value::Object(object) => object,
other => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
};
let payload_json = serde_json::Value::Object(object.clone());
if crate::dex_event_classification::decoded_payload_has_trade_amount_or_price_payload(
&payload_json,
) {
return serde_json::Value::Object(object);
}
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("meteora_damm_v1_swap_without_amount_payload".to_string()),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String("meteora_damm_v1_swap_without_amount_payload".to_string()),
);
return serde_json::Value::Object(object);
}
// Marks incomplete PumpSwap decoded trades as non-materializable candidates before generic
// classification metadata is inserted.
fn prepare_pump_swap_trade_payload_for_classification(
event: &crate::PumpSwapTradeDecoded,
) -> serde_json::Value {
let mut object = match event.payload_json.clone() {
serde_json::Value::Object(object) => object,
other => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
};
if event.pool_account.is_some() && event.token_a_mint.is_some() && event.token_b_mint.is_some()
{
return serde_json::Value::Object(object);
}
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("incomplete_pump_swap_trade_accounts".to_string()),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String("incomplete_pump_swap_trade_accounts".to_string()),
);
return serde_json::Value::Object(object);
}
#[cfg(test)]
mod tests {
async fn make_database() -> std::sync::Arc<crate::Database> {
let tempdir_result = tempfile::tempdir();
let tempdir = match tempdir_result {
Ok(tempdir) => tempdir,
Err(error) => panic!("tempdir must succeed: {}", error),
};
let database_path = tempdir.path().join("dex_decode.sqlite3");
let config = crate::DatabaseConfig {
enabled: true,
backend: crate::DatabaseBackend::Sqlite,
sqlite: crate::SqliteDatabaseConfig {
path: database_path.to_string_lossy().to_string(),
create_if_missing: true,
busy_timeout_ms: 5000,
max_connections: 1,
auto_initialize_schema: true,
use_wal: true,
},
};
let database_result = crate::Database::connect_and_initialize(&config).await;
let database = match database_result {
Ok(database) => database,
Err(error) => panic!("database init must succeed: {}", error),
};
return std::sync::Arc::new(database);
}
async fn seed_projected_raydium_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999001,
"blockTime": 1779000001,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium_amm_v4",
"stackHeight": 1,
"accounts": [
"Account0",
"Account1",
"Account2",
"Account3",
"PoolXYZ",
"Account5",
"Account6",
"LpMintXYZ",
"TokenAXYZ",
"TokenBXYZ",
"Account10",
"Account11",
"Account12",
"Account13",
"Account14",
"Account15",
"MarketXYZ"
],
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: initialize2"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
async fn seed_projected_pump_fun_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999002,
"blockTime": 1779000002,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::PUMP_FUN_PROGRAM_ID,
"program": "pump",
"stackHeight": 1,
"accounts": [
"MintPF111",
"MintAuthorityPF111",
"BondingCurvePF111",
"AssociatedBondingCurvePF111",
"GlobalPF111",
"CreatorPF111",
"System111",
"Token2022Program111",
"AtaProgram111"
],
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: CreateV2"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
async fn seed_projected_pump_swap_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999003,
"blockTime": 1779000003,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::PUMP_SWAP_PROGRAM_ID,
"program": "pump-amm",
"stackHeight": 1,
"accounts": [
"PumpSwapPool111",
"PumpSwapTokenA111",
"PumpSwapTokenB111",
"PumpSwapPoolV2_111"
],
"parsed": {
"info": {
"pool": "PumpSwapPool111",
"baseMint": "PumpSwapTokenA111",
"quoteMint": "PumpSwapTokenB111",
"poolV2": "PumpSwapPoolV2_111"
}
},
"data": "AJTQ2h9DXrBfqJi53PDQG2Fvki5tkaTU3"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: Buy"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_raydium_event() {
let database = make_database().await;
seed_projected_raydium_transaction(database.clone(), "sig-dex-decode-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "raydium_amm_v4");
assert_eq!(decoded[0].event_kind, "raydium_amm_v4.initialize2_pool");
assert_eq!(decoded[0].pool_account, Some("PoolXYZ".to_string()));
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_pump_fun_event() {
let database = make_database().await;
seed_projected_pump_fun_transaction(database.clone(), "sig-dex-decode-pump-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-pump-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "pump_fun");
assert_eq!(decoded[0].event_kind, "pump_fun.create_v2_token");
assert_eq!(decoded[0].pool_account, Some("BondingCurvePF111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("MintPF111".to_string()));
}
#[test]
fn prepare_meteora_damm_v1_swap_without_amounts_marks_event_non_actionable() {
let event = crate::MeteoraDammV1SwapDecoded {
transaction_id: 1,
instruction_id: 2,
signature: "sig-damm-v1-no-amounts".to_string(),
program_id: crate::METEORA_DAMM_V1_PROGRAM_ID.to_string(),
trade_side: crate::SwapTradeSide::Unknown,
pool_account: Some("PoolDammV1".to_string()),
token_a_mint: Some("TokenA".to_string()),
token_b_mint: Some("TokenB".to_string()),
payload_json: serde_json::json!({
"decoder": "meteora_damm_v1",
"eventKind": "swap"
}),
};
let prepared_payload =
super::prepare_meteora_damm_v1_swap_payload_for_classification(&event);
let object_option = prepared_payload.as_object();
let object = match object_option {
Some(object) => object,
None => {
panic!("expected prepared payload object");
},
};
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
assert_eq!(
object.get("skipTradeReason"),
Some(&serde_json::Value::String(
"meteora_damm_v1_swap_without_amount_payload".to_string()
))
);
}
#[test]
fn prepare_meteora_damm_v1_swap_with_amounts_keeps_event_actionable() {
let event = crate::MeteoraDammV1SwapDecoded {
transaction_id: 1,
instruction_id: 2,
signature: "sig-damm-v1-with-amounts".to_string(),
program_id: crate::METEORA_DAMM_V1_PROGRAM_ID.to_string(),
trade_side: crate::SwapTradeSide::Unknown,
pool_account: Some("PoolDammV1".to_string()),
token_a_mint: Some("TokenA".to_string()),
token_b_mint: Some("TokenB".to_string()),
payload_json: serde_json::json!({
"decoder": "meteora_damm_v1",
"eventKind": "swap",
"baseAmountRaw": "100",
"quoteAmountRaw": "250"
}),
};
let prepared_payload =
super::prepare_meteora_damm_v1_swap_payload_for_classification(&event);
let object_option = prepared_payload.as_object();
let object = match object_option {
Some(object) => object,
None => {
panic!("expected prepared payload object");
},
};
assert_eq!(object.get("tradeCandidate"), None);
assert_eq!(object.get("candleCandidate"), None);
assert_eq!(object.get("skipTradeReason"), None);
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_pump_swap_event() {
let database = make_database().await;
seed_projected_pump_swap_transaction(database.clone(), "sig-dex-decode-pumpswap-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result =
service.decode_transaction_by_signature("sig-dex-decode-pumpswap-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "pump_swap");
assert_eq!(decoded[0].event_kind, "pump_swap.buy");
assert_eq!(decoded[0].pool_account, Some("PumpSwapPool111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("PumpSwapTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some("PumpSwapTokenB111".to_string()));
assert_eq!(decoded[0].market_account, Some("PumpSwapPoolV2_111".to_string()));
}
async fn seed_projected_meteora_dbc_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999004,
"blockTime": 1779000004,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcPoolDecode111",
"DbcTokenDecode111",
crate::WSOL_MINT_ID,
"DbcConfigDecode111",
"DbcCreatorDecode111"
],
"parsed": {
"info": {
"pool": "DbcPoolDecode111",
"baseMint": "DbcTokenDecode111",
"quoteMint": crate::WSOL_MINT_ID,
"poolConfig": "DbcConfigDecode111",
"creator": "DbcCreatorDecode111"
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: CreatePool"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_meteora_dbc_event() {
let database = make_database().await;
seed_projected_meteora_dbc_transaction(database.clone(), "sig-dex-decode-dbc-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-dbc-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "meteora_dbc");
assert_eq!(decoded[0].event_kind, "meteora_dbc.create_pool");
assert_eq!(decoded[0].pool_account, Some("DbcPoolDecode111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("DbcTokenDecode111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
assert_eq!(decoded[0].market_account, Some("DbcConfigDecode111".to_string()));
}
async fn seed_projected_meteora_damm_v2_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999005,
"blockTime": 1779000005,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora_damm_v2",
"stackHeight": 1,
"accounts": [
"DammV2DecodePool111",
"DammV2DecodeTokenA111",
crate::WSOL_MINT_ID,
"DammV2DecodeConfig111",
"DammV2DecodeCreator111"
],
"parsed": {
"info": {
"instruction": "initialize_customizable_pool",
"pool": "DammV2DecodePool111",
"tokenAMint": "DammV2DecodeTokenA111",
"tokenBMint": crate::WSOL_MINT_ID,
"creator": "DammV2DecodeCreator111",
"isCustomizablePool": true
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: InitializeCustomizablePool"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_meteora_damm_v2_event() {
let database = make_database().await;
seed_projected_meteora_damm_v2_transaction(database.clone(), "sig-dex-decode-dammv2-1")
.await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result =
service.decode_transaction_by_signature("sig-dex-decode-dammv2-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "meteora_damm_v2");
assert_eq!(decoded[0].event_kind, "meteora_damm_v2.create_pool");
assert_eq!(decoded[0].pool_account, Some("DammV2DecodePool111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("DammV2DecodeTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
}
async fn seed_projected_meteora_damm_v1_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999006,
"blockTime": 1779000006,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora_damm_v1",
"stackHeight": 1,
"accounts": [
"DammV1DecodePool111",
"DammV1DecodeTokenA111",
crate::WSOL_MINT_ID,
"DammV1DecodeConfig111",
"DammV1DecodeCreator111"
],
"parsed": {
"info": {
"instruction": "initialize_pool_with_config",
"pool": "DammV1DecodePool111",
"tokenAMint": "DammV1DecodeTokenA111",
"tokenBMint": crate::WSOL_MINT_ID,
"config": "DammV1DecodeConfig111",
"creator": "DammV1DecodeCreator111"
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: InitializePoolWithConfig"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_meteora_damm_v1_event() {
let database = make_database().await;
seed_projected_meteora_damm_v1_transaction(database.clone(), "sig-dex-decode-dammv1-1")
.await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result =
service.decode_transaction_by_signature("sig-dex-decode-dammv1-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "meteora_damm_v1");
assert_eq!(decoded[0].event_kind, "meteora_damm_v1.create_pool");
assert_eq!(decoded[0].pool_account, Some("DammV1DecodePool111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("DammV1DecodeTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
}
async fn seed_projected_orca_whirlpools_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999007,
"blockTime": 1779000007,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca_whirlpools",
"stackHeight": 1,
"accounts": [
"OrcaDecodePool111",
"OrcaDecodeTokenA111",
crate::WSOL_MINT_ID,
"OrcaDecodeConfig111",
"OrcaDecodeCreator111"
],
"parsed": {
"info": {
"instruction": "initialize_pool_v2",
"whirlpool": "OrcaDecodePool111",
"tokenMintA": "OrcaDecodeTokenA111",
"tokenMintB": crate::WSOL_MINT_ID,
"whirlpoolsConfig": "OrcaDecodeConfig111",
"funder": "OrcaDecodeCreator111",
"tokenProgramA": crate::SPL_TOKEN_PROGRAM_ID,
"tokenProgramB": crate::SPL_TOKEN_PROGRAM_ID
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: InitializePoolV2"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_orca_whirlpools_event() {
let database = make_database().await;
seed_projected_orca_whirlpools_transaction(database.clone(), "sig-dex-decode-orca-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result = service.decode_transaction_by_signature("sig-dex-decode-orca-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "orca_whirlpools");
assert_eq!(decoded[0].event_kind, "orca_whirlpools.create_pool");
assert_eq!(decoded[0].pool_account, Some("OrcaDecodePool111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("OrcaDecodeTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
assert_eq!(decoded[0].market_account, Some("OrcaDecodeConfig111".to_string()));
}
async fn seed_projected_fluxbeam_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999008,
"blockTime": 1779000008,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::FLUXBEAM_PROGRAM_ID,
"program": "fluxbeam",
"stackHeight": 1,
"accounts": [
"FluxDecodePool111",
"FluxDecodeLpMint111",
"FluxDecodeTokenA111",
crate::WSOL_MINT_ID,
"FluxDecodeCreator111"
],
"parsed": {
"info": {
"instruction": "create_pool",
"pool": "FluxDecodePool111",
"lpMint": "FluxDecodeLpMint111",
"tokenA": "FluxDecodeTokenA111",
"tokenB": crate::WSOL_MINT_ID,
"payer": "FluxDecodeCreator111"
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: CreatePool"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_fluxbeam_event() {
let database = make_database().await;
seed_projected_fluxbeam_transaction(database.clone(), "sig-dex-decode-fluxbeam-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result =
service.decode_transaction_by_signature("sig-dex-decode-fluxbeam-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "fluxbeam");
assert_eq!(decoded[0].event_kind, "fluxbeam.create_pool");
assert_eq!(decoded[0].pool_account, Some("FluxDecodePool111".to_string()));
assert_eq!(decoded[0].market_account, Some("FluxDecodeLpMint111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("FluxDecodeTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
}
async fn seed_projected_dexlab_transaction(
database: std::sync::Arc<crate::Database>,
signature: &str,
) {
let service = crate::TransactionModelService::new(database);
let resolved_transaction = serde_json::json!({
"slot": 999009,
"blockTime": 1779000009,
"version": 0,
"transaction": {
"message": {
"instructions": [
{
"programId": crate::DEXLAB_PROGRAM_ID,
"program": "dexlab",
"stackHeight": 1,
"accounts": [
"DexlabDecodePool111",
"DexlabDecodeTokenA111",
crate::WSOL_MINT_ID,
"DexlabDecodeCreator111"
],
"parsed": {
"info": {
"instruction": "create_pool",
"pool": "DexlabDecodePool111",
"tokenA": "DexlabDecodeTokenA111",
"tokenB": crate::WSOL_MINT_ID,
"payer": "DexlabDecodeCreator111",
"feeTier": "0.3%"
}
},
"data": "opaque"
}
]
}
},
"meta": {
"err": null,
"logMessages": [
"Program log: Instruction: CreatePool"
]
}
});
let persist_result = service
.persist_resolved_transaction(
signature,
Some("helius_primary_http".to_string()),
&resolved_transaction,
)
.await;
if let Err(error) = persist_result {
panic!("projection must succeed: {}", error);
}
}
#[tokio::test]
async fn decode_transaction_by_signature_persists_decoded_dexlab_event() {
let database = make_database().await;
seed_projected_dexlab_transaction(database.clone(), "sig-dex-decode-dexlab-1").await;
let service = crate::DexDecodeService::new(database.clone());
let decoded_result =
service.decode_transaction_by_signature("sig-dex-decode-dexlab-1").await;
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].protocol_name, "dexlab");
assert_eq!(decoded[0].event_kind, "dexlab.create_pool");
assert_eq!(decoded[0].pool_account, Some("DexlabDecodePool111".to_string()));
assert_eq!(decoded[0].token_a_mint, Some("DexlabDecodeTokenA111".to_string()));
assert_eq!(decoded[0].token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
}
#[test]
fn classifies_swap_events_as_trade_candidates() {
assert_eq!(
crate::classify_dex_event_category_code("raydium_cpmm.swap_base_input"),
"trade"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_cpmm.swap_base_output"),
"trade"
);
assert_eq!(crate::classify_dex_event_category_code("raydium_clmm.swap"), "trade");
assert_eq!(crate::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
assert_eq!(crate::classify_dex_event_category_code("pump_fun.buy"), "trade");
assert!(crate::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
assert!(crate::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
}
#[test]
fn classifies_router_swap_as_trade_but_not_direct_candle_candidate() {
assert_eq!(
crate::classify_dex_event_category_code("raydium_clmm.swap_router_base_in"),
"trade"
);
assert!(crate::is_dex_trade_event_kind("raydium_clmm.swap_router_base_in"));
assert!(!crate::is_dex_candle_candidate_event_kind("raydium_clmm.swap_router_base_in"));
}
#[test]
fn classifies_fee_reward_liquidity_and_lifecycle_events() {
assert_eq!(
crate::classify_dex_event_category_code("raydium_cpmm.collect_creator_fee"),
"fee"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_clmm.collect_protocol_fee"),
"fee"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_clmm.set_reward_params"),
"reward"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_clmm.increase_liquidity_v2"),
"liquidity"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_cpmm.initialize"),
"pool_lifecycle"
);
assert_eq!(
crate::classify_dex_event_category_code("raydium_clmm.instruction_audit"),
"informational"
);
assert_eq!(
crate::classify_dex_event_lifecycle_kind_code("raydium_clmm.instruction_audit"),
"instruction_audit"
);
assert_eq!(
crate::classify_dex_event_actionability_code(
"raydium_clmm.instruction_audit",
false,
false
),
"informational"
);
}
#[test]
fn enriches_payload_without_overriding_existing_fields() {
let payload_json = serde_json::json!({
"eventCategory": "custom",
"amountIn": "10"
});
let enriched_payload = crate::enrich_dex_decoded_payload(
"raydium_cpmm",
"raydium_cpmm.swap_base_input",
payload_json,
);
let object_option = enriched_payload.as_object();
let object = match object_option {
Some(object) => object,
None => {
panic!("expected enriched payload object");
},
};
assert_eq!(
object.get("eventCategory"),
Some(&serde_json::Value::String("custom".to_owned()))
);
assert_eq!(
object.get("protocolName"),
Some(&serde_json::Value::String("raydium_cpmm".to_owned()))
);
assert_eq!(
object.get("eventKind"),
Some(&serde_json::Value::String("raydium_cpmm.swap_base_input".to_owned()))
);
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(true)));
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(true)));
}
#[test]
fn enriches_non_object_payload_as_raw_payload() {
let payload_json = serde_json::Value::String("raw".to_owned());
let enriched_payload = crate::enrich_dex_decoded_payload(
"raydium_clmm",
"raydium_clmm.collect_protocol_fee",
payload_json,
);
let object_option = enriched_payload.as_object();
let object = match object_option {
Some(object) => object,
None => {
panic!("expected enriched payload object");
},
};
assert_eq!(object.get("rawPayload"), Some(&serde_json::Value::String("raw".to_owned())));
assert_eq!(object.get("eventCategory"), Some(&serde_json::Value::String("fee".to_owned())));
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
assert_eq!(
object.get("skipTradeReason"),
Some(&serde_json::Value::String("non_trade_event".to_owned()))
);
}
#[test]
fn maps_observed_raydium_clmm_non_swap_discriminators() {
let create_pool = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("e992d18ecf6840bc"),
13,
);
let create_pool = match create_pool {
Some(create_pool) => create_pool,
None => panic!("create_pool discriminator must be mapped"),
};
assert_eq!(create_pool.event_kind, "raydium_clmm.create_pool");
assert_eq!(create_pool.pool_account_index, Some(2));
assert_eq!(create_pool.token_a_mint_index, Some(3));
assert_eq!(create_pool.token_b_mint_index, Some(4));
let collect_protocol_fee = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("8888fcddc2427e59"),
11,
);
let collect_protocol_fee = match collect_protocol_fee {
Some(collect_protocol_fee) => collect_protocol_fee,
None => panic!("collect_protocol_fee discriminator must be mapped"),
};
assert_eq!(collect_protocol_fee.event_kind, "raydium_clmm.collect_protocol_fee");
assert_eq!(collect_protocol_fee.pool_account_index, Some(1));
assert_eq!(collect_protocol_fee.token_a_mint_index, Some(5));
assert_eq!(collect_protocol_fee.token_b_mint_index, Some(6));
let decrease = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("3a7fbc3e4f52c460"),
19,
);
let decrease = match decrease {
Some(decrease) => decrease,
None => panic!("decrease_liquidity_v2 discriminator must be mapped"),
};
assert_eq!(decrease.event_kind, "raydium_clmm.decrease_liquidity_v2");
assert_eq!(decrease.pool_account_index, Some(3));
assert_eq!(decrease.token_a_mint_index, Some(14));
assert_eq!(decrease.token_b_mint_index, Some(15));
let increase = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("851d59df45eeb00a"),
15,
);
let increase = match increase {
Some(increase) => increase,
None => panic!("increase_liquidity_v2 discriminator must be mapped"),
};
assert_eq!(increase.event_kind, "raydium_clmm.increase_liquidity_v2");
assert_eq!(increase.pool_account_index, Some(2));
let open_limit_order = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("9d20dab7471d1293"),
11,
);
let open_limit_order = match open_limit_order {
Some(open_limit_order) => open_limit_order,
None => panic!("open_limit_order discriminator must be mapped"),
};
assert_eq!(open_limit_order.event_kind, "raydium_clmm.open_limit_order");
assert_eq!(open_limit_order.pool_account_index, Some(1));
assert_eq!(open_limit_order.token_a_mint_index, Some(7));
let increase_limit_order = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("b19059ecfaba7d63"),
8,
);
let increase_limit_order = match increase_limit_order {
Some(increase_limit_order) => increase_limit_order,
None => panic!("increase_limit_order discriminator must be mapped"),
};
assert_eq!(increase_limit_order.event_kind, "raydium_clmm.increase_limit_order");
assert_eq!(increase_limit_order.pool_account_index, Some(1));
assert_eq!(increase_limit_order.token_a_mint_index, Some(6));
let decrease_limit_order = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("759d3c674231a300"),
13,
);
let decrease_limit_order = match decrease_limit_order {
Some(decrease_limit_order) => decrease_limit_order,
None => panic!("decrease_limit_order discriminator must be mapped"),
};
assert_eq!(decrease_limit_order.event_kind, "raydium_clmm.decrease_limit_order");
assert_eq!(decrease_limit_order.pool_account_index, Some(1));
assert_eq!(decrease_limit_order.token_a_mint_index, Some(8));
assert_eq!(decrease_limit_order.token_b_mint_index, Some(9));
let update_dynamic_fee_config = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("0707500802c784f0"),
2,
);
let update_dynamic_fee_config = match update_dynamic_fee_config {
Some(update_dynamic_fee_config) => update_dynamic_fee_config,
None => panic!("update_dynamic_fee_config discriminator must be mapped"),
};
assert_eq!(update_dynamic_fee_config.event_kind, "raydium_clmm.update_dynamic_fee_config");
let cpi_event = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("e445a52e51cb9a1d"),
1,
);
let cpi_event = match cpi_event {
Some(cpi_event) => cpi_event,
None => panic!("clmm cpi_event discriminator must be mapped"),
};
assert_eq!(cpi_event.event_kind, "raydium_clmm.cpi_event");
}
#[test]
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
let expected = [
("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
("e445a52e51cb9a1d", 1_usize, "raydium_cpmm.cpi_event"),
("40f4bc78a7e9690a", 3_usize, "raydium_cpmm.anchor_idl_instruction"),
("40f4bc78a7e9690a", 6_usize, "raydium_cpmm.anchor_idl_instruction"),
];
for (discriminator, account_count, event_kind) in expected {
let mapped = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some(discriminator),
account_count,
);
let mapped = match mapped {
Some(mapped) => mapped,
None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
};
assert_eq!(mapped.event_kind, event_kind);
}
}
#[test]
fn extracts_instruction_discriminator_from_camel_and_snake_payload_keys() {
let camel_payload = serde_json::json!({
"instructionDiscriminatorHex": "e992d18ecf6840bc"
});
assert_eq!(
super::instruction_discriminator_hex_from_payload(&camel_payload),
Some("e992d18ecf6840bc".to_string())
);
let snake_payload = serde_json::json!({
"instruction_discriminator_hex": "8888fcddc2427e59"
});
assert_eq!(
super::instruction_discriminator_hex_from_payload(&snake_payload),
Some("8888fcddc2427e59".to_string())
);
}
#[test]
fn skips_raydium_audit_when_discriminator_was_already_decoded() {
let mut keys = std::collections::HashSet::<std::string::String>::new();
keys.insert(super::raydium_decoded_discriminator_key("raydium_clmm", "e992d18ecf6840bc"));
assert!(super::raydium_instruction_already_decoded_by_discriminator(
&keys,
"raydium_clmm",
Some("e992d18ecf6840bc"),
));
assert!(!super::raydium_instruction_already_decoded_by_discriminator(
&keys,
"raydium_clmm",
Some("8888fcddc2427e59"),
));
}
#[test]
fn immediately_materializes_only_targeted_clmm_non_trade_events() {
assert!(super::should_immediately_materialize_decoded_non_trade_event(
"raydium_clmm.create_pool",
));
assert!(super::should_immediately_materialize_decoded_non_trade_event(
"raydium_clmm.collect_protocol_fee",
));
assert!(!super::should_immediately_materialize_decoded_non_trade_event(
"raydium_clmm.swap",
));
assert!(!super::should_immediately_materialize_decoded_non_trade_event(
"raydium_cpmm.collect_protocol_fee",
));
}
#[test]
fn maps_raydium_launchpad_non_trade_discriminators() {
let buy = super::raydium_mapped_non_trade_instruction_spec(
"raydium_launchpad",
Some("faea0d7bd59c13ec"),
11,
);
let buy = match buy {
Some(buy) => buy,
None => panic!("buy_exact_in discriminator must map"),
};
assert_eq!(buy.instruction_name, "buy_exact_in");
assert_eq!(buy.event_kind, "raydium_launchpad.buy_exact_in");
assert_eq!(buy.pool_account_index, Some(4));
assert_eq!(buy.token_a_mint_index, Some(9));
assert_eq!(buy.token_b_mint_index, Some(10));
assert_eq!(buy.lp_mint_index, None);
let migration = super::raydium_mapped_non_trade_instruction_spec(
"raydium_launchpad",
Some("cf52c091fecf91df"),
1,
);
let migration = match migration {
Some(migration) => migration,
None => panic!("migrate_to_amm discriminator must map"),
};
assert_eq!(migration.event_kind, "raydium_launchpad.migrate_to_amm");
assert_eq!(migration.pool_account_index, None);
}
#[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("raydium_launchpad"),
Some("raydium_launchpad.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("meteora_damm_v1"),
Some("meteora_damm_v1.instruction_audit")
);
assert_eq!(
super::instruction_audit_event_kind_by_protocol("meteora_damm_v2"),
Some("meteora_damm_v2.instruction_audit")
);
assert_eq!(
super::instruction_audit_event_kind_by_protocol("meteora_dbc"),
Some("meteora_dbc.instruction_audit")
);
assert_eq!(super::instruction_audit_event_kind_by_protocol("unknown"), None);
}
#[test]
fn upstream_registry_match_payload_is_never_trade_or_candle_candidate() {
let transaction = crate::ChainTransactionDto::new(
"upstream-registry-test-signature".to_string(),
Some(123),
Some(123456),
Some("test".to_string()),
None,
None,
None,
"{}".to_string(),
);
let instruction = crate::ChainInstructionDto::new(
1,
None,
0,
None,
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
None,
None,
"[]".to_string(),
Some("data".to_string()),
None,
None,
);
let registry_match = crate::UpstreamRegistryEntryDto {
source_repo: Some("sevenlabs-hq/carbon".to_string()),
source_path: Some("decoders/example.rs".to_string()),
decoder_code: "meteora_damm_v2".to_string(),
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
program_family: "meteora".to_string(),
surface_kind: "amm".to_string(),
entry_kind: crate::ENTRY_KIND_INSTRUCTION.to_string(),
entry_name: "swap".to_string(),
discriminator_hex: Some("f8c69e91e17587c8".to_string()),
discriminator_len: Some(8),
proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED.to_string(),
notes: "test".to_string(),
};
let payload = super::build_upstream_registry_instruction_match_payload(
&transaction,
&instruction,
&registry_match,
Some("data"),
);
assert_eq!(payload.get("tradeCandidate").and_then(serde_json::Value::as_bool), Some(false));
assert_eq!(
payload.get("candleCandidate").and_then(serde_json::Value::as_bool),
Some(false)
);
assert_eq!(
payload.get("upstreamProofStatus").and_then(serde_json::Value::as_str),
Some(crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED)
);
}
}