Files
khadhroony-bobobot/kb_lib/src/dex_decode.rs
2026-05-13 20:11:29 +02:00

2119 lines
85 KiB
Rust

// file: kb_lib/src/dex_decode.rs
//! Persistence-oriented DEX decoding service.
/// DEX decode service.
#[derive(Debug, Clone)]
pub struct DexDecodeService {
database: std::sync::Arc<crate::Database>,
persistence: crate::DetectionPersistenceService,
raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder,
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,
}
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(),
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(),
};
}
/// 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_clmm_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_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.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);
}
return Ok(persisted);
}
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 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(),
};
return crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input)
.await;
}
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;
},
}
}
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.config_account.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;
},
}
}
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;
},
}
}
async fn persist_raydium_clmm_event(
&self,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
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 instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"raydium clmm 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 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(),
Some(decoded_event.pool_account().to_string()),
None,
Some(decoded_event.base_mint().to_string()),
Some(decoded_event.quote_mint().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(),
Some(decoded_event.pool_account().to_string()),
None,
Some(decoded_event.base_mint().to_string()),
Some(decoded_event.quote_mint().to_string()),
None,
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();
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 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);
}
}
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 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,
};
if program_id.as_str() != crate::RAYDIUM_CLMM_PROGRAM_ID {
continue;
}
let data_json = match instruction.data_json.as_ref() {
Some(data_json) => data_json,
None => continue,
};
let decoded_events = crate::decode_raydium_clmm_instruction(
instruction.accounts_json.as_str(),
data_json.as_str(),
);
for decoded_event in &decoded_events {
let persist_result =
self.persist_raydium_clmm_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);
}
}
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 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 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);
}
}
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);
}
}
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"
);
}
#[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()))
);
}
}