This commit is contained in:
2026-06-15 20:16:27 +02:00
parent 3b908b318e
commit 045af4931c
44 changed files with 5328 additions and 113 deletions

View File

@@ -62,6 +62,7 @@ pub use phoenix_v1::PhoenixV1AuditDecoded;
pub use phoenix_v1::PhoenixV1DecodedEvent;
pub use phoenix_v1::PhoenixV1Decoder;
pub use pump_fun::PumpFunCreateV2TokenDecoded;
pub use pump_fun::PumpFunInstructionAuditDecoded;
pub use pump_fun::PumpFunDecodedEvent;
pub use pump_fun::PumpFunDecoder;
pub use pump_fun::PumpFunTradeDecoded;

File diff suppressed because it is too large Load Diff

View File

@@ -1627,9 +1627,85 @@ impl DexDecodeService {
)
.await;
},
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
let pool_account = Self::pump_fun_payload_string(
&event.payload_json,
&["poolAccount", "bondingCurve", "bonding_curve", "pool"],
);
let token_a_mint = Self::pump_fun_payload_string(
&event.payload_json,
&["tokenAMint", "token_a_mint", "tokenMint", "token_mint", "mint"],
);
let token_b_mint = match Self::pump_fun_payload_string(
&event.payload_json,
&["tokenBMint", "token_b_mint", "quoteMint", "quote_mint"],
) {
Some(token_b_mint) => Some(token_b_mint),
None => {
if token_a_mint.is_some() {
Some(crate::WSOL_MINT_ID.to_string())
} else {
None
}
},
};
let lp_mint = Self::pump_fun_payload_string(
&event.payload_json,
&[
"lpMint",
"lp_mint",
"associatedBondingCurve",
"associated_bonding_curve",
"poolBaseTokenAccount",
],
);
return self
.materialize_named_dex_event(
transaction,
event.transaction_id,
event.instruction_id,
"pump_fun",
event.program_id.clone(),
event.event_kind.as_str(),
pool_account,
None,
token_a_mint,
token_b_mint,
lp_mint,
event.payload_json.clone(),
)
.await;
},
}
}
fn pump_fun_payload_string(
payload: &serde_json::Value,
keys: &[&str],
) -> std::option::Option<std::string::String> {
if let Some(object) = payload.as_object() {
for key in keys {
let value = object.get(*key);
if let Some(value) = value {
if let Some(text) = value.as_str() {
if !text.trim().is_empty() {
return Some(text.to_string());
}
}
}
}
let decoded_arguments = object.get("decodedArguments");
if let Some(decoded_arguments) = decoded_arguments {
let nested = Self::pump_fun_payload_string(decoded_arguments, keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
async fn persist_pump_fun_trade_event(
&self,
transaction: &crate::ChainTransactionDto,
@@ -2266,8 +2342,23 @@ impl DexDecodeService {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let decoded_events = pump_fun_enrich_trade_events_with_instruction_context(decoded_events);
let mut persisted = std::vec::Vec::new();
for decoded_event in &decoded_events {
if !pump_fun_decoded_event_is_trade_event(decoded_event) {
continue;
}
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);
}
for decoded_event in &decoded_events {
if pump_fun_decoded_event_is_trade_event(decoded_event) {
continue;
}
let persist_result = self.persist_pump_fun_event(transaction, decoded_event).await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
@@ -4313,6 +4404,310 @@ fn dex_decode_extract_first_amount_string(
return dex_decode_extract_first_number_as_string(value, candidate_keys);
}
fn pump_fun_enrich_trade_events_with_instruction_context(
decoded_events: std::vec::Vec<crate::PumpFunDecodedEvent>,
) -> std::vec::Vec<crate::PumpFunDecodedEvent> {
let mut enriched_events = std::vec::Vec::new();
for decoded_event in &decoded_events {
let enriched_event = match decoded_event {
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
if event.event_kind.as_str() == "pump_fun.trade_event" {
let mut enriched_event = event.clone();
pump_fun_merge_matching_instruction_context_into_trade_event(
&decoded_events,
&mut enriched_event,
);
pump_fun_mark_trade_event_duplicate_when_direct_instruction_exists(
&decoded_events,
&mut enriched_event,
);
crate::PumpFunDecodedEvent::InstructionAudit(enriched_event)
} else {
decoded_event.clone()
}
},
_ => decoded_event.clone(),
};
enriched_events.push(enriched_event);
}
return enriched_events;
}
fn pump_fun_merge_matching_instruction_context_into_trade_event(
decoded_events: &[crate::PumpFunDecodedEvent],
trade_event: &mut crate::PumpFunInstructionAuditDecoded,
) {
let trade_payload = trade_event.payload_json.clone();
let trade_instruction_id = dex_decode_extract_first_i64(
&trade_payload,
&["instructionId", "instruction_id"],
);
let trade_mint = dex_decode_extract_first_string(
&trade_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_actor = dex_decode_extract_first_string(
&trade_payload,
&["user", "actorWallet", "userWallet"],
);
for sibling in decoded_events {
let sibling_event = match sibling {
crate::PumpFunDecodedEvent::InstructionAudit(sibling_event) => sibling_event,
_ => continue,
};
if sibling_event.event_kind.as_str() == "pump_fun.trade_event" {
continue;
}
if !pump_fun_instruction_context_can_back_trade_event(sibling_event.event_kind.as_str()) {
continue;
}
let sibling_instruction_id = Some(sibling_event.instruction_id);
if trade_instruction_id.is_some()
&& sibling_instruction_id.is_some()
&& trade_instruction_id != sibling_instruction_id
{
continue;
}
let sibling_mint = dex_decode_extract_first_string(
&sibling_event.payload_json,
&["mint", "tokenMint", "tokenAMint"],
);
if !dex_decode_optional_strings_match(trade_mint.as_deref(), sibling_mint.as_deref()) {
continue;
}
let sibling_actor = dex_decode_extract_first_string(
&sibling_event.payload_json,
&["user", "actorWallet", "userWallet"],
);
if !dex_decode_optional_strings_match(trade_actor.as_deref(), sibling_actor.as_deref()) {
continue;
}
pump_fun_merge_instruction_context_payload(
&sibling_event.payload_json,
&mut trade_event.payload_json,
);
return;
}
}
fn pump_fun_instruction_context_can_back_trade_event(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy_exact_quote_in_v2" => return true,
"pump_fun.buy_v2" => return true,
"pump_fun.sell_v2" => return true,
"pump_fun.buy_exact_sol_in" => return true,
_ => return false,
}
}
fn pump_fun_mark_trade_event_duplicate_when_direct_instruction_exists(
decoded_events: &[crate::PumpFunDecodedEvent],
trade_event: &mut crate::PumpFunInstructionAuditDecoded,
) {
let trade_payload = trade_event.payload_json.clone();
let trade_instruction_id = dex_decode_extract_first_i64(
&trade_payload,
&["instructionId", "instruction_id"],
);
let trade_mint = dex_decode_extract_first_string(
&trade_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_actor = dex_decode_extract_first_string(
&trade_payload,
&["user", "actorWallet", "userWallet"],
);
for sibling in decoded_events {
let direct_match = match sibling {
crate::PumpFunDecodedEvent::BuyTrade(event) => {
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
event.mint.as_deref(),
event.user.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
},
crate::PumpFunDecodedEvent::SellTrade(event) => {
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
event.mint.as_deref(),
event.user.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
},
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
if event.event_kind.as_str() != "pump_fun.buy_exact_sol_in" {
false
} else {
let instruction_mint = dex_decode_extract_first_string(
&event.payload_json,
&["mint", "tokenMint", "tokenAMint"],
);
let instruction_actor = dex_decode_extract_first_string(
&event.payload_json,
&["user", "actorWallet", "userWallet"],
);
pump_fun_direct_trade_matches_anchor_trade_event(
event.instruction_id,
instruction_mint.as_deref(),
instruction_actor.as_deref(),
trade_instruction_id,
trade_mint.as_deref(),
trade_actor.as_deref(),
)
}
},
_ => false,
};
if !direct_match {
continue;
}
let object = match trade_event.payload_json.as_object_mut() {
Some(object) => object,
None => return,
};
object.insert(
"skipTradeReason".to_string(),
serde_json::Value::String(
"pump_fun_trade_event_covered_by_direct_instruction_trade".to_string(),
),
);
object.insert(
"skipCandleReason".to_string(),
serde_json::Value::String(
"pump_fun_trade_event_covered_by_direct_instruction_trade".to_string(),
),
);
object.insert(
"anchorTradeEventCoveredByDirectInstructionTrade".to_string(),
serde_json::Value::Bool(true),
);
return;
}
}
fn pump_fun_direct_trade_matches_anchor_trade_event(
direct_instruction_id: i64,
direct_mint: std::option::Option<&str>,
direct_actor: std::option::Option<&str>,
trade_instruction_id: std::option::Option<i64>,
trade_mint: std::option::Option<&str>,
trade_actor: std::option::Option<&str>,
) -> bool {
if let Some(trade_instruction_id) = trade_instruction_id {
if direct_instruction_id != trade_instruction_id {
return false;
}
}
if !dex_decode_optional_strings_match(trade_mint, direct_mint) {
return false;
}
if !dex_decode_optional_strings_match(trade_actor, direct_actor) {
return false;
}
return true;
}
fn pump_fun_merge_instruction_context_payload(
instruction_payload: &serde_json::Value,
trade_payload: &mut serde_json::Value,
) {
let trade_object = match trade_payload.as_object_mut() {
Some(trade_object) => trade_object,
None => return,
};
let instruction_object = match instruction_payload.as_object() {
Some(instruction_object) => instruction_object,
None => return,
};
let copy_keys = [
"poolAccount",
"bondingCurve",
"associatedBondingCurve",
"poolBaseTokenAccount",
"poolQuoteTokenAccount",
"associatedQuoteBondingCurve",
"lpMint",
"tokenAMint",
"tokenBMint",
"quoteMint",
"feeRecipient",
"creatorVault",
"associatedCreatorVault",
];
for key in copy_keys {
if trade_object.contains_key(key) {
continue;
}
let value = match instruction_object.get(key) {
Some(value) => value.clone(),
None => continue,
};
trade_object.insert(key.to_string(), value);
}
trade_object.insert(
"amountSource".to_string(),
serde_json::Value::String("pump_fun_anchor_trade_event".to_string()),
);
trade_object.insert(
"anchorTradeEventInstructionContextSource".to_string(),
serde_json::Value::String("matching_instruction_audit".to_string()),
);
trade_object.remove("skipTradeReason");
trade_object.remove("skipCandleReason");
trade_object.remove("skipCatalogReason");
}
fn dex_decode_extract_first_i64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<i64> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = match object.get(*candidate_key) {
Some(candidate_value) => candidate_value,
None => continue,
};
if let Some(number) = candidate_value.as_i64() {
return Some(number);
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<i64>();
match parsed {
Ok(parsed) => return Some(parsed),
Err(_) => {},
}
}
}
}
return None;
}
fn dex_decode_optional_strings_match(
left: std::option::Option<&str>,
right: std::option::Option<&str>,
) -> bool {
match (left, right) {
(Some(left), Some(right)) => return left == right,
_ => return true,
}
}
fn pump_fun_decoded_event_is_trade_event(decoded_event: &crate::PumpFunDecodedEvent) -> bool {
match decoded_event {
crate::PumpFunDecodedEvent::InstructionAudit(event) => {
return event.event_kind.as_str() == "pump_fun.trade_event";
},
_ => return false,
}
}
fn dex_decode_extract_first_string(
value: &serde_json::Value,
candidate_keys: &[&str],

View File

@@ -119,6 +119,21 @@ pub(crate) fn dex_detection_route(
("pump_fun", "pump_fun.sell") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_exact_sol_in") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_exact_quote_in_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.buy_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.sell_v2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_fun", "pump_fun.trade_event") => {
return Some(crate::dex_detection_route::DexDetectionRoute::PumpFunTrade);
},
("pump_swap", "pump_swap.buy") => {
if crate::dex_detection_route::is_incomplete_pump_swap_decoded_event(decoded_event) {
return Some(

View File

@@ -323,6 +323,15 @@ pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_launchpad.trade_event" {
return true;
}
if event_kind == "pump_fun.trade_event" {
return true;
}
if event_kind.ends_with(".buy_v2") {
return true;
}
if event_kind.ends_with(".sell_v2") {
return true;
}
if event_kind.ends_with(".buy") {
return true;
}
@@ -520,6 +529,15 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.contains("partner_claim_fee") {
return true;
}
if event_kind.contains("distribute_creator_fees") {
return true;
}
if event_kind.contains("minimum_distributable_fee") {
return true;
}
if event_kind.contains("reserved_fee_recipients_event") {
return false;
}
return false;
}
@@ -828,6 +846,15 @@ pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".extend_account") {
return true;
}
if event_kind.contains(".add_quote_mint") {
return true;
}
if event_kind.contains(".remove_quote_mint") {
return true;
}
if event_kind.contains("reserved_fee_recipients") {
return true;
}
if event_kind.contains("update_") {
return true;
}
@@ -1165,6 +1192,9 @@ mod tests {
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.sell_v2"), "trade");
assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "trade");
assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
assert!(super::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
}

View File

@@ -318,6 +318,9 @@ fn infer_expected_db_target_for_entry(
if decoder_code == "pump_swap" {
return infer_pump_swap_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "pump_fun" {
return infer_pump_fun_expected_db_target(entry_name, entry_kind);
}
if decoder_code == "raydium_cpmm"
&& (entry_name == "swap_event" || entry_name == "anchor_idl_instruction")
{
@@ -524,6 +527,104 @@ fn infer_expected_db_target(
return Some(target.to_string());
}
fn infer_pump_fun_expected_db_target(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
if entry_name == "buy"
|| entry_name == "sell"
|| entry_name == "buy_v2"
|| entry_name == "sell_v2"
|| entry_name == "buy_exact_sol_in"
|| entry_name == "buy_exact_quote_in_v2"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if entry_name == "trade_event" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string());
}
if entry_name == "create"
|| entry_name == "create_event"
|| entry_name == "create_v2"
|| entry_name == "create_v2_token"
|| entry_name == "complete_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string());
}
if entry_name == "initialize" {
return Some(
crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS.to_string(),
);
}
if entry_name == "migrate"
|| entry_name == "migrate_v2"
|| entry_name == "migrate_bonding_curve_creator"
|| entry_name == "migrate_bonding_curve_creator_event"
|| entry_name == "complete_pump_amm_migration_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string());
}
if entry_name == "collect_creator_fee"
|| entry_name == "collect_creator_fee_v2"
|| entry_name == "collect_creator_fee_event"
|| entry_name == "distribute_creator_fees"
|| entry_name == "distribute_creator_fees_v2"
|| entry_name == "distribute_creator_fees_event"
|| entry_name == "get_minimum_distributable_fee"
|| entry_name == "minimum_distributable_fee_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS.to_string());
}
if entry_name == "claim_cashback"
|| entry_name == "claim_cashback_v2"
|| entry_name == "claim_cashback_event"
|| entry_name == "claim_token_incentives"
|| entry_name == "claim_token_incentives_event"
|| entry_name == "admin_update_token_incentives"
|| entry_name == "admin_update_token_incentives_event"
|| entry_name == "init_user_volume_accumulator"
|| entry_name == "init_user_volume_accumulator_event"
|| entry_name == "sync_user_volume_accumulator"
|| entry_name == "sync_user_volume_accumulator_event"
|| entry_name == "close_user_volume_accumulator"
|| entry_name == "close_user_volume_accumulator_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS.to_string());
}
if entry_name == "admin_set_creator"
|| entry_name == "admin_set_creator_event"
|| entry_name == "admin_set_idl_authority"
|| entry_name == "admin_set_idl_authority_event"
|| entry_name == "add_quote_mint"
|| entry_name == "remove_quote_mint"
|| entry_name == "extend_account"
|| entry_name == "extend_account_event"
|| entry_name == "set_creator"
|| entry_name == "set_creator_event"
|| entry_name == "set_mayhem_virtual_params"
|| entry_name == "update_mayhem_virtual_params_event"
|| entry_name == "set_metaplex_creator"
|| entry_name == "set_metaplex_creator_event"
|| entry_name == "set_params"
|| entry_name == "set_params_event"
|| entry_name == "set_reserved_fee_recipients"
|| entry_name == "reserved_fee_recipients_event"
|| entry_name == "set_virtual_quote_reserves"
|| entry_name == "toggle_cashback_enabled"
|| entry_name == "toggle_create_v2"
|| entry_name == "toggle_mayhem_mode"
|| entry_name == "update_buyback_config"
|| entry_name == "update_global_authority"
|| entry_name == "update_global_authority_event"
{
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS.to_string());
}
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
fn infer_pump_swap_expected_db_target(
entry_name: &str,
entry_kind: &str,
@@ -654,6 +755,161 @@ fn infer_pump_swap_event_family(
return infer_event_family(entry_name, entry_kind);
}
fn infer_pump_fun_event_family(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if entry_kind == crate::ENTRY_KIND_PROGRAM {
return None;
}
match entry_name {
"buy"
| "sell"
| "buy_v2"
| "sell_v2"
| "buy_exact_quote_in_v2"
| "buy_exact_sol_in"
| "trade_event" => return Some("swap".to_string()),
"create" | "create_event" | "create_v2" | "create_v2_token" | "complete_event" => {
return Some("launch".to_string());
},
"migrate"
| "migrate_v2"
| "migrate_bonding_curve_creator"
| "migrate_bonding_curve_creator_event"
| "complete_pump_amm_migration_event" => return Some("migration".to_string()),
"claim_cashback"
| "claim_cashback_v2"
| "claim_cashback_event"
| "claim_token_incentives"
| "claim_token_incentives_event"
| "admin_update_token_incentives"
| "admin_update_token_incentives_event"
| "init_user_volume_accumulator"
| "init_user_volume_accumulator_event"
| "sync_user_volume_accumulator"
| "sync_user_volume_accumulator_event"
| "close_user_volume_accumulator"
| "close_user_volume_accumulator_event" => return Some("reward".to_string()),
"collect_creator_fee"
| "collect_creator_fee_v2"
| "collect_creator_fee_event"
| "distribute_creator_fees"
| "distribute_creator_fees_v2"
| "distribute_creator_fees_event"
| "get_minimum_distributable_fee"
| "minimum_distributable_fee_event" => return Some("fee".to_string()),
"add_quote_mint"
| "remove_quote_mint"
| "admin_set_creator"
| "admin_set_creator_event"
| "admin_set_idl_authority"
| "admin_set_idl_authority_event"
| "extend_account"
| "extend_account_event"
| "set_creator"
| "set_creator_event"
| "set_mayhem_virtual_params"
| "update_mayhem_virtual_params_event"
| "set_metaplex_creator"
| "set_metaplex_creator_event"
| "set_params"
| "set_params_event"
| "set_reserved_fee_recipients"
| "reserved_fee_recipients_event"
| "set_virtual_quote_reserves"
| "toggle_cashback_enabled"
| "toggle_create_v2"
| "toggle_mayhem_mode"
| "update_buyback_config"
| "update_global_authority"
| "update_global_authority_event" => return Some("admin_config".to_string()),
"initialize" => return Some("pool_create".to_string()),
_ => return infer_event_family(entry_name, entry_kind),
}
}
fn pump_fun_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
if entry_name.ends_with("_event") {
return Some(format!("pump_fun.{}", entry_name));
}
match entry_name {
"buy" => return Some("pump_fun.buy".to_string()),
"sell" => return Some("pump_fun.sell".to_string()),
"create_v2" => return Some("pump_fun.create_v2_token".to_string()),
"add_quote_mint" => return Some("pump_fun.add_quote_mint".to_string()),
"admin_set_creator" => return Some("pump_fun.admin_set_creator".to_string()),
"admin_set_idl_authority" => {
return Some("pump_fun.admin_set_idl_authority".to_string());
},
"admin_update_token_incentives" => {
return Some("pump_fun.admin_update_token_incentives".to_string());
},
"buy_exact_quote_in_v2" => {
return Some("pump_fun.buy_exact_quote_in_v2".to_string());
},
"buy_exact_sol_in" => return Some("pump_fun.buy_exact_sol_in".to_string()),
"buy_v2" => return Some("pump_fun.buy_v2".to_string()),
"claim_cashback" => return Some("pump_fun.claim_cashback".to_string()),
"claim_cashback_v2" => return Some("pump_fun.claim_cashback_v2".to_string()),
"claim_token_incentives" => {
return Some("pump_fun.claim_token_incentives".to_string());
},
"close_user_volume_accumulator" => {
return Some("pump_fun.close_user_volume_accumulator".to_string());
},
"collect_creator_fee" => return Some("pump_fun.collect_creator_fee".to_string()),
"collect_creator_fee_v2" => return Some("pump_fun.collect_creator_fee_v2".to_string()),
"create" => return Some("pump_fun.create".to_string()),
"distribute_creator_fees" => {
return Some("pump_fun.distribute_creator_fees".to_string());
},
"distribute_creator_fees_v2" => {
return Some("pump_fun.distribute_creator_fees_v2".to_string());
},
"extend_account" => return Some("pump_fun.extend_account".to_string()),
"get_minimum_distributable_fee" => {
return Some("pump_fun.get_minimum_distributable_fee".to_string());
},
"init_user_volume_accumulator" => {
return Some("pump_fun.init_user_volume_accumulator".to_string());
},
"initialize" => return Some("pump_fun.initialize".to_string()),
"migrate" => return Some("pump_fun.migrate".to_string()),
"migrate_bonding_curve_creator" => {
return Some("pump_fun.migrate_bonding_curve_creator".to_string());
},
"migrate_v2" => return Some("pump_fun.migrate_v2".to_string()),
"remove_quote_mint" => return Some("pump_fun.remove_quote_mint".to_string()),
"sell_v2" => return Some("pump_fun.sell_v2".to_string()),
"set_creator" => return Some("pump_fun.set_creator".to_string()),
"set_mayhem_virtual_params" => {
return Some("pump_fun.set_mayhem_virtual_params".to_string());
},
"set_metaplex_creator" => return Some("pump_fun.set_metaplex_creator".to_string()),
"set_params" => return Some("pump_fun.set_params".to_string()),
"set_reserved_fee_recipients" => {
return Some("pump_fun.set_reserved_fee_recipients".to_string());
},
"set_virtual_quote_reserves" => {
return Some("pump_fun.set_virtual_quote_reserves".to_string());
},
"sync_user_volume_accumulator" => {
return Some("pump_fun.sync_user_volume_accumulator".to_string());
},
"toggle_cashback_enabled" => {
return Some("pump_fun.toggle_cashback_enabled".to_string());
},
"toggle_create_v2" => return Some("pump_fun.toggle_create_v2".to_string()),
"toggle_mayhem_mode" => return Some("pump_fun.toggle_mayhem_mode".to_string()),
"update_buyback_config" => return Some("pump_fun.update_buyback_config".to_string()),
"update_global_authority" => {
return Some("pump_fun.update_global_authority".to_string());
},
_ => return None,
}
}
fn pump_swap_local_event_kind(entry_name: &str) -> std::option::Option<std::string::String> {
if entry_name.ends_with("_event") {
return Some(format!("pump_swap.{}", entry_name));
@@ -716,6 +972,9 @@ fn infer_event_family_for_entry(
entry_name: &str,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "pump_fun" {
return infer_pump_fun_event_family(entry_name, entry_kind);
}
if decoder_code == "pump_swap" {
return infer_pump_swap_event_family(entry_name, entry_kind);
}
@@ -1110,6 +1369,9 @@ pub(crate) fn known_local_event_kind(
decoder_code: &str,
entry_name: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "pump_fun" {
return pump_fun_local_event_kind(entry_name);
}
if decoder_code == "pump_swap" {
return pump_swap_local_event_kind(entry_name);
}
@@ -1488,6 +1750,85 @@ mod tests {
Some("raydium_clmm.pool_created_event".to_string())
);
}
#[test]
fn pump_fun_coverage_maps_local_idl_and_audit_entries() {
assert_eq!(
super::known_local_event_kind("pump_fun", "buy"),
Some("pump_fun.buy".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "create_v2"),
Some("pump_fun.create_v2_token".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "buy_v2"),
Some("pump_fun.buy_v2".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "collect_creator_fee_v2"),
Some("pump_fun.collect_creator_fee_v2".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "trade_event"),
Some("pump_fun.trade_event".to_string())
);
assert_eq!(
super::known_local_event_kind("pump_fun", "claim_cashback_event"),
Some("pump_fun.claim_cashback_event".to_string())
);
assert_eq!(
super::infer_event_family_for_entry("pump_fun", "create_event", crate::ENTRY_KIND_EVENT),
Some("launch".to_string())
);
assert_eq!(
super::infer_event_family_for_entry(
"pump_fun",
"set_metaplex_creator_event",
crate::ENTRY_KIND_EVENT,
),
Some("admin_config".to_string())
);
assert_eq!(
super::infer_event_family_for_entry(
"pump_fun",
"claim_token_incentives_event",
crate::ENTRY_KIND_EVENT,
),
Some("reward".to_string())
);
assert_eq!(
super::infer_event_family_for_entry("pump_fun", "buy_v2", crate::ENTRY_KIND_INSTRUCTION),
Some("swap".to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"buy",
Some("swap"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"buy_v2",
Some("swap"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS.to_string())
);
assert_eq!(
super::infer_expected_db_target_for_entry(
"pump_fun",
"create_v2",
Some("launch"),
crate::ENTRY_KIND_INSTRUCTION,
),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_LAUNCH_EVENTS.to_string())
);
}
#[test]
fn launchpad_swap_instructions_materialize_as_launch_events_without_duplicate_trades() {
assert_eq!(

View File

@@ -319,6 +319,53 @@ fn resolve_instruction_name(
};
return Some(format!("raydium_launchpad.{}", layout.instruction_name));
}
if program_id == crate::PUMP_FUN_PROGRAM_ID || decoder_code == Some("pump_fun") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "anchor_self_cpi_log",
"6f79153828185ed1" => "add_quote_mint",
"4519ab8e39ef0d04" => "admin_set_creator",
"08d960e79068c005" => "admin_set_idl_authority",
"d10b7357d5177ccc" => "admin_update_token_incentives",
"66063d1201daebea" => "buy",
"c2ab1c46684d5b2f" => "buy_exact_quote_in_v2",
"38fc74089edfcd5f" => "buy_exact_sol_in",
"b817ee6167c5d33d" => "buy_v2",
"253a237ebe35e4c5" => "claim_cashback",
"7af3cc415e741d37" => "claim_cashback_v2",
"1004471ccc01281b" => "claim_token_incentives",
"f945a4da9667548a" => "close_user_volume_accumulator",
"1416567bc61cdb84" => "collect_creator_fee",
"cf118af204221338" => "collect_creator_fee_v2",
"181ec828051c0777" => "create",
"d6904cec5f8b31b4" => "create_v2",
"a572670079cef751" => "distribute_creator_fees",
"ffcb134ff444089f" => "distribute_creator_fees_v2",
"ea66c2cb96483ee5" => "extend_account",
"75e17fca865f4423" => "get_minimum_distributable_fee",
"5e06ca73ff60e8b7" => "init_user_volume_accumulator",
"afaf6d1f0d989bed" => "initialize",
"9beae792ec9ea21e" => "migrate",
"577c34bf3426d6e8" => "migrate_bonding_curve_creator",
"bbcb121fceedfe29" => "migrate_v2",
"b141df2658d19e9b" => "remove_quote_mint",
"33e685a4017f83ad" => "sell",
"5df6823ce7e940b2" => "sell_v2",
"fe94ff70cf8eaaa5" => "set_creator",
"3da9bcbf99952a61" => "set_mayhem_virtual_params",
"8a60aed93055c5f6" => "set_metaplex_creator",
"1beab2349302bb8d" => "set_params",
"6faca2e87259d58e" => "set_reserved_fee_recipients",
"6587bf6809581460" => "set_virtual_quote_reserves",
"561fc057a3574fee" => "sync_user_volume_accumulator",
"7367e0ffbd5956c3" => "toggle_cashback_enabled",
"1cffe6f0ac6bcbab" => "toggle_create_v2",
"01096fd0641fffa3" => "toggle_mayhem_mode",
"fbe0ab92a01a71e9" => "update_buyback_config",
"e3b54ac4d01561d5" => "update_global_authority",
_ => return None,
};
return Some(name.to_string());
}
if program_id == crate::PUMP_SWAP_PROGRAM_ID || decoder_code == Some("pump_swap") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "anchor_self_cpi_log",

View File

@@ -1177,6 +1177,8 @@ pub use dex::PumpFunCreateV2TokenDecoded;
pub use dex::PumpFunDecodedEvent;
/// Pump.fun decoder.
pub use dex::PumpFunDecoder;
/// Decoded Pump.fun audit-only instruction event.
pub use dex::PumpFunInstructionAuditDecoded;
/// Decoded Pump.fun bonding-curve trade event.
pub use dex::PumpFunTradeDecoded;
/// Decoded PumpSwap event.

View File

@@ -112,6 +112,15 @@ impl NonTradeEventMaterializationService {
if is_anchor_event_audit_only(&payload) {
continue;
}
if should_skip_pump_fun_duplicate_non_trade_event(decoded_event, &decoded_events) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
decoded_event_id = ?decoded_event.id,
signature = %transaction.signature,
"skipping duplicate pump_fun non-trade materialization"
);
continue;
}
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
let cleanup_result =
self.delete_stale_pool_admin_event_for_lifecycle(decoded_event).await;
@@ -140,7 +149,9 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
}
}
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str())
&& !is_launchpad_launch_event_materializable(decoded_event.event_kind.as_str())
{
let materialized = self
.materialize_pool_lifecycle_event(&transaction, transaction_id, decoded_event)
.await;
@@ -672,6 +683,10 @@ impl NonTradeEventMaterializationService {
"poolState",
"pool_state",
"poolAccount",
"bondingCurve",
"bonding_curve",
"sharingConfig",
"sharing_config",
],
);
let related_mint = extract_first_string(
@@ -737,9 +752,8 @@ impl NonTradeEventMaterializationService {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(false),
};
let context = self
.resolve_liquidity_context(transaction, transaction_id, decoded_event)
.await;
let context =
self.resolve_liquidity_context(transaction, transaction_id, decoded_event).await;
let context = match context {
Ok(context) => context,
Err(error) => return Err(error),
@@ -1018,9 +1032,8 @@ impl NonTradeEventMaterializationService {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
let payload_result = serde_json::from_str::<serde_json::Value>(
decoded_event.payload_json.as_str(),
);
let payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let mut object = match payload_result {
Ok(serde_json::Value::Object(object)) => object,
Ok(other) => {
@@ -1179,9 +1192,8 @@ impl NonTradeEventMaterializationService {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let target_payload_result = serde_json::from_str::<serde_json::Value>(
decoded_event.payload_json.as_str(),
);
let target_payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let target_payload = match target_payload_result {
Ok(target_payload) => target_payload,
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
@@ -1193,9 +1205,8 @@ impl NonTradeEventMaterializationService {
if !candidate.event_kind.starts_with("raydium_clmm.") {
continue;
}
let candidate_payload_result = serde_json::from_str::<serde_json::Value>(
candidate.payload_json.as_str(),
);
let candidate_payload_result =
serde_json::from_str::<serde_json::Value>(candidate.payload_json.as_str());
let candidate_payload = match candidate_payload_result {
Ok(candidate_payload) => candidate_payload,
Err(_) => serde_json::Value::Object(serde_json::Map::new()),
@@ -1425,9 +1436,8 @@ struct MaterializationAccountKeyInfo {
fn token_mints_by_account_from_transaction(
transaction: &crate::ChainTransactionDto,
) -> std::collections::HashMap<std::string::String, std::string::String> {
let transaction_json = serde_json::from_str::<serde_json::Value>(
transaction.transaction_json.as_str(),
);
let transaction_json =
serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
let transaction_json = match transaction_json {
Ok(transaction_json) => transaction_json,
Err(_) => return std::collections::HashMap::new(),
@@ -1475,10 +1485,7 @@ fn materialization_account_keys(
value.get("pubkey").and_then(serde_json::Value::as_str).map(str::to_string)
};
if let Some(address) = address {
account_keys.push(MaterializationAccountKeyInfo {
index: index as i64,
address,
});
account_keys.push(MaterializationAccountKeyInfo { index: index as i64, address });
}
index += 1;
}
@@ -1507,10 +1514,7 @@ fn append_materialization_loaded_addresses(
None => continue,
};
let index = account_keys.len() as i64;
account_keys.push(MaterializationAccountKeyInfo {
index,
address: address.to_string(),
});
account_keys.push(MaterializationAccountKeyInfo { index, address: address.to_string() });
}
}
@@ -1567,21 +1571,15 @@ fn infer_raydium_clmm_pair_mints_from_payload_accounts(
Some(accounts) => accounts,
None => return None,
};
let instruction_name = payload
.get("instructionName")
.and_then(serde_json::Value::as_str);
let instruction_name = payload.get("instructionName").and_then(serde_json::Value::as_str);
let instruction_name = match instruction_name {
Some(instruction_name) => instruction_name,
None => "",
};
let candidate_pairs = raydium_clmm_token_account_candidate_pairs(instruction_name);
for pair in candidate_pairs {
let inferred = infer_mints_from_account_pair(
accounts,
pair.0,
pair.1,
token_mints_by_account,
);
let inferred =
infer_mints_from_account_pair(accounts, pair.0, pair.1, token_mints_by_account);
if let Some(inferred) = inferred {
return Some(inferred);
}
@@ -1610,7 +1608,17 @@ fn raydium_clmm_token_account_candidate_pairs(
if instruction_name == "increase_liquidity_v2" {
return vec![(13, 14), (9, 10), (7, 8)];
}
return vec![(12, 13), (13, 14), (9, 10), (7, 8), (5, 6), (10, 11), (14, 15), (18, 19), (20, 21)];
return vec![
(12, 13),
(13, 14),
(9, 10),
(7, 8),
(5, 6),
(10, 11),
(14, 15),
(18, 19),
(20, 21),
];
}
fn infer_mints_from_account_pair(
@@ -1732,6 +1740,21 @@ fn extract_account_string(
}
fn is_launchpad_launch_event_materializable(event_kind: &str) -> bool {
if event_kind.contains("pump_fun.create_v2_token") {
return true;
}
if event_kind == "pump_fun.create" || event_kind == "pump_fun.create_event" {
return true;
}
if event_kind == "pump_fun.migrate"
|| event_kind == "pump_fun.migrate_v2"
|| event_kind == "pump_fun.migrate_bonding_curve_creator"
|| event_kind == "pump_fun.migrate_bonding_curve_creator_event"
|| event_kind == "pump_fun.complete_event"
|| event_kind == "pump_fun.complete_pump_amm_migration_event"
{
return true;
}
if event_kind.contains("raydium_launchpad.buy_exact_in") {
return true;
}
@@ -1793,6 +1816,17 @@ fn launchpad_launch_event_role(event_kind: &str) -> std::string::String {
if event_kind.contains("migrate_to_cpswap") {
return "migration_to_cpswap".to_string();
}
if event_kind.contains("pump_fun.migrate")
|| event_kind.contains("pump_fun.complete_pump_amm_migration")
{
return "pump_fun_migration".to_string();
}
if event_kind.contains("pump_fun.complete_event") {
return "pump_fun_completion".to_string();
}
if event_kind.contains("pump_fun.create") {
return "pump_fun_launch".to_string();
}
return "launch".to_string();
}
@@ -1938,7 +1972,108 @@ fn extract_first_bool(
return None;
}
fn should_skip_pump_fun_duplicate_non_trade_event(
decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto],
) -> bool {
if !decoded_event.event_kind.starts_with("pump_fun.") {
return false;
}
let preferred_siblings =
pump_fun_preferred_non_trade_siblings(decoded_event.event_kind.as_str());
if preferred_siblings.is_empty() {
return false;
}
for sibling in decoded_events {
if sibling.id == decoded_event.id {
continue;
}
for preferred in &preferred_siblings {
if sibling.event_kind.as_str() == *preferred {
return true;
}
}
}
return false;
}
fn pump_fun_preferred_non_trade_siblings(event_kind: &str) -> std::vec::Vec<&'static str> {
match event_kind {
"pump_fun.admin_set_creator" => return vec!["pump_fun.admin_set_creator_event"],
"pump_fun.admin_set_idl_authority" => {
return vec!["pump_fun.admin_set_idl_authority_event"];
},
"pump_fun.admin_update_token_incentives" => {
return vec!["pump_fun.admin_update_token_incentives_event"];
},
"pump_fun.claim_cashback" | "pump_fun.claim_cashback_v2" => {
return vec!["pump_fun.claim_cashback_event"];
},
"pump_fun.claim_token_incentives" => return vec!["pump_fun.claim_token_incentives_event"],
"pump_fun.close_user_volume_accumulator" => {
return vec!["pump_fun.close_user_volume_accumulator_event"];
},
"pump_fun.collect_creator_fee" | "pump_fun.collect_creator_fee_v2" => {
return vec!["pump_fun.collect_creator_fee_event"];
},
"pump_fun.create" => return vec!["pump_fun.create_v2_token", "pump_fun.create_event"],
"pump_fun.create_event" => return vec!["pump_fun.create_v2_token"],
"pump_fun.distribute_creator_fees" | "pump_fun.distribute_creator_fees_v2" => {
return vec!["pump_fun.distribute_creator_fees_event"];
},
"pump_fun.extend_account" => return vec!["pump_fun.extend_account_event"],
"pump_fun.get_minimum_distributable_fee" => {
return vec!["pump_fun.minimum_distributable_fee_event"];
},
"pump_fun.init_user_volume_accumulator" => {
return vec!["pump_fun.init_user_volume_accumulator_event"];
},
"pump_fun.migrate_bonding_curve_creator" => {
return vec!["pump_fun.migrate_bonding_curve_creator_event"];
},
"pump_fun.set_creator" => return vec!["pump_fun.set_creator_event"],
"pump_fun.set_metaplex_creator" => return vec!["pump_fun.set_metaplex_creator_event"],
"pump_fun.set_params" => return vec!["pump_fun.set_params_event"],
"pump_fun.set_reserved_fee_recipients" => {
return vec!["pump_fun.reserved_fee_recipients_event"];
},
"pump_fun.sync_user_volume_accumulator" => {
return vec!["pump_fun.sync_user_volume_accumulator_event"];
},
"pump_fun.update_global_authority" => {
return vec!["pump_fun.update_global_authority_event"];
},
"pump_fun.set_mayhem_virtual_params" => {
return vec!["pump_fun.update_mayhem_virtual_params_event"];
},
_ => return std::vec::Vec::new(),
}
}
fn is_pump_fun_payload(payload: &serde_json::Value) -> bool {
if let Some(object) = payload.as_object() {
let protocol_name = object.get("protocolName").and_then(serde_json::Value::as_str);
if protocol_name == Some("pump_fun") {
return true;
}
let decoder = object.get("decoder").and_then(serde_json::Value::as_str);
if decoder == Some("pump_fun") {
return true;
}
let event_kind = object.get("eventKind").and_then(serde_json::Value::as_str);
if let Some(event_kind) = event_kind {
if event_kind.starts_with("pump_fun.") {
return true;
}
}
}
return false;
}
fn is_anchor_event_audit_only(payload: &serde_json::Value) -> bool {
if is_pump_fun_payload(payload) {
return false;
}
if let Some(object) = payload.as_object() {
let flag = object.get("anchorEventAuditOnly");
if let Some(flag) = flag {
@@ -1946,6 +2081,12 @@ fn is_anchor_event_audit_only(payload: &serde_json::Value) -> bool {
return true;
}
}
let flag = object.get("instructionAuditOnly");
if let Some(flag) = flag {
if flag.as_bool() == Some(true) {
return true;
}
}
}
return false;
}

View File

@@ -62,6 +62,18 @@ impl TradeAggregationService {
if !crate::is_dex_trade_event_kind(decoded_event.event_kind.as_str()) {
continue;
}
if crate::trade_aggregation::should_skip_pump_fun_duplicate_trade_event(
decoded_event,
&decoded_events,
) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
decoded_event_id = ?decoded_event.id,
transaction_signature = %transaction.signature,
"skipping duplicate pump_fun trade_event because an instruction trade exists"
);
continue;
}
let event_context =
crate::trade_aggregation_context::load_trade_aggregation_decoded_event_context(
self.database.as_ref(),
@@ -200,6 +212,68 @@ impl TradeAggregationService {
}
}
fn should_skip_pump_fun_duplicate_trade_event(
decoded_event: &crate::DexDecodedEventDto,
decoded_events: &[crate::DexDecodedEventDto],
) -> bool {
if decoded_event.event_kind.as_str() != "pump_fun.trade_event" {
return false;
}
let trade_instruction_id = pump_fun_payload_instruction_id(decoded_event.payload_json.as_str());
for sibling in decoded_events {
if sibling.id == decoded_event.id {
continue;
}
if !is_direct_materialized_pump_fun_instruction_trade_kind(sibling.event_kind.as_str()) {
continue;
}
let sibling_instruction_id = pump_fun_payload_instruction_id(sibling.payload_json.as_str());
if trade_instruction_id.is_some()
&& sibling_instruction_id.is_some()
&& trade_instruction_id != sibling_instruction_id
{
continue;
}
return true;
}
return false;
}
fn is_direct_materialized_pump_fun_instruction_trade_kind(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy" => return true,
"pump_fun.sell" => return true,
"pump_fun.buy_exact_sol_in" => return true,
_ => return false,
}
}
fn pump_fun_payload_instruction_id(payload_json: &str) -> std::option::Option<i64> {
let parsed_result = serde_json::from_str::<serde_json::Value>(payload_json);
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(_) => return None,
};
let object = match parsed.as_object() {
Some(object) => object,
None => return None,
};
let value = match object.get("instructionId") {
Some(value) => value,
None => return None,
};
if let Some(number) = value.as_i64() {
return Some(number);
}
if let Some(text) = value.as_str() {
let parsed_number = text.parse::<i64>();
match parsed_number {
Ok(parsed_number) => return Some(parsed_number),
Err(_) => return None,
}
}
return None;
}
fn transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {

View File

@@ -91,7 +91,8 @@ pub(crate) async fn resolve_trade_amounts(
&mut base_amount_raw,
&mut quote_amount_raw,
&mut price_quote_per_base,
);
)
.await;
if let Err(error) = resolution_result {
return Err(error);
}
@@ -788,7 +789,7 @@ fn apply_raydium_launchpad_side_amount_mapping(
}
}
fn apply_pump_fun_amount_fallback(
async fn apply_pump_fun_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
@@ -813,9 +814,183 @@ fn apply_pump_fun_amount_fallback(
if price_quote_per_base.is_none() {
*price_quote_per_base = inferred.2;
}
if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() {
let sibling_result = crate::trade_amount_resolution::apply_pump_fun_trade_event_sibling_amount_fallback(
input,
base_amount_raw,
quote_amount_raw,
price_quote_per_base,
)
.await;
if let Err(error) = sibling_result {
return Err(error);
}
}
return Ok(());
}
async fn apply_pump_fun_trade_event_sibling_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
if !crate::trade_amount_resolution::pump_fun_instruction_trade_can_use_trade_event_fallback(
input.decoded_event.event_kind.as_str(),
) {
return Ok(());
}
let sibling_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
input.database,
input.decoded_event.transaction_id,
)
.await;
let sibling_events = match sibling_events_result {
Ok(sibling_events) => sibling_events,
Err(error) => return Err(error),
};
for sibling_event in sibling_events {
if sibling_event.id == input.decoded_event.id {
continue;
}
if sibling_event.protocol_name.as_str() != "pump_fun" {
continue;
}
if sibling_event.event_kind.as_str() != "pump_fun.trade_event" {
continue;
}
let sibling_payload_result =
serde_json::from_str::<serde_json::Value>(sibling_event.payload_json.as_str());
let sibling_payload = match sibling_payload_result {
Ok(sibling_payload) => sibling_payload,
Err(error) => {
tracing::debug!(
decoded_event_id = ?sibling_event.id,
error = %error,
"cannot parse pump_fun trade_event sibling payload for amount fallback"
);
continue;
},
};
if !crate::trade_amount_resolution::pump_fun_trade_event_sibling_matches_instruction(
input.decoded_event.event_kind.as_str(),
input.payload,
&sibling_payload,
) {
continue;
}
let sibling_base_amount = crate::trade_amount_resolution::extract_amount_string(
&sibling_payload,
&["baseAmountRaw", "baseAmount", "token_amount", "tokenAmount"],
);
let sibling_quote_amount = crate::trade_amount_resolution::extract_amount_string(
&sibling_payload,
&["quoteAmountRaw", "quoteAmount", "sol_amount", "solAmount", "quote_amount"],
);
if base_amount_raw.is_none() {
*base_amount_raw = sibling_base_amount;
}
if quote_amount_raw.is_none() {
*quote_amount_raw = sibling_quote_amount;
}
if price_quote_per_base.is_none() {
*price_quote_per_base = crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts_with_decimals(
base_amount_raw.as_deref(),
quote_amount_raw.as_deref(),
input.base_token_decimals,
input.quote_token_decimals,
);
}
tracing::debug!(
event_kind = %input.decoded_event.event_kind,
decoded_event_id = ?input.decoded_event.id,
sibling_decoded_event_id = ?sibling_event.id,
base_amount_raw = ?base_amount_raw,
quote_amount_raw = ?quote_amount_raw,
price_quote_per_base = ?price_quote_per_base,
"pump_fun instruction amounts recovered from sibling trade_event"
);
if base_amount_raw.is_some() && quote_amount_raw.is_some() {
return Ok(());
}
}
return Ok(());
}
fn pump_fun_instruction_trade_can_use_trade_event_fallback(event_kind: &str) -> bool {
match event_kind {
"pump_fun.buy_exact_quote_in_v2" => return true,
"pump_fun.buy_exact_sol_in" => return true,
"pump_fun.buy_v2" => return true,
"pump_fun.sell_v2" => return true,
_ => return false,
}
}
fn pump_fun_trade_event_sibling_matches_instruction(
instruction_event_kind: &str,
instruction_payload: &serde_json::Value,
trade_event_payload: &serde_json::Value,
) -> bool {
let expected_is_buy = match instruction_event_kind {
"pump_fun.buy_exact_quote_in_v2" => Some(true),
"pump_fun.buy_exact_sol_in" => Some(true),
"pump_fun.buy_v2" => Some(true),
"pump_fun.sell_v2" => Some(false),
_ => None,
};
if let Some(expected_is_buy) = expected_is_buy {
let actual_is_buy = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
trade_event_payload,
&["is_buy", "isBuy"],
);
match actual_is_buy {
Some(actual_is_buy) if actual_is_buy == expected_is_buy => {},
Some(_) => return false,
None => {},
}
}
let instruction_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
instruction_payload,
&["mint", "tokenMint", "tokenAMint"],
);
let trade_event_mint = crate::trade_amount_resolution::extract_string_by_candidate_keys(
trade_event_payload,
&["mint", "tokenMint", "tokenAMint"],
);
if !crate::trade_amount_resolution::optional_string_values_match(
instruction_mint.as_deref(),
trade_event_mint.as_deref(),
) {
return false;
}
let instruction_user = crate::trade_amount_resolution::extract_string_by_candidate_keys(
instruction_payload,
&["user", "actorWallet"],
);
let trade_event_user = crate::trade_amount_resolution::extract_string_by_candidate_keys(
trade_event_payload,
&["user", "actorWallet"],
);
if !crate::trade_amount_resolution::optional_string_values_match(
instruction_user.as_deref(),
trade_event_user.as_deref(),
) {
return false;
}
return true;
}
fn optional_string_values_match(
left: std::option::Option<&str>,
right: std::option::Option<&str>,
) -> bool {
match (left, right) {
(Some(left), Some(right)) => return left == right,
_ => return true,
}
}
async fn apply_raydium_instruction_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
@@ -1492,6 +1667,44 @@ fn extract_amount_string(
);
}
fn extract_bool_by_candidate_keys(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<bool> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let direct_option = object.get(*candidate_key);
if let Some(direct) = direct_option {
if let Some(bool_value) = direct.as_bool() {
return Some(bool_value);
}
}
}
for nested_value in object.values() {
let nested_result = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
nested_value,
candidate_keys,
);
if nested_result.is_some() {
return nested_result;
}
}
return None;
}
if let Some(array) = value.as_array() {
for nested_value in array {
let nested_result = crate::trade_amount_resolution::extract_bool_by_candidate_keys(
nested_value,
candidate_keys,
);
if nested_result.is_some() {
return nested_result;
}
}
}
return None;
}
fn extract_string_by_candidate_keys(
value: &serde_json::Value,
candidate_keys: &[&str],

View File

@@ -11042,6 +11042,127 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/pumpfun-decoder/src/instructions/admin_update_token_incentives.rs",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"add_quote_mint",
"6f79153828185ed1",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"buy_exact_quote_in_v2",
"c2ab1c46684d5b2f",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"buy_v2",
"b817ee6167c5d33d",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"claim_cashback_v2",
"7af3cc415e741d37",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"collect_creator_fee_v2",
"cf118af204221338",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"distribute_creator_fees_v2",
"ffcb134ff444089f",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"migrate_v2",
"bbcb121fceedfe29",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"remove_quote_mint",
"b141df2658d19e9b",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"sell_v2",
"5df6823ce7e940b2",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"set_virtual_quote_reserves",
"6587bf6809581460",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
manual_solscan_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),
"pump",
"launch",
crate::ENTRY_KIND_INSTRUCTION,
"update_buyback_config",
"fbe0ab92a01a71e9",
8,
"idls/pump_fun.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.json",
),
upstream_git_discriminator_entry(
"pump_fun",
Some(crate::PUMP_FUN_PROGRAM_ID),