6493 lines
255 KiB
Rust
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(®istry_match) {
|
|
continue;
|
|
}
|
|
let payload = build_upstream_registry_instruction_match_payload(
|
|
transaction,
|
|
instruction,
|
|
®istry_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,
|
|
®istry_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)
|
|
);
|
|
}
|
|
}
|