0.7.42
This commit is contained in:
@@ -8,6 +8,7 @@ pub struct DexDecodeService {
|
||||
database: std::sync::Arc<crate::Database>,
|
||||
persistence: crate::DetectionPersistenceService,
|
||||
raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder,
|
||||
raydium_clmm_decoder: crate::RaydiumClmmDecoder,
|
||||
pump_fun_decoder: crate::PumpFunDecoder,
|
||||
pump_swap_decoder: crate::PumpSwapDecoder,
|
||||
orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder,
|
||||
@@ -27,6 +28,7 @@ impl DexDecodeService {
|
||||
database,
|
||||
persistence,
|
||||
raydium_amm_v4_decoder: crate::RaydiumAmmV4Decoder::new(),
|
||||
raydium_clmm_decoder: crate::RaydiumClmmDecoder::new(),
|
||||
pump_fun_decoder: crate::PumpFunDecoder::new(),
|
||||
pump_swap_decoder: crate::PumpSwapDecoder::new(),
|
||||
orca_whirlpools_decoder: crate::OrcaWhirlpoolsDecoder::new(),
|
||||
@@ -77,6 +79,14 @@ impl DexDecodeService {
|
||||
if let Err(error) = append_result {
|
||||
return Err(error);
|
||||
}
|
||||
let append_result = append_persisted_events_result(
|
||||
&mut persisted,
|
||||
self.preserve_unmatched_raydium_instruction_audits(&transaction, &instructions)
|
||||
.await,
|
||||
);
|
||||
if let Err(error) = append_result {
|
||||
return Err(error);
|
||||
}
|
||||
let append_result = append_persisted_events_result(
|
||||
&mut persisted,
|
||||
self.decode_and_persist_pump_fun_events(&transaction, &instructions).await,
|
||||
@@ -181,8 +191,52 @@ impl DexDecodeService {
|
||||
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)
|
||||
let materialized_result =
|
||||
crate::dex_decoded_event_materialization::materialize_dex_decoded_event(input).await;
|
||||
let materialized = match materialized_result {
|
||||
Ok(materialized) => materialized,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let cleanup_result = self
|
||||
.delete_replaced_raydium_instruction_audit(
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
protocol_name,
|
||||
event_kind,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = cleanup_result {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(materialized);
|
||||
}
|
||||
|
||||
async fn delete_replaced_raydium_instruction_audit(
|
||||
&self,
|
||||
transaction_id: i64,
|
||||
instruction_id: i64,
|
||||
protocol_name: &str,
|
||||
event_kind: &str,
|
||||
) -> Result<(), crate::Error> {
|
||||
if event_kind.ends_with(".instruction_audit") {
|
||||
return Ok(());
|
||||
}
|
||||
let audit_event_kind = match raydium_instruction_audit_event_kind_by_protocol(protocol_name)
|
||||
{
|
||||
Some(audit_event_kind) => audit_event_kind,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let delete_result = crate::query_dex_decoded_events_delete_by_key(
|
||||
self.database.as_ref(),
|
||||
transaction_id,
|
||||
Some(instruction_id),
|
||||
audit_event_kind,
|
||||
)
|
||||
.await;
|
||||
match delete_result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_dexlab_event(
|
||||
@@ -586,7 +640,7 @@ impl DexDecodeService {
|
||||
async fn persist_raydium_clmm_event(
|
||||
&self,
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instruction: &crate::ChainInstructionDto,
|
||||
instruction_id: i64,
|
||||
decoded_event: &crate::RaydiumClmmDecodedEvent,
|
||||
) -> Result<crate::DexDecodedEventDto, crate::Error> {
|
||||
let transaction_id = match transaction.id {
|
||||
@@ -598,15 +652,6 @@ impl DexDecodeService {
|
||||
)));
|
||||
},
|
||||
};
|
||||
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,
|
||||
@@ -889,32 +934,26 @@ impl DexDecodeService {
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
|
||||
let decoded_result =
|
||||
self.raydium_clmm_decoder.decode_transaction(transaction, instructions);
|
||||
let decoded_events = match decoded_result {
|
||||
Ok(decoded_events) => decoded_events,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut persisted = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
let program_id = match instruction.program_id.as_ref() {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
for decoded_event in &decoded_events {
|
||||
let persist_result = self
|
||||
.persist_raydium_clmm_event(
|
||||
transaction,
|
||||
decoded_event.instruction_id,
|
||||
&decoded_event.decoded_event,
|
||||
)
|
||||
.await;
|
||||
let persisted_event = match persist_result {
|
||||
Ok(persisted_event) => persisted_event,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
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);
|
||||
}
|
||||
persisted.push(persisted_event);
|
||||
}
|
||||
return Ok(persisted);
|
||||
}
|
||||
@@ -943,6 +982,129 @@ impl DexDecodeService {
|
||||
return Ok(persisted);
|
||||
}
|
||||
|
||||
async fn preserve_unmatched_raydium_instruction_audits(
|
||||
&self,
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instructions: &[crate::ChainInstructionDto],
|
||||
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
|
||||
let transaction_id = match transaction.id {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
},
|
||||
};
|
||||
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
|
||||
self.database.as_ref(),
|
||||
transaction_id,
|
||||
)
|
||||
.await;
|
||||
let decoded_events = match decoded_events_result {
|
||||
Ok(decoded_events) => decoded_events,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut decoded_instruction_ids = std::collections::HashSet::<i64>::new();
|
||||
for decoded_event in &decoded_events {
|
||||
if !decoded_event.protocol_name.starts_with("raydium_") {
|
||||
continue;
|
||||
}
|
||||
if decoded_event.event_kind.ends_with(".instruction_audit") {
|
||||
continue;
|
||||
}
|
||||
let instruction_id = match decoded_event.instruction_id {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
decoded_instruction_ids.insert(instruction_id);
|
||||
}
|
||||
let mut persisted = std::vec::Vec::new();
|
||||
for instruction in instructions {
|
||||
let program_id = match instruction.program_id.as_ref() {
|
||||
Some(program_id) => program_id,
|
||||
None => continue,
|
||||
};
|
||||
let audit_spec = match raydium_instruction_audit_spec(program_id.as_str()) {
|
||||
Some(audit_spec) => audit_spec,
|
||||
None => continue,
|
||||
};
|
||||
let instruction_id = match instruction.id {
|
||||
Some(instruction_id) => instruction_id,
|
||||
None => continue,
|
||||
};
|
||||
if decoded_instruction_ids.contains(&instruction_id) {
|
||||
continue;
|
||||
}
|
||||
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
|
||||
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
|
||||
let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref());
|
||||
let mapped_spec = raydium_mapped_non_trade_instruction_spec(
|
||||
audit_spec.protocol_name,
|
||||
discriminator_hex.as_deref(),
|
||||
accounts.len(),
|
||||
);
|
||||
let event_kind = match mapped_spec {
|
||||
Some(mapped_spec) => mapped_spec.event_kind,
|
||||
None => audit_spec.event_kind,
|
||||
};
|
||||
let mut payload = build_raydium_instruction_audit_payload(
|
||||
transaction,
|
||||
instruction,
|
||||
audit_spec.protocol_name,
|
||||
event_kind,
|
||||
program_id.as_str(),
|
||||
);
|
||||
if let Some(mapped_spec) = mapped_spec {
|
||||
payload = enrich_raydium_mapped_non_trade_payload(
|
||||
payload,
|
||||
mapped_spec,
|
||||
data_base58.as_deref(),
|
||||
);
|
||||
}
|
||||
let pool_account = candidate_raydium_mapped_pool_account(
|
||||
mapped_spec,
|
||||
accounts.as_slice(),
|
||||
audit_spec.protocol_name,
|
||||
instruction.accounts_json.as_str(),
|
||||
);
|
||||
let token_a_mint = candidate_raydium_mapped_account(
|
||||
mapped_spec.and_then(|spec| spec.token_a_mint_index),
|
||||
accounts.as_slice(),
|
||||
);
|
||||
let token_b_mint = candidate_raydium_mapped_account(
|
||||
mapped_spec.and_then(|spec| spec.token_b_mint_index),
|
||||
accounts.as_slice(),
|
||||
);
|
||||
let lp_mint = candidate_raydium_mapped_account(
|
||||
mapped_spec.and_then(|spec| spec.lp_mint_index),
|
||||
accounts.as_slice(),
|
||||
);
|
||||
let persist_result = self
|
||||
.materialize_named_dex_event(
|
||||
transaction,
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
audit_spec.protocol_name,
|
||||
program_id.clone(),
|
||||
event_kind,
|
||||
pool_account,
|
||||
None,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
lp_mint,
|
||||
payload,
|
||||
)
|
||||
.await;
|
||||
let persisted_event = match persist_result {
|
||||
Ok(persisted_event) => persisted_event,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
persisted.push(persisted_event);
|
||||
}
|
||||
return Ok(persisted);
|
||||
}
|
||||
|
||||
async fn decode_and_persist_pump_fun_events(
|
||||
&self,
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
@@ -1150,6 +1312,453 @@ impl DexDecodeService {
|
||||
}
|
||||
}
|
||||
|
||||
struct RaydiumInstructionAuditSpec {
|
||||
protocol_name: &'static str,
|
||||
event_kind: &'static str,
|
||||
candidate_pool_account_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: &'static str,
|
||||
event_kind: &'static str,
|
||||
pool_account_index: std::option::Option<usize>,
|
||||
token_a_mint_index: std::option::Option<usize>,
|
||||
token_b_mint_index: std::option::Option<usize>,
|
||||
lp_mint_index: std::option::Option<usize>,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum RaydiumMappedNonTradeAmountLayout {
|
||||
None,
|
||||
ClmmLiquidityV2,
|
||||
CpmmWithdraw,
|
||||
}
|
||||
|
||||
fn raydium_instruction_audit_spec(
|
||||
program_id: &str,
|
||||
) -> std::option::Option<RaydiumInstructionAuditSpec> {
|
||||
if program_id == crate::RAYDIUM_AMM_V4_PROGRAM_ID {
|
||||
return Some(RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_amm_v4",
|
||||
event_kind: "raydium_amm_v4.instruction_audit",
|
||||
candidate_pool_account_index: 1,
|
||||
});
|
||||
}
|
||||
if program_id == crate::RAYDIUM_CLMM_PROGRAM_ID {
|
||||
return Some(RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_clmm",
|
||||
event_kind: "raydium_clmm.instruction_audit",
|
||||
candidate_pool_account_index: 2,
|
||||
});
|
||||
}
|
||||
if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID {
|
||||
return Some(RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_cpmm",
|
||||
event_kind: "raydium_cpmm.instruction_audit",
|
||||
candidate_pool_account_index: 3,
|
||||
});
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn raydium_mapped_non_trade_instruction_spec(
|
||||
protocol_name: &str,
|
||||
discriminator_hex: std::option::Option<&str>,
|
||||
account_count: usize,
|
||||
) -> std::option::Option<RaydiumMappedNonTradeInstructionSpec> {
|
||||
let discriminator_hex = match discriminator_hex {
|
||||
Some(discriminator_hex) => discriminator_hex,
|
||||
None => return None,
|
||||
};
|
||||
if protocol_name == "raydium_clmm" {
|
||||
if discriminator_hex == "3a7fbc3e4f52c460" && account_count >= 16 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "decrease_liquidity_v2",
|
||||
event_kind: "raydium_clmm.decrease_liquidity_v2",
|
||||
pool_account_index: Some(3),
|
||||
token_a_mint_index: Some(14),
|
||||
token_b_mint_index: Some(15),
|
||||
lp_mint_index: None,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2,
|
||||
});
|
||||
}
|
||||
if discriminator_hex == "851d59df45eeb00a" && account_count >= 15 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "increase_liquidity_v2",
|
||||
event_kind: "raydium_clmm.increase_liquidity_v2",
|
||||
pool_account_index: Some(2),
|
||||
token_a_mint_index: Some(13),
|
||||
token_b_mint_index: Some(14),
|
||||
lp_mint_index: None,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2,
|
||||
});
|
||||
}
|
||||
if discriminator_hex == "4dffae527d1dc92e" && account_count >= 20 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "open_position_with_token22_nft",
|
||||
event_kind: "raydium_clmm.open_position_with_token22_nft",
|
||||
pool_account_index: Some(4),
|
||||
token_a_mint_index: Some(18),
|
||||
token_b_mint_index: Some(19),
|
||||
lp_mint_index: Some(2),
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||
});
|
||||
}
|
||||
if discriminator_hex == "7b86510031446262" && account_count >= 6 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "close_position",
|
||||
event_kind: "raydium_clmm.close_position",
|
||||
pool_account_index: None,
|
||||
token_a_mint_index: None,
|
||||
token_b_mint_index: None,
|
||||
lp_mint_index: Some(1),
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
if protocol_name == "raydium_cpmm" {
|
||||
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "collect_creator_fee",
|
||||
event_kind: "raydium_cpmm.collect_creator_fee",
|
||||
pool_account_index: Some(3),
|
||||
token_a_mint_index: None,
|
||||
token_b_mint_index: None,
|
||||
lp_mint_index: None,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||
});
|
||||
}
|
||||
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "withdraw",
|
||||
event_kind: "raydium_cpmm.withdraw",
|
||||
pool_account_index: Some(3),
|
||||
token_a_mint_index: None,
|
||||
token_b_mint_index: None,
|
||||
lp_mint_index: None,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
|
||||
});
|
||||
}
|
||||
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
|
||||
return Some(RaydiumMappedNonTradeInstructionSpec {
|
||||
instruction_name: "initialize",
|
||||
event_kind: "raydium_cpmm.initialize",
|
||||
pool_account_index: Some(3),
|
||||
token_a_mint_index: Some(4),
|
||||
token_b_mint_index: Some(5),
|
||||
lp_mint_index: Some(13),
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn candidate_raydium_mapped_pool_account(
|
||||
mapped_spec: std::option::Option<RaydiumMappedNonTradeInstructionSpec>,
|
||||
accounts: &[std::string::String],
|
||||
protocol_name: &str,
|
||||
accounts_json: &str,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if let Some(mapped_spec) = mapped_spec {
|
||||
if let Some(pool_account_index) = mapped_spec.pool_account_index {
|
||||
return candidate_raydium_mapped_account(Some(pool_account_index), accounts);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
return candidate_raydium_audit_pool_account(protocol_name, accounts_json);
|
||||
}
|
||||
|
||||
fn candidate_raydium_mapped_account(
|
||||
index: std::option::Option<usize>,
|
||||
accounts: &[std::string::String],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let index = match index {
|
||||
Some(index) => index,
|
||||
None => return None,
|
||||
};
|
||||
return accounts.get(index).cloned();
|
||||
}
|
||||
|
||||
fn enrich_raydium_mapped_non_trade_payload(
|
||||
payload: serde_json::Value,
|
||||
mapped_spec: RaydiumMappedNonTradeInstructionSpec,
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
let mut object = match payload {
|
||||
serde_json::Value::Object(object) => object,
|
||||
other => {
|
||||
let mut object = serde_json::Map::new();
|
||||
object.insert("rawPayload".to_string(), other);
|
||||
object
|
||||
},
|
||||
};
|
||||
object.remove("tradeCandidate");
|
||||
object.remove("candleCandidate");
|
||||
object.remove("nonTradeUseful");
|
||||
object.remove("skipTradeReason");
|
||||
object.remove("skipCandleReason");
|
||||
object.insert(
|
||||
"instructionName".to_string(),
|
||||
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
|
||||
);
|
||||
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
|
||||
object.insert(
|
||||
"auditReason".to_string(),
|
||||
serde_json::Value::String("raydium_non_swap_instruction_mapped_from_corpus".to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"proofSource".to_string(),
|
||||
serde_json::Value::String(
|
||||
"local_corpus_discriminator_and_raydium_idl_instruction_name".to_string(),
|
||||
),
|
||||
);
|
||||
let data_bytes = instruction_data_bytes_from_base58(data_base58);
|
||||
if let Some(data_bytes) = data_bytes {
|
||||
insert_raydium_mapped_amounts(
|
||||
&mut object,
|
||||
mapped_spec.amount_layout,
|
||||
data_bytes.as_slice(),
|
||||
);
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
|
||||
fn insert_raydium_mapped_amounts(
|
||||
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
|
||||
amount_layout: RaydiumMappedNonTradeAmountLayout,
|
||||
data: &[u8],
|
||||
) {
|
||||
match amount_layout {
|
||||
RaydiumMappedNonTradeAmountLayout::None => return,
|
||||
RaydiumMappedNonTradeAmountLayout::ClmmLiquidityV2 => {
|
||||
if let Some(liquidity) = read_u128_le_from_bytes(data, 8) {
|
||||
object.insert(
|
||||
"liquidity".to_string(),
|
||||
serde_json::Value::String(liquidity.to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"lpAmountRaw".to_string(),
|
||||
serde_json::Value::String(liquidity.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(amount_0) = read_u64_le_from_bytes(data, 24) {
|
||||
object.insert(
|
||||
"tokenAAmount".to_string(),
|
||||
serde_json::Value::String(amount_0.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(amount_1) = read_u64_le_from_bytes(data, 32) {
|
||||
object.insert(
|
||||
"tokenBAmount".to_string(),
|
||||
serde_json::Value::String(amount_1.to_string()),
|
||||
);
|
||||
}
|
||||
},
|
||||
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
|
||||
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
|
||||
object.insert(
|
||||
"lpAmountRaw".to_string(),
|
||||
serde_json::Value::String(lp_amount.to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"liquidity".to_string(),
|
||||
serde_json::Value::String(lp_amount.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(amount_0) = read_u64_le_from_bytes(data, 16) {
|
||||
object.insert(
|
||||
"tokenAAmount".to_string(),
|
||||
serde_json::Value::String(amount_0.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
|
||||
object.insert(
|
||||
"tokenBAmount".to_string(),
|
||||
serde_json::Value::String(amount_1.to_string()),
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn instruction_data_bytes_from_base58(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::vec::Vec<u8>> {
|
||||
let data_base58 = match data_base58 {
|
||||
Some(data_base58) => data_base58,
|
||||
None => return None,
|
||||
};
|
||||
let bytes_result = bs58::decode(data_base58).into_vec();
|
||||
match bytes_result {
|
||||
Ok(bytes) => return Some(bytes),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = [0_u8; 8];
|
||||
let mut index = 0_usize;
|
||||
while index < 8 {
|
||||
bytes[index] = data[offset + index];
|
||||
index += 1;
|
||||
}
|
||||
return Some(u64::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn read_u128_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u128> {
|
||||
if data.len() < offset + 16 {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = [0_u8; 16];
|
||||
let mut index = 0_usize;
|
||||
while index < 16 {
|
||||
bytes[index] = data[offset + index];
|
||||
index += 1;
|
||||
}
|
||||
return Some(u128::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn raydium_instruction_audit_event_kind_by_protocol(
|
||||
protocol_name: &str,
|
||||
) -> std::option::Option<&'static str> {
|
||||
match protocol_name {
|
||||
"raydium_amm_v4" => return Some("raydium_amm_v4.instruction_audit"),
|
||||
"raydium_clmm" => return Some("raydium_clmm.instruction_audit"),
|
||||
"raydium_cpmm" => return Some("raydium_cpmm.instruction_audit"),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_raydium_instruction_audit_payload(
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
instruction: &crate::ChainInstructionDto,
|
||||
protocol_name: &str,
|
||||
event_kind: &str,
|
||||
program_id: &str,
|
||||
) -> serde_json::Value {
|
||||
let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str());
|
||||
let account_count = match accounts.as_array() {
|
||||
Some(items) => items.len(),
|
||||
None => 0,
|
||||
};
|
||||
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
|
||||
let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref());
|
||||
return serde_json::json!({
|
||||
"decoder": protocol_name,
|
||||
"eventKind": event_kind,
|
||||
"signature": transaction.signature,
|
||||
"instructionId": instruction.id,
|
||||
"instructionIndex": instruction.instruction_index,
|
||||
"innerInstructionIndex": instruction.inner_instruction_index,
|
||||
"innerInstruction": instruction.inner_instruction_index.is_some(),
|
||||
"parentInstructionId": instruction.parent_instruction_id,
|
||||
"programId": program_id,
|
||||
"accounts": accounts,
|
||||
"accountCount": account_count,
|
||||
"data": data_base58,
|
||||
"discriminatorHex": discriminator_hex,
|
||||
"auditReason": "raydium_instruction_not_decoded_by_specific_decoder",
|
||||
"tradeCandidate": false,
|
||||
"candleCandidate": false,
|
||||
"nonTradeUseful": false,
|
||||
"skipTradeReason": "instruction_audit_only",
|
||||
"skipCandleReason": "instruction_audit_only"
|
||||
});
|
||||
}
|
||||
|
||||
fn candidate_raydium_audit_pool_account(
|
||||
protocol_name: &str,
|
||||
accounts_json: &str,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let spec = match protocol_name {
|
||||
"raydium_amm_v4" => RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_amm_v4",
|
||||
event_kind: "raydium_amm_v4.instruction_audit",
|
||||
candidate_pool_account_index: 1,
|
||||
},
|
||||
"raydium_clmm" => RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_clmm",
|
||||
event_kind: "raydium_clmm.instruction_audit",
|
||||
candidate_pool_account_index: 2,
|
||||
},
|
||||
"raydium_cpmm" => RaydiumInstructionAuditSpec {
|
||||
protocol_name: "raydium_cpmm",
|
||||
event_kind: "raydium_cpmm.instruction_audit",
|
||||
candidate_pool_account_index: 3,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
|
||||
let accounts = match accounts_result {
|
||||
Ok(accounts) => accounts,
|
||||
Err(_) => return None,
|
||||
};
|
||||
return accounts.get(spec.candidate_pool_account_index).cloned();
|
||||
}
|
||||
|
||||
fn parse_instruction_accounts_vec(accounts_json: &str) -> std::vec::Vec<std::string::String> {
|
||||
let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
|
||||
match accounts_result {
|
||||
Ok(accounts) => return accounts,
|
||||
Err(_) => return std::vec::Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instruction_accounts_value(accounts_json: &str) -> serde_json::Value {
|
||||
let accounts_result = serde_json::from_str::<serde_json::Value>(accounts_json);
|
||||
match accounts_result {
|
||||
Ok(accounts) => return accounts,
|
||||
Err(_) => return serde_json::Value::Array(std::vec::Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instruction_data_base58(
|
||||
data_json: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let data_json = match data_json {
|
||||
Some(data_json) => data_json,
|
||||
None => return None,
|
||||
};
|
||||
let data_result = serde_json::from_str::<std::string::String>(data_json);
|
||||
match data_result {
|
||||
Ok(data) => return Some(data),
|
||||
Err(_) => {
|
||||
if data_json.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
return Some(data_json.to_string());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn discriminator_hex_from_base58(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let data_base58 = match data_base58 {
|
||||
Some(data_base58) => data_base58,
|
||||
None => return None,
|
||||
};
|
||||
let bytes_result = bs58::decode(data_base58).into_vec();
|
||||
let bytes = match bytes_result {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return None,
|
||||
};
|
||||
if bytes.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
let mut text = std::string::String::new();
|
||||
for byte in bytes.iter().take(8) {
|
||||
text.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
fn append_persisted_events(
|
||||
target: &mut std::vec::Vec<crate::DexDecodedEventDto>,
|
||||
source: std::vec::Vec<crate::DexDecodedEventDto>,
|
||||
@@ -2073,6 +2682,22 @@ mod tests {
|
||||
crate::classify_dex_event_category_code("raydium_cpmm.initialize"),
|
||||
"pool_lifecycle"
|
||||
);
|
||||
assert_eq!(
|
||||
crate::classify_dex_event_category_code("raydium_clmm.instruction_audit"),
|
||||
"informational"
|
||||
);
|
||||
assert_eq!(
|
||||
crate::classify_dex_event_lifecycle_kind_code("raydium_clmm.instruction_audit"),
|
||||
"instruction_audit"
|
||||
);
|
||||
assert_eq!(
|
||||
crate::classify_dex_event_actionability_code(
|
||||
"raydium_clmm.instruction_audit",
|
||||
false,
|
||||
false
|
||||
),
|
||||
"informational"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2133,4 +2758,69 @@ mod tests {
|
||||
Some(&serde_json::Value::String("non_trade_event".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_observed_raydium_clmm_non_swap_discriminators() {
|
||||
let decrease = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_clmm",
|
||||
Some("3a7fbc3e4f52c460"),
|
||||
19,
|
||||
);
|
||||
let decrease = match decrease {
|
||||
Some(decrease) => decrease,
|
||||
None => panic!("decrease_liquidity_v2 discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(decrease.event_kind, "raydium_clmm.decrease_liquidity_v2");
|
||||
assert_eq!(decrease.pool_account_index, Some(3));
|
||||
assert_eq!(decrease.token_a_mint_index, Some(14));
|
||||
assert_eq!(decrease.token_b_mint_index, Some(15));
|
||||
|
||||
let increase = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_clmm",
|
||||
Some("851d59df45eeb00a"),
|
||||
15,
|
||||
);
|
||||
let increase = match increase {
|
||||
Some(increase) => increase,
|
||||
None => panic!("increase_liquidity_v2 discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(increase.event_kind, "raydium_clmm.increase_liquidity_v2");
|
||||
assert_eq!(increase.pool_account_index, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
|
||||
let collect_creator_fee = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("1416567bc61cdb84"),
|
||||
14,
|
||||
);
|
||||
let collect_creator_fee = match collect_creator_fee {
|
||||
Some(collect_creator_fee) => collect_creator_fee,
|
||||
None => panic!("collect_creator_fee discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(collect_creator_fee.event_kind, "raydium_cpmm.collect_creator_fee");
|
||||
|
||||
let withdraw = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("b712469c946da122"),
|
||||
14,
|
||||
);
|
||||
let withdraw = match withdraw {
|
||||
Some(withdraw) => withdraw,
|
||||
None => panic!("withdraw discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(withdraw.event_kind, "raydium_cpmm.withdraw");
|
||||
|
||||
let initialize = super::raydium_mapped_non_trade_instruction_spec(
|
||||
"raydium_cpmm",
|
||||
Some("afaf6d1f0d989bed"),
|
||||
20,
|
||||
);
|
||||
let initialize = match initialize {
|
||||
Some(initialize) => initialize,
|
||||
None => panic!("initialize discriminator must be mapped"),
|
||||
};
|
||||
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user