This commit is contained in:
2026-06-01 19:05:46 +02:00
parent abb810d544
commit 27e25d5bf4
59 changed files with 5727 additions and 1706 deletions

View File

@@ -1012,11 +1012,11 @@ impl DexDecodeService {
"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()),
decoded_event.pool_account().map(|value| return value.to_string()),
None,
decoded_event.base_mint().map(|value| return value.to_string()),
decoded_event.quote_mint().map(|value| return value.to_string()),
decoded_event.lp_mint().map(|value| return value.to_string()),
payload_value,
)
.await;
@@ -1174,6 +1174,7 @@ impl DexDecodeService {
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let mut persisted = std::vec::Vec::new();
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
@@ -1186,6 +1187,8 @@ impl DexDecodeService {
Some(data_json) => data_json,
None => continue,
};
let instruction_kind =
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
let decoded_events = crate::decode_raydium_cpmm_instruction(
instruction.accounts_json.as_str(),
data_json.as_str(),
@@ -1199,6 +1202,18 @@ impl DexDecodeService {
};
persisted.push(persisted_event);
}
let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event(
self,
transaction,
instruction,
instruction_kind,
&mut program_data_events,
&mut persisted,
)
.await;
if let Err(error) = program_data_persist_result {
return Err(error);
}
}
return Ok(persisted);
}
@@ -1808,6 +1823,11 @@ struct RaydiumMappedNonTradeInstructionSpec {
enum RaydiumMappedNonTradeAmountLayout {
None,
ClmmLiquidityV2,
CpmmAmmConfig,
CpmmDeposit,
CpmmFeePair,
CpmmInitialize,
CpmmPoolStatus,
CpmmWithdraw,
}
@@ -1894,26 +1914,81 @@ fn raydium_mapped_non_trade_instruction_spec(
}
}
if protocol_name == "raydium_cpmm" {
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 {
if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(3),
instruction_name: "close_permission_pda",
event_kind: "raydium_cpmm.close_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(3),
instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_fund_fee",
event_kind: "raydium_cpmm.collect_fund_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_protocol_fee",
event_kind: "raydium_cpmm.collect_protocol_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_amm_config",
event_kind: "raydium_cpmm.create_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_permission_pda",
event_kind: "raydium_cpmm.create_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "deposit",
event_kind: "raydium_cpmm.deposit",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit,
});
}
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
@@ -1923,8 +1998,52 @@ fn raydium_mapped_non_trade_instruction_spec(
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,
lp_mint_index: Some(6),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize_with_permission",
event_kind: "raydium_cpmm.initialize_with_permission",
pool_account_index: Some(4),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: Some(7),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_amm_config",
event_kind: "raydium_cpmm.update_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "82576c062ee0757b" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_pool_status",
event_kind: "raydium_cpmm.update_pool_status",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
});
}
}
@@ -1979,6 +2098,15 @@ fn enrich_raydium_mapped_non_trade_payload(
"instructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert(
"upstreamInstructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
object.insert(
"adminAction".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
object.insert(
"auditReason".to_string(),
@@ -2032,6 +2160,94 @@ fn insert_raydium_mapped_amounts(
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => {
if let Some(param) = read_u8_from_bytes(data, 8) {
object.insert(
"configParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(value) = read_u64_le_from_bytes(data, 9) {
object.insert(
"configValue".to_string(),
serde_json::Value::String(value.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmDeposit => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert(
"lpAmountRaw".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
object.insert(
"liquidity".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
}
if let Some(amount_0) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmFeePair => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
object.insert(
"amount0RequestedRaw".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
object.insert(
"amount1RequestedRaw".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmInitialize => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
object.insert(
"openTime".to_string(),
serde_json::Value::String(open_time.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => {
if let Some(status) = read_u8_from_bytes(data, 8) {
object.insert(
"poolStatus".to_string(),
serde_json::Value::Number(serde_json::Number::from(status as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert(
@@ -2073,6 +2289,13 @@ fn instruction_data_bytes_from_base58(
}
}
fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
if data.len() < offset + 1 {
return None;
}
return Some(data[offset]);
}
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 {
return None;
@@ -2453,6 +2676,150 @@ fn append_persisted_events(
}
}
#[derive(Clone, Debug)]
struct RaydiumCpmmProgramDataEventCandidate {
decoded_event: crate::RaydiumCpmmDecodedEvent,
consumed: bool,
}
fn collect_raydium_cpmm_program_data_events(
transaction: &crate::ChainTransactionDto,
) -> std::vec::Vec<RaydiumCpmmProgramDataEventCandidate> {
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
let mut events = std::vec::Vec::new();
let mut cpmm_stack_depth = 0_u32;
for log_message in logs {
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth += 1;
continue;
}
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1);
continue;
}
if cpmm_stack_depth == 0 {
continue;
}
let data_base64 = match log_message.strip_prefix("Program data: ") {
Some(data_base64) => data_base64.trim(),
None => continue,
};
if data_base64.is_empty() {
continue;
}
let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64);
if let Some(decoded_event) = decoded_event {
events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false });
}
}
return events;
}
async fn persist_matching_raydium_cpmm_program_data_event(
service: &DexDecodeService,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
instruction_kind: std::option::Option<&str>,
program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate],
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
) -> Result<(), crate::Error> {
let expected_event_kind = match instruction_kind {
Some("swap_base_input") => Some("swap_event"),
Some("swap_base_output") => Some("swap_event"),
Some("deposit") => Some("lp_change_event"),
Some("withdraw") => Some("lp_change_event"),
_ => None,
};
let expected_event_kind = match expected_event_kind {
Some(expected_event_kind) => expected_event_kind,
None => return Ok(()),
};
let mut index = 0_usize;
while index < program_data_events.len() {
if program_data_events[index].consumed {
index += 1;
continue;
}
let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) {
(crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true,
(crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true,
_ => false,
};
if !event_matches {
index += 1;
continue;
}
program_data_events[index].consumed = true;
let persist_result = service
.persist_raydium_cpmm_event(
transaction,
instruction,
&program_data_events[index].decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
return Ok(());
}
return Ok(());
}
fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec<std::string::String> {
let value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
let value = match value_result {
Ok(value) => value,
Err(_) => return std::vec::Vec::new(),
};
let meta = match value.get("meta") {
Some(meta) => meta,
None => return std::vec::Vec::new(),
};
let logs = match meta.get("logMessages") {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let logs = match logs.as_array() {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for log in logs {
if let Some(log) = log.as_str() {
output.push(log.to_string());
}
}
return output;
}
fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(" invoke [") {
return false;
}
return log_message.contains(program_id);
}
fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(program_id) {
return false;
}
if log_message.ends_with(" success") {
return true;
}
if log_message.contains(" failed: ") {
return true;
}
return false;
}
fn decoded_instruction_ids_from_persisted_events(
persisted: &[crate::DexDecodedEventDto],
) -> std::collections::HashSet<i64> {
@@ -2603,7 +2970,7 @@ mod tests {
"instructions": [
{
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium-amm-v4",
"program": "raydium_amm_v4",
"stackHeight": 1,
"accounts": [
"Account0",
@@ -2887,7 +3254,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc",
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcPoolDecode111",
@@ -2962,7 +3329,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2",
"program": "meteora_damm_v2",
"stackHeight": 1,
"accounts": [
"DammV2DecodePool111",
@@ -3039,7 +3406,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora-damm-v1",
"program": "meteora_damm_v1",
"stackHeight": 1,
"accounts": [
"DammV1DecodePool111",
@@ -3116,7 +3483,7 @@ mod tests {
"instructions": [
{
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca-whirlpools",
"program": "orca_whirlpools",
"stackHeight": 1,
"accounts": [
"OrcaDecodePool111",
@@ -3488,36 +3855,32 @@ mod tests {
#[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");
let expected = [
("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
];
for (discriminator, account_count, event_kind) in expected {
let mapped = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some(discriminator),
account_count,
);
let mapped = match mapped {
Some(mapped) => mapped,
None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
};
assert_eq!(mapped.event_kind, event_kind);
}
}
#[test]
@@ -3573,7 +3936,7 @@ mod tests {
let registry_match = crate::UpstreamRegistryEntryDto {
source_repo: Some("sevenlabs-hq/carbon".to_string()),
source_path: Some("decoders/example.rs".to_string()),
decoder_code: "meteora-damm-v2".to_string(),
decoder_code: "meteora_damm_v2".to_string(),
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
program_family: "meteora".to_string(),
surface_kind: "amm".to_string(),