2119 lines
85 KiB
Rust
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()))
|
|
);
|
|
}
|
|
}
|