This commit is contained in:
2026-06-19 10:29:17 +02:00
parent 319be14aa6
commit 58f9b36969
24 changed files with 6432 additions and 741 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1126,7 +1126,7 @@ impl DexDecodeService {
event.instruction_id,
"meteora_dlmm",
event.program_id.clone(),
"meteora_dlmm.swap",
event.event_kind.as_str(),
event.pool_account.clone(),
None,
event.token_a_mint.clone(),
@@ -4273,6 +4273,18 @@ fn candidate_meteora_audit_pool_account(
return accounts.get(index).cloned();
}
fn is_meteora_dlmm_instruction_swap_event_kind(event_kind: &str) -> bool {
return matches!(
event_kind,
"meteora_dlmm.swap"
| "meteora_dlmm.swap2"
| "meteora_dlmm.swap_exact_out"
| "meteora_dlmm.swap_exact_out2"
| "meteora_dlmm.swap_with_price_impact"
| "meteora_dlmm.swap_with_price_impact2"
);
}
fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap(
protocol_name: &str,
instruction: &crate::ChainInstructionDto,
@@ -4294,7 +4306,7 @@ fn is_meteora_dlmm_anchor_swap_log_replaced_by_decoded_swap(
}
for decoded_event in decoded_events {
if decoded_event.protocol_name == "meteora_dlmm"
&& decoded_event.event_kind == "meteora_dlmm.swap"
&& is_meteora_dlmm_instruction_swap_event_kind(decoded_event.event_kind.as_str())
{
return true;
}

View File

@@ -170,7 +170,12 @@ pub(crate) fn dex_detection_route(
("meteora_dlmm", "meteora_dlmm.create_pool") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool);
},
("meteora_dlmm", "meteora_dlmm.swap") => {
("meteora_dlmm", "meteora_dlmm.swap")
| ("meteora_dlmm", "meteora_dlmm.swap2")
| ("meteora_dlmm", "meteora_dlmm.swap_exact_out")
| ("meteora_dlmm", "meteora_dlmm.swap_exact_out2")
| ("meteora_dlmm", "meteora_dlmm.swap_with_price_impact")
| ("meteora_dlmm", "meteora_dlmm.swap_with_price_impact2") => {
return Some(crate::dex_detection_route::DexDetectionRoute::MeteoraDlmmPool);
},
("meteora_damm_v1", "meteora_damm_v1.create_pool") => {

View File

@@ -323,6 +323,9 @@ pub fn is_dex_informational_event_kind(event_kind: &str) -> bool {
/// Returns true when the event kind represents a swap-like event.
pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if is_meteora_dlmm_anchor_swap_log_event_kind(event_kind) {
return false;
}
if event_kind == "raydium_launchpad.trade_event" {
return true;
}
@@ -376,8 +379,126 @@ pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool {
return is_dex_trade_event_kind(event_kind);
}
fn is_meteora_dlmm_anchor_swap_log_event_kind(event_kind: &str) -> bool {
return matches!(event_kind, "meteora_dlmm.swap_event" | "meteora_dlmm.swap2_evt");
}
fn is_meteora_dlmm_position_lifecycle_event_kind(event_kind: &str) -> bool {
if !event_kind.starts_with("meteora_dlmm.") {
return false;
}
return matches!(
event_kind,
"meteora_dlmm.initialize_position"
| "meteora_dlmm.initialize_position2"
| "meteora_dlmm.initialize_position_by_operator"
| "meteora_dlmm.initialize_position_pda"
| "meteora_dlmm.position_create_event"
| "meteora_dlmm.close_position"
| "meteora_dlmm.close_position2"
| "meteora_dlmm.close_position_if_empty"
| "meteora_dlmm.position_close_event"
| "meteora_dlmm.decrease_position_length"
| "meteora_dlmm.decrease_position_length_event"
| "meteora_dlmm.increase_position_length"
| "meteora_dlmm.increase_position_length2"
| "meteora_dlmm.increase_position_length_event"
| "meteora_dlmm.update_position_operator"
| "meteora_dlmm.update_position_operator_event"
| "meteora_dlmm.update_position_lock_release_point_event"
| "meteora_dlmm.set_position_permissionless_operation_bits_evt"
);
}
fn is_meteora_dlmm_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if !event_kind.starts_with("meteora_dlmm.") {
return false;
}
if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) {
return true;
}
return matches!(
event_kind,
"meteora_dlmm.create_pool"
| "meteora_dlmm.swap_event"
| "meteora_dlmm.swap2_evt"
| "meteora_dlmm.lb_pair_create_event"
| "meteora_dlmm.initialize_lb_pair"
| "meteora_dlmm.initialize_lb_pair2"
| "meteora_dlmm.initialize_permission_lb_pair"
| "meteora_dlmm.initialize_customizable_permissionless_lb_pair"
| "meteora_dlmm.initialize_customizable_permissionless_lb_pair2"
| "meteora_dlmm.initialize_bin_array"
| "meteora_dlmm.initialize_bin_array_bitmap_extension"
| "meteora_dlmm.close_bin_array"
| "meteora_dlmm.go_to_a_bin"
| "meteora_dlmm.go_to_a_bin_event"
| "meteora_dlmm.increase_oracle_length"
| "meteora_dlmm.increase_observation_event"
| "meteora_dlmm.migrate_bin_array"
| "meteora_dlmm.migrate_position"
);
}
fn is_meteora_dlmm_admin_event_kind(event_kind: &str) -> bool {
if !event_kind.starts_with("meteora_dlmm.") {
return false;
}
if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
return false;
}
return matches!(
event_kind,
"meteora_dlmm.instruction_audit"
| "meteora_dlmm.close_claim_fee_operator_account"
| "meteora_dlmm.close_claim_protocol_fee_operator"
| "meteora_dlmm.close_operator_account"
| "meteora_dlmm.close_preset_parameter"
| "meteora_dlmm.close_preset_parameter2"
| "meteora_dlmm.close_token_badge"
| "meteora_dlmm.create_claim_protocol_fee_operator"
| "meteora_dlmm.create_operator_account"
| "meteora_dlmm.dynamic_fee_parameter_update_event"
| "meteora_dlmm.fee_parameter_update_event"
| "meteora_dlmm.initialize_preset_parameter"
| "meteora_dlmm.initialize_preset_parameter2"
| "meteora_dlmm.initialize_preset_parameter_v2"
| "meteora_dlmm.initialize_token_badge"
| "meteora_dlmm.set_activation_point"
| "meteora_dlmm.set_pair_status"
| "meteora_dlmm.set_pair_status_permissionless"
| "meteora_dlmm.set_permissionless_operation_bits"
| "meteora_dlmm.set_pre_activation_duration"
| "meteora_dlmm.set_pre_activation_swap_address"
| "meteora_dlmm.update_base_fee_parameters"
| "meteora_dlmm.update_dynamic_fee_parameters"
| "meteora_dlmm.update_fees_and_reward2"
| "meteora_dlmm.update_fees_and_rewards"
| "meteora_dlmm.update_reward_duration"
| "meteora_dlmm.update_reward_duration_event"
| "meteora_dlmm.update_reward_funder"
| "meteora_dlmm.update_reward_funder_event"
);
}
/// Returns true for liquidity lifecycle changes that must not become candles.
pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dlmm.")
&& (event_kind.contains("withdraw_protocol_fee")
|| event_kind.contains("withdraw_ineligible_reward"))
{
return false;
}
if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) {
return false;
}
if event_kind.starts_with("meteora_dlmm.")
&& (event_kind == "meteora_dlmm.rebalance_liquidity"
|| event_kind == "meteora_dlmm.rebalancing_event"
|| event_kind == "meteora_dlmm.remove_all_liquidity")
{
return true;
}
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("fee")
|| event_kind.contains("surplus")
@@ -461,6 +582,15 @@ pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool {
/// Returns true for liquidity remove-like DEX events.
pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dlmm.")
&& (event_kind.contains("withdraw_protocol_fee")
|| event_kind.contains("withdraw_ineligible_reward"))
{
return false;
}
if event_kind == "meteora_dlmm.remove_all_liquidity" {
return true;
}
if event_kind.contains(".withdraw_pnl") {
return false;
}
@@ -509,6 +639,17 @@ pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
/// Returns true for fee collection events.
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dlmm.") && event_kind.contains("claim_fee_operator") {
return false;
}
if event_kind.starts_with("meteora_dlmm.")
&& (event_kind.contains("claim_fee")
|| event_kind.contains("composition_fee")
|| event_kind.contains("withdraw_protocol_fee")
|| event_kind.contains("zap_protocol_fee"))
{
return true;
}
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("fee")
|| event_kind.contains("surplus")
@@ -580,6 +721,13 @@ pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
/// Returns true for reward or incentive events.
pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dlmm.")
&& (event_kind.contains("update_fees_and_reward")
|| event_kind.contains("update_reward_duration")
|| event_kind.contains("update_reward_funder"))
{
return false;
}
if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("social_fee_pda_claimed")
|| event_kind.contains("claim_social_fee_pda"))
@@ -633,6 +781,15 @@ pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".open_limit_order") {
return true;
}
if event_kind.contains(".place_limit_order") {
return true;
}
if event_kind.contains(".cancel_limit_order") {
return true;
}
if event_kind.contains("limit_order_evt") {
return true;
}
if event_kind.contains(".increase_limit_order") {
return true;
}
@@ -650,6 +807,9 @@ pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
return true;
}
if event_kind.starts_with("pump_fees.")
&& (event_kind.contains("create_fee_sharing_config")
|| event_kind.contains("create_social_fee_pda")
@@ -772,6 +932,25 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
/// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind.starts_with("meteora_dlmm.") {
if event_kind.contains(".initialize_position") {
return false;
}
if event_kind.contains(".initialize_bin_array") {
return true;
}
if event_kind.contains(".initialize_customizable_permissionless_lb_pair")
|| event_kind.contains(".initialize_permission_lb_pair")
|| event_kind.contains(".initialize_lb_pair")
|| event_kind.contains(".lb_pair_create")
|| event_kind.contains(".create_pool")
{
return true;
}
if event_kind.contains(".initialize") {
return false;
}
}
if event_kind == "meteora_dbc.evt_initialize_pool_event"
|| event_kind.contains(".initialize_virtual_pool")
{
@@ -859,6 +1038,12 @@ pub fn is_dex_token_account_close_event_kind(event_kind: &str) -> bool {
/// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
return false;
}
if is_meteora_dlmm_admin_event_kind(event_kind) {
return true;
}
if event_kind.starts_with("meteora_dbc.")
&& (event_kind.contains("config")
|| event_kind.contains("operator")
@@ -1391,6 +1576,42 @@ mod tests {
);
}
#[test]
fn classifies_meteora_dlmm_fee_reward_admin_without_multi_target_overlap() {
assert!(super::is_dex_fee_event_kind("meteora_dlmm.withdraw_protocol_fee"));
assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.withdraw_protocol_fee"));
assert!(!super::is_dex_liquidity_remove_event_kind("meteora_dlmm.withdraw_protocol_fee"));
assert!(!super::is_dex_fee_event_kind("meteora_dlmm.close_claim_fee_operator_account"));
assert!(super::is_dex_admin_event_kind("meteora_dlmm.close_claim_fee_operator_account"));
assert!(super::is_dex_reward_event_kind("meteora_dlmm.initialize_reward"));
assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.initialize_reward"));
assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_fees_and_rewards"));
assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_fees_and_rewards"));
assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_reward_duration_event"));
assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_reward_duration_event"));
assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.position_create_event"));
assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.position_create_event"));
assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.increase_position_length"));
assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.increase_position_length"));
assert!(!super::is_dex_admin_event_kind("meteora_dlmm.update_position_operator"));
assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.close_bin_array"));
assert!(super::is_dex_admin_event_kind("meteora_dlmm.create_operator_account"));
assert!(super::is_dex_admin_event_kind(
"meteora_dlmm.initialize_preset_parameter_v2"
));
assert!(super::is_dex_admin_event_kind("meteora_dlmm.instruction_audit"));
assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.instruction_audit"));
assert!(!super::is_dex_fee_event_kind("meteora_dlmm.instruction_audit"));
assert!(!super::is_dex_reward_event_kind("meteora_dlmm.instruction_audit"));
assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.rebalancing_event"));
assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.remove_all_liquidity"));
assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap_event"));
assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap_event"));
assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap_event"));
assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap2_evt"));
assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap2_evt"));
assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap2_evt"));
}
#[test]
fn enriched_payload_keeps_existing_fields() {
let payload_json = serde_json::json!({
@@ -1514,6 +1735,10 @@ mod tests {
super::classify_dex_event_category_code("meteora_dlmm.remove_liquidity"),
"liquidity"
);
assert_eq!(
super::classify_dex_event_category_code("meteora_dlmm.initialize_position"),
"pool_lifecycle"
);
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.initialize_position"),
"position_open"

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,90 @@ fn resolve_instruction_name(
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
if program_id == crate::METEORA_DLMM_PROGRAM_ID || decoder_code == Some("meteora_dlmm") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "anchor_self_cpi_log",
"b59d59438fb63448" => "add_liquidity",
"e4a24e1c46db7473" => "add_liquidity2",
"0703967f94283dc8" => "add_liquidity_by_strategy",
"03dd95da6f8d76d5" => "add_liquidity_by_strategy2",
"2905eeaf64e106cd" => "add_liquidity_by_strategy_one_side",
"1c8cee63e7a21595" => "add_liquidity_by_weight",
"d13b3f5b6fc899e4" => "add_liquidity_by_weight2",
"5e9b6797465fdca5" => "add_liquidity_one_side",
"a1c26754ab47fa9a" => "add_liquidity_one_side_precise",
"2133a3c975627de7" => "add_liquidity_one_side_precise2",
"849c841f4328e861" => "cancel_limit_order",
"a9204f8988e84689" => "claim_fee",
"70bf65ab1c907fbb" => "claim_fee2",
"955fb5f25e5a9ea2" => "claim_reward",
"be037f77b2579db7" => "claim_reward2",
"44ae5850b5cc13e0" => "close_bin_array",
"b8d5581fb3658224" => "close_claim_fee_operator_account",
"397c249b7ef95dab" => "close_limit_order_if_empty",
"ab09d54a7817031d" => "close_operator_account",
"7b86510031446262" => "close_position",
"ae5a2373ba2893e2" => "close_position2",
"3b7cd4765b986e9d" => "close_position_if_empty",
"04949164861ab53d" => "close_preset_parameter",
"27195f6b7411731c" => "close_preset_parameter2",
"6c92566eb3fe0a68" => "close_token_badge",
"dd40f695f099e5a3" => "create_operator_account",
"c2db882019606925" => "decrease_position_length",
"b46945505f32496c" => "for_idl_type_generation_do_not_call",
"bc32f9a55d97263f" => "fund_reward",
"9248aee028fd54ae" => "go_to_a_bin",
"be3d7d57674f9ead" => "increase_oracle_length",
"505375d3420d2195" => "increase_position_length",
"ffd2cc477389e171" => "increase_position_length2",
"235613b94ed44bd3" => "initialize_bin_array",
"2f9de2b40cf02147" => "initialize_bin_array_bitmap_extension",
"2e2729876fb7c840" => "initialize_customizable_permissionless_lb_pair",
"f349817e3313f16b" => "initialize_customizable_permissionless_lb_pair2",
"2d9aedd2dd0fa65c" => "initialize_lb_pair",
"493b2478ed536cc6" => "initialize_lb_pair2",
"6c66d555fb033515" => "initialize_permission_lb_pair",
"dbc0ea47bebf6650" => "initialize_position",
"8f13f291d50f6873" => "initialize_position2",
"fbbdbef475fe2394" => "initialize_position_by_operator",
"2e527d92558de499" => "initialize_position_pda",
"42bc47d3626d0eba" => "initialize_preset_parameter",
"75c73e67068e1fcb" => "initialize_preset_parameter_v2",
"5f87c0c4f281e644" => "initialize_reward",
"fd4dcd5f1be059df" => "initialize_token_badge",
"6cb021ba92e501c5" => "place_limit_order",
"5c04b0c177b95309" => "rebalance_liquidity",
"0a333d2370691855" => "remove_all_liquidity",
"5055d14818ceb16c" => "remove_liquidity",
"e6d7527ff165e392" => "remove_liquidity2",
"1a526698f04a691a" => "remove_liquidity_by_range",
"cc02c391359191cd" => "remove_liquidity_by_range2",
"5bf90fa51a81fe7d" => "set_activation_point",
"43f8e7899a95d9ae" => "set_pair_status",
"4e3b98d346b72ed0" => "set_pair_status_permissionless",
"543acb8ba351beba" => "set_permissionless_operation_bits",
"a53dc9f4829f1664" => "set_pre_activation_duration",
"398b2f7bd850df0a" => "set_pre_activation_swap_address",
"f8c69e91e17587c8" => "swap",
"414b3f4ceb5b5b88" => "swap2",
"fa49652126cf4bb8" => "swap_exact_out",
"2bd7f784893cf351" => "swap_exact_out2",
"38ade6d0ade49ccd" => "swap_with_price_impact",
"4a62c0d6b1334b33" => "swap_with_price_impact2",
"4ba8dfa110c3032f" => "update_base_fee_parameters",
"5ca12ef6ffbd1616" => "update_dynamic_fee_parameters",
"208eb89a6741b858" => "update_fees_and_reward2",
"9ae6fa0decd14bdf" => "update_fees_and_rewards",
"cab8678fb4bf74d9" => "update_position_operator",
"8aaec4a9d5ebfe6b" => "update_reward_duration",
"d31c3020d7a02317" => "update_reward_funder",
"94ce2ac3f7316708" => "withdraw_ineligible_reward",
"9ec99ebd215da267" => "withdraw_protocol_fee",
"d59bbb2238b65bf0" => "zap_protocol_fee",
_ => "instruction_audit",
};
return Some(format!("meteora_dlmm.{}", name));
}
if program_id == crate::METEORA_DBC_PROGRAM_ID || decoder_code == Some("meteora_dbc") {
let name = match discriminator_hex {
"e445a52e51cb9a1d" => "meteora_dbc.anchor_self_cpi_log",
@@ -578,6 +662,43 @@ fn option_i64_key(value: std::option::Option<i64>) -> std::string::String {
#[cfg(test)]
mod tests {
#[test]
fn resolves_meteora_dlmm_instruction_observation_names() {
let anchor_name = super::resolve_instruction_name(
crate::METEORA_DLMM_PROGRAM_ID,
Some("meteora_dlmm"),
Some("e445a52e51cb9a1d"),
);
assert_eq!(anchor_name.as_deref(), Some("meteora_dlmm.anchor_self_cpi_log"));
let swap_exact_out2_name = super::resolve_instruction_name(
crate::METEORA_DLMM_PROGRAM_ID,
Some("meteora_dlmm"),
Some("2bd7f784893cf351"),
);
assert_eq!(swap_exact_out2_name.as_deref(), Some("meteora_dlmm.swap_exact_out2"));
let close_position_name = super::resolve_instruction_name(
crate::METEORA_DLMM_PROGRAM_ID,
Some("meteora_dlmm"),
Some("7b86510031446262"),
);
assert_eq!(close_position_name.as_deref(), Some("meteora_dlmm.close_position"));
let preset_v2_name = super::resolve_instruction_name(
crate::METEORA_DLMM_PROGRAM_ID,
Some("meteora_dlmm"),
Some("75c73e67068e1fcb"),
);
assert_eq!(
preset_v2_name.as_deref(),
Some("meteora_dlmm.initialize_preset_parameter_v2")
);
let unknown_name = super::resolve_instruction_name(
crate::METEORA_DLMM_PROGRAM_ID,
Some("meteora_dlmm"),
Some("ffffffffffffffff"),
);
assert_eq!(unknown_name.as_deref(), Some("meteora_dlmm.instruction_audit"));
}
#[test]
fn resolves_meteora_dbc_instruction_observation_names() {
let anchor_name = super::resolve_instruction_name(

View File

@@ -71,10 +71,14 @@ fn should_skip_non_trade_event_due_to_explicit_reason(
{
return true;
}
if crate::is_dex_orderbook_event_kind(decoded_event.event_kind.as_str())
&& payload_has_non_empty_text(payload, "skipOrderbookReason")
{
return true;
}
return false;
}
fn should_attempt_meteora_dbc_explicit_skip_materialization(
decoded_event: &crate::DexDecodedEventDto,
payload: &serde_json::Value,
@@ -121,7 +125,6 @@ fn is_meteora_dbc_instruction_admin_materialization_candidate(event_kind: &str)
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FeeAmountRecoveryPolicy {
Disabled,
@@ -151,11 +154,43 @@ fn fee_amount_recovery_policy_for_event_kind(event_kind: &str) -> FeeAmountRecov
| "raydium_launchpad.claim_platform_fee_from_vault"
| "raydium_launchpad.collect_fee"
| "raydium_stable_swap.withdraw_pnl"
| "raydium_stable_swap.withdraw_srm" => FeeAmountRecoveryPolicy::InnerSplTransfer,
| "raydium_stable_swap.withdraw_srm"
| "meteora_dlmm.claim_fee"
| "meteora_dlmm.claim_fee2"
| "meteora_dlmm.claim_fee_event"
| "meteora_dlmm.claim_fee2_event"
| "meteora_dlmm.composition_fee_event"
| "meteora_dlmm.withdraw_protocol_fee"
| "meteora_dlmm.zap_protocol_fee" => FeeAmountRecoveryPolicy::InnerSplTransfer,
_ => FeeAmountRecoveryPolicy::Disabled,
};
}
fn reward_amount_recovery_policy_for_event_kind(event_kind: &str) -> FeeAmountRecoveryPolicy {
return match event_kind {
"meteora_dlmm.claim_reward"
| "meteora_dlmm.claim_reward2"
| "meteora_dlmm.claim_reward_event"
| "meteora_dlmm.claim_reward2_event"
| "meteora_dlmm.fund_reward"
| "meteora_dlmm.fund_reward_event"
| "meteora_dlmm.initialize_reward"
| "meteora_dlmm.initialize_reward_event"
| "meteora_dlmm.withdraw_ineligible_reward"
| "meteora_dlmm.withdraw_ineligible_reward_event" => {
FeeAmountRecoveryPolicy::InnerSplTransfer
},
_ => FeeAmountRecoveryPolicy::Disabled,
};
}
fn optional_text_is_missing(value: &std::option::Option<std::string::String>) -> bool {
match value.as_ref() {
Some(value) => return value.trim().is_empty(),
None => return true,
}
}
fn payload_has_non_empty_text(payload: &serde_json::Value, key: &str) -> bool {
let value = match payload.get(key) {
Some(value) => value,
@@ -240,7 +275,10 @@ impl NonTradeEventMaterializationService {
continue;
}
if should_skip_non_trade_event_due_to_explicit_reason(decoded_event, &payload)
&& !should_attempt_meteora_dbc_explicit_skip_materialization(decoded_event, &payload)
&& !should_attempt_meteora_dbc_explicit_skip_materialization(
decoded_event,
&payload,
)
{
tracing::debug!(
event_kind = %decoded_event.event_kind,
@@ -534,10 +572,44 @@ impl NonTradeEventMaterializationService {
],
);
let mut payload_json = decoded_event.payload_json.clone();
let mut fee_amount_legs: std::vec::Vec<MeteoraDbcActualAmount> = std::vec::Vec::new();
if is_meteora_dbc_instruction_fee_materialization_candidate(decoded_event.event_kind.as_str()) {
let mut fee_amount_legs: std::vec::Vec<MeteoraDbcActualAmount> =
fee_amount_legs_from_payload(payload);
if !fee_amount_legs.is_empty() {
match fee_amount_legs.as_slice() {
[first_leg] => {
fee_token_mint = Some(first_leg.token_mint.clone());
fee_amount_raw = Some(first_leg.amount_raw.clone());
},
_ => {
fee_token_mint = None;
fee_amount_raw = None;
},
}
let updated_payload =
decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs);
payload_json = updated_payload.to_string();
let update_result = crate::query_dex_decoded_events_update_payload_json_by_id(
self.database.as_ref(),
decoded_event_id,
payload_json.as_str(),
)
.await;
if let Err(error) = update_result {
return Err(error);
}
}
if fee_amount_legs.is_empty()
&& is_meteora_dbc_instruction_fee_materialization_candidate(
decoded_event.event_kind.as_str(),
)
{
let inferred_result = self
.resolve_meteora_dbc_actual_fee_amounts(transaction, transaction_id, decoded_event, payload)
.resolve_meteora_dbc_actual_fee_amounts(
transaction,
transaction_id,
decoded_event,
payload,
)
.await;
let inferred = match inferred_result {
Ok(inferred) => inferred,
@@ -555,7 +627,8 @@ impl NonTradeEventMaterializationService {
fee_amount_raw = None;
},
}
let updated_payload = decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs);
let updated_payload =
decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs);
payload_json = updated_payload.to_string();
let update_result = crate::query_dex_decoded_events_update_payload_json_by_id(
self.database.as_ref(),
@@ -597,11 +670,14 @@ impl NonTradeEventMaterializationService {
return Ok(false);
}
}
let recovery_policy = fee_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str());
let recovery_policy =
fee_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str());
if fee_amount_legs.is_empty()
&& fee_amount_raw.is_none()
&& recovery_policy != FeeAmountRecoveryPolicy::Disabled
&& !is_meteora_dbc_instruction_fee_materialization_candidate(decoded_event.event_kind.as_str())
&& !is_meteora_dbc_instruction_fee_materialization_candidate(
decoded_event.event_kind.as_str(),
)
{
let inferred_result = self
.resolve_allowlisted_actual_fee_amounts_from_spl_transfers(
@@ -626,7 +702,8 @@ impl NonTradeEventMaterializationService {
fee_amount_raw = None;
},
}
let updated_payload = decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs);
let updated_payload =
decoded_payload_with_materialized_fee_legs(payload, &fee_amount_legs);
payload_json = updated_payload.to_string();
let update_result = crate::query_dex_decoded_events_update_payload_json_by_id(
self.database.as_ref(),
@@ -650,10 +727,11 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
};
if let Some(explanation) = explanation {
let updated_payload = decoded_payload_with_unmaterialized_fee_amount_recovery_reason(
payload,
explanation.as_str(),
);
let updated_payload =
decoded_payload_with_unmaterialized_fee_amount_recovery_reason(
payload,
explanation.as_str(),
);
payload_json = updated_payload.to_string();
let update_result = crate::query_dex_decoded_events_update_payload_json_by_id(
self.database.as_ref(),
@@ -781,10 +859,13 @@ impl NonTradeEventMaterializationService {
Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]),
None => None,
};
let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result =
crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id)
.await;
let instruction_index =
extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
@@ -855,10 +936,13 @@ impl NonTradeEventMaterializationService {
Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]),
None => None,
};
let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result =
crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id)
.await;
let instruction_index =
extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
@@ -929,10 +1013,13 @@ impl NonTradeEventMaterializationService {
Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]),
None => None,
};
let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result =
crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id)
.await;
let instruction_index =
extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
@@ -988,9 +1075,7 @@ impl NonTradeEventMaterializationService {
non_zero_mints.insert(mint);
}
if non_zero_mints.len() > 1 {
return Ok(Some(
"multi_token_fee_amounts_materialized_as_fee_amount_legs".to_string(),
));
return Ok(Some("multi_token_fee_amounts_materialized_as_fee_amount_legs".to_string()));
}
if transfer_count > 0 && non_zero_transfer_count == 0 {
return Ok(Some("fee_instruction_has_only_zero_amount_transfers".to_string()));
@@ -1001,6 +1086,86 @@ impl NonTradeEventMaterializationService {
return Ok(None);
}
async fn resolve_allowlisted_actual_reward_amount_from_spl_transfers(
&self,
transaction_id: i64,
decoded_event: &crate::DexDecodedEventDto,
payload: &serde_json::Value,
) -> Result<std::option::Option<MeteoraDbcActualAmount>, crate::Error> {
let parent_instruction_id = match decoded_event.id {
Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]),
None => None,
};
let instruction_index =
extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
};
let mut amount_legs = std::vec::Vec::new();
for instruction in &instructions {
if !instruction_is_child_of_decoded_event(
instruction,
parent_instruction_id,
instruction_index,
) {
continue;
}
let parsed_type = match instruction.parsed_type.as_deref() {
Some(parsed_type) => parsed_type,
None => continue,
};
if parsed_type != "transferChecked" && parsed_type != "transfer" {
continue;
}
let parsed_json = match instruction.parsed_json.as_deref() {
Some(parsed_json) => parsed_json,
None => continue,
};
let parsed_value = serde_json::from_str::<serde_json::Value>(parsed_json);
let parsed_value = match parsed_value {
Ok(parsed_value) => parsed_value,
Err(_) => continue,
};
let amount_raw = extract_spl_transfer_amount_raw(&parsed_value);
let amount_raw = match amount_raw {
Some(amount_raw) => amount_raw,
None => continue,
};
let amount = amount_raw.parse::<u128>();
let amount = match amount {
Ok(amount) => amount,
Err(_) => continue,
};
if amount == 0 {
continue;
}
let mint = extract_first_string(&parsed_value, &["mint"]);
let mint = match mint {
Some(mint) => mint,
None => continue,
};
let source_account = extract_first_string(&parsed_value, &["source"]);
let destination_account = extract_first_string(&parsed_value, &["destination"]);
amount_legs.push(MeteoraDbcActualAmount {
token_mint: mint,
amount_raw,
source: "allowlisted_reward_inner_spl_transfer".to_string(),
source_account,
destination_account,
});
}
return Ok(single_mint_amount_from_actual_amounts(
amount_legs.as_slice(),
"allowlisted_reward_inner_spl_transfer",
));
}
async fn resolve_meteora_dbc_unmaterialized_fee_skip_reason(
&self,
transaction_id: i64,
@@ -1011,10 +1176,13 @@ impl NonTradeEventMaterializationService {
Some(_) => extract_first_i64(payload, &["instructionId", "instruction_id"]),
None => None,
};
let instruction_index = extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result =
crate::query_chain_instructions_list_by_transaction_id(self.database.as_ref(), transaction_id)
.await;
let instruction_index =
extract_first_i64(payload, &["instructionIndex", "instruction_index"]);
let instructions_result = crate::query_chain_instructions_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let instructions = match instructions_result {
Ok(instructions) => instructions,
Err(error) => return Err(error),
@@ -1070,9 +1238,7 @@ impl NonTradeEventMaterializationService {
non_zero_mints.insert(mint);
}
if non_zero_mints.len() > 1 {
return Ok(Some(
"multi_token_fee_amounts_require_multiple_fee_rows".to_string(),
));
return Ok(Some("multi_token_fee_amounts_require_multiple_fee_rows".to_string()));
}
if transfer_count > 0 && non_zero_transfer_count == 0 {
return Ok(Some("fee_instruction_has_only_zero_amount_transfers".to_string()));
@@ -1112,11 +1278,11 @@ impl NonTradeEventMaterializationService {
"user",
],
);
let reward_token_mint = extract_first_string(
let mut reward_token_mint = extract_first_string(
payload,
&["rewardTokenMint", "reward_token_mint", "tokenMint", "token_mint", "mint"],
);
let reward_amount_raw = extract_first_amount_string(
let mut reward_amount_raw = extract_first_amount_string(
payload,
&[
"rewardAmountRaw",
@@ -1128,6 +1294,35 @@ impl NonTradeEventMaterializationService {
"amount",
],
);
let mut reward_payload_json = decoded_event.payload_json.clone();
if reward_amount_recovery_policy_for_event_kind(decoded_event.event_kind.as_str())
== FeeAmountRecoveryPolicy::InnerSplTransfer
&& (optional_text_is_missing(&reward_token_mint)
|| optional_text_is_missing(&reward_amount_raw))
{
let recovered_reward_result = self
.resolve_allowlisted_actual_reward_amount_from_spl_transfers(
transaction_id,
decoded_event,
payload,
)
.await;
let recovered_reward = match recovered_reward_result {
Ok(recovered_reward) => recovered_reward,
Err(error) => return Err(error),
};
if let Some(recovered_reward) = recovered_reward {
if optional_text_is_missing(&reward_token_mint) {
reward_token_mint = Some(recovered_reward.token_mint.clone());
}
if optional_text_is_missing(&reward_amount_raw) {
reward_amount_raw = Some(recovered_reward.amount_raw.clone());
}
reward_payload_json =
decoded_payload_with_materialized_reward_amount(payload, &recovered_reward)
.to_string();
}
}
let dto = crate::RewardEventDto::new(
transaction_id,
Some(decoded_event_id),
@@ -1143,7 +1338,7 @@ impl NonTradeEventMaterializationService {
actor_wallet,
reward_token_mint,
reward_amount_raw,
decoded_event.payload_json.clone(),
reward_payload_json,
);
let upsert_result = crate::query_reward_events_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
@@ -2100,6 +2295,119 @@ struct MeteoraDbcActualAmount {
destination_account: std::option::Option<std::string::String>,
}
fn single_mint_amount_from_actual_amounts(
amount_legs: &[MeteoraDbcActualAmount],
source: &str,
) -> std::option::Option<MeteoraDbcActualAmount> {
let mut token_mint: std::option::Option<std::string::String> = None;
let mut amount_total: u128 = 0;
let mut source_account: std::option::Option<std::string::String> = None;
let mut destination_account: std::option::Option<std::string::String> = None;
for leg in amount_legs {
if leg.token_mint.trim().is_empty() {
continue;
}
let amount = leg.amount_raw.parse::<u128>();
let amount = match amount {
Ok(amount) => amount,
Err(_) => continue,
};
if amount == 0 {
continue;
}
match token_mint.as_ref() {
Some(existing_mint) => {
if existing_mint != &leg.token_mint {
return None;
}
},
None => token_mint = Some(leg.token_mint.clone()),
}
let checked_total = amount_total.checked_add(amount);
amount_total = match checked_total {
Some(checked_total) => checked_total,
None => return None,
};
if source_account.is_none() {
source_account = leg.source_account.clone();
}
if destination_account.is_none() {
destination_account = leg.destination_account.clone();
}
}
let token_mint = match token_mint {
Some(token_mint) => token_mint,
None => return None,
};
if amount_total == 0 {
return None;
}
return Some(MeteoraDbcActualAmount {
token_mint,
amount_raw: amount_total.to_string(),
source: source.to_string(),
source_account,
destination_account,
});
}
fn fee_amount_legs_from_payload(
payload: &serde_json::Value,
) -> std::vec::Vec<MeteoraDbcActualAmount> {
let mut legs = std::vec::Vec::new();
let candidate_arrays = ["feeAmountLegs", "materializedFeeAmountLegs"];
for candidate_key in candidate_arrays {
let candidate = match payload.get(candidate_key) {
Some(candidate) => candidate,
None => continue,
};
let array = match candidate.as_array() {
Some(array) => array,
None => continue,
};
for leg_value in array {
let token_mint = extract_first_string(
leg_value,
&["tokenMint", "token_mint", "feeTokenMint", "fee_token_mint", "mint"],
);
let amount_raw = extract_first_amount_string(
leg_value,
&["amountRaw", "amount_raw", "feeAmountRaw", "fee_amount_raw", "amount"],
);
let token_mint = match token_mint {
Some(token_mint) => token_mint,
None => continue,
};
let amount_raw = match amount_raw {
Some(amount_raw) => amount_raw,
None => continue,
};
let amount_trimmed = amount_raw.trim();
if amount_trimmed.is_empty() || amount_trimmed == "0" {
continue;
}
let source =
match extract_first_string(leg_value, &["amountSource", "amount_source", "source"])
{
Some(source) => source,
None => "decoded_fee_amount_leg".to_string(),
};
let source_account =
extract_first_string(leg_value, &["sourceAccount", "source_account"]);
let destination_account =
extract_first_string(leg_value, &["destinationAccount", "destination_account"]);
legs.push(MeteoraDbcActualAmount {
token_mint,
amount_raw: amount_trimmed.to_string(),
source,
source_account,
destination_account,
});
}
}
return legs;
}
fn instruction_is_child_of_decoded_event(
instruction: &crate::ChainInstructionDto,
parent_instruction_id: std::option::Option<i64>,
@@ -2147,6 +2455,57 @@ fn extract_spl_transfer_amount_raw(
return None;
}
fn decoded_payload_with_materialized_reward_amount(
payload: &serde_json::Value,
reward_amount: &MeteoraDbcActualAmount,
) -> serde_json::Value {
let mut object = match payload.clone() {
serde_json::Value::Object(object) => object,
other => {
let mut object = serde_json::Map::new();
object.insert("rawPayload".to_string(), other);
object
},
};
object.insert("skipRewardReason".to_string(), serde_json::Value::String(String::new()));
object.insert("rewardMaterialized".to_string(), serde_json::Value::Bool(true));
object.insert(
"rewardMaterializationSource".to_string(),
serde_json::Value::String(reward_amount.source.clone()),
);
object.insert(
"rewardTokenMint".to_string(),
serde_json::Value::String(reward_amount.token_mint.clone()),
);
object.insert(
"rewardAmountRaw".to_string(),
serde_json::Value::String(reward_amount.amount_raw.clone()),
);
match reward_amount.source_account.as_ref() {
Some(source_account) => {
object.insert(
"rewardSourceAccount".to_string(),
serde_json::Value::String(source_account.clone()),
);
},
None => {},
}
match reward_amount.destination_account.as_ref() {
Some(destination_account) => {
object.insert(
"rewardDestinationAccount".to_string(),
serde_json::Value::String(destination_account.clone()),
);
},
None => {},
}
object.insert(
"eventActionability".to_string(),
serde_json::Value::String("reward_materialized".to_string()),
);
return serde_json::Value::Object(object);
}
fn decoded_payload_with_materialized_fee_legs(
payload: &serde_json::Value,
legs: &[MeteoraDbcActualAmount],
@@ -2159,11 +2518,11 @@ fn decoded_payload_with_materialized_fee_legs(
object
},
};
object.insert("skipFeeReason".to_string(), serde_json::Value::String(std::string::String::new()));
object.insert(
"feeMaterialized".to_string(),
serde_json::Value::Bool(true),
"skipFeeReason".to_string(),
serde_json::Value::String(std::string::String::new()),
);
object.insert("feeMaterialized".to_string(), serde_json::Value::Bool(true));
object.insert(
"feeMaterializationSource".to_string(),
serde_json::Value::String("fee_event_amounts".to_string()),
@@ -2217,18 +2576,9 @@ fn fee_amount_leg_payload_json(
"legIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(u64::from(leg_index))),
);
object.insert(
"tokenMint".to_string(),
serde_json::Value::String(leg.token_mint.clone()),
);
object.insert(
"amountRaw".to_string(),
serde_json::Value::String(leg.amount_raw.clone()),
);
object.insert(
"amountSource".to_string(),
serde_json::Value::String(leg.source.clone()),
);
object.insert("tokenMint".to_string(), serde_json::Value::String(leg.token_mint.clone()));
object.insert("amountRaw".to_string(), serde_json::Value::String(leg.amount_raw.clone()));
object.insert("amountSource".to_string(), serde_json::Value::String(leg.source.clone()));
match leg.source_account.as_ref() {
Some(source_account) => {
object.insert(
@@ -2250,7 +2600,6 @@ fn fee_amount_leg_payload_json(
return serde_json::Value::Object(object).to_string();
}
fn fee_event_amount_leg_from_parent_amount(
decoded_event: &crate::DexDecodedEventDto,
transaction_id: i64,
@@ -2272,7 +2621,11 @@ fn fee_event_amount_leg_from_parent_amount(
if fee_amount_raw.trim().is_empty() {
return None;
}
let payload_json = fee_amount_parent_payload_json(decoded_event, fee_token_mint.as_str(), fee_amount_raw.as_str());
let payload_json = fee_amount_parent_payload_json(
decoded_event,
fee_token_mint.as_str(),
fee_amount_raw.as_str(),
);
return Some(crate::FeeEventAmountDto::new(
0,
transaction_id,
@@ -2302,14 +2655,8 @@ fn fee_amount_parent_payload_json(
"legIndex".to_string(),
serde_json::Value::Number(serde_json::Number::from(0_u64)),
);
object.insert(
"tokenMint".to_string(),
serde_json::Value::String(token_mint.to_string()),
);
object.insert(
"amountRaw".to_string(),
serde_json::Value::String(amount_raw.to_string()),
);
object.insert("tokenMint".to_string(), serde_json::Value::String(token_mint.to_string()));
object.insert("amountRaw".to_string(), serde_json::Value::String(amount_raw.to_string()));
object.insert(
"amountSource".to_string(),
serde_json::Value::String("parent_fee_event_amount".to_string()),
@@ -2329,14 +2676,8 @@ fn decoded_payload_with_unmaterialized_fee_reason(
object
},
};
object.insert(
"skipFeeReason".to_string(),
serde_json::Value::String(reason.to_string()),
);
object.insert(
"feeMaterialized".to_string(),
serde_json::Value::Bool(false),
);
object.insert("skipFeeReason".to_string(), serde_json::Value::String(reason.to_string()));
object.insert("feeMaterialized".to_string(), serde_json::Value::Bool(false));
object.insert(
"feeMaterializationSource".to_string(),
serde_json::Value::String("none".to_string()),
@@ -2368,10 +2709,7 @@ fn decoded_payload_with_unmaterialized_fee_amount_recovery_reason(
"feeAmountRecoveryReason".to_string(),
serde_json::Value::String(reason.to_string()),
);
object.insert(
"feeAmountLegsMaterialized".to_string(),
serde_json::Value::Bool(false),
);
object.insert("feeAmountLegsMaterialized".to_string(), serde_json::Value::Bool(false));
object.insert(
"feeAmountLegCount".to_string(),
serde_json::Value::Number(serde_json::Number::from(0_u64)),
@@ -2387,7 +2725,8 @@ fn resolve_meteora_dbc_actual_fee_amount_from_lamport_delta(
if !decoded_event.event_kind.contains("pool_creation_fee") {
return None;
}
let transaction_value = serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
let transaction_value =
serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
let transaction_value = match transaction_value {
Ok(transaction_value) => transaction_value,
Err(_) => return None,
@@ -2408,7 +2747,8 @@ fn resolve_meteora_dbc_actual_fee_amount_from_lamport_delta(
append_unique_candidate_account(&mut candidate_accounts, decoded_event.pool_account.as_deref());
let payload_pool_account = extract_first_string(payload, &["poolAccount", "pool", "poolState"]);
append_unique_candidate_account(&mut candidate_accounts, payload_pool_account.as_deref());
let payload_related_account = extract_first_string(payload, &["relatedAccount", "receiver", "feeReceiver"]);
let payload_related_account =
extract_first_string(payload, &["relatedAccount", "receiver", "feeReceiver"]);
append_unique_candidate_account(&mut candidate_accounts, payload_related_account.as_deref());
if let Some(accounts) = payload.get("accounts").and_then(serde_json::Value::as_array) {
for account in accounts {
@@ -3325,6 +3665,155 @@ mod tests {
super::fee_amount_recovery_policy_for_event_kind("meteora_dbc.claim_trading_fee"),
super::FeeAmountRecoveryPolicy::Disabled
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_fee"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_fee2"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.composition_fee_event"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.withdraw_protocol_fee"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.zap_protocol_fee"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind(
"meteora_dlmm.close_claim_fee_operator_account"
),
super::FeeAmountRecoveryPolicy::Disabled
);
assert_eq!(
super::fee_amount_recovery_policy_for_event_kind("meteora_dlmm.swap_event"),
super::FeeAmountRecoveryPolicy::Disabled
);
}
#[test]
fn reward_amount_recovery_policy_is_allowlisted_only() {
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward2"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.claim_reward_event"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.fund_reward"),
super::FeeAmountRecoveryPolicy::InnerSplTransfer
);
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind(
"meteora_dlmm.update_fees_and_rewards"
),
super::FeeAmountRecoveryPolicy::Disabled
);
assert_eq!(
super::reward_amount_recovery_policy_for_event_kind("meteora_dlmm.swap_event"),
super::FeeAmountRecoveryPolicy::Disabled
);
}
#[test]
fn single_mint_reward_amount_sums_matching_mints_only() {
let legs = vec![
super::MeteoraDbcActualAmount {
token_mint: "RewardMint111".to_string(),
amount_raw: "7".to_string(),
source: "test".to_string(),
source_account: Some("Source111".to_string()),
destination_account: Some("Destination111".to_string()),
},
super::MeteoraDbcActualAmount {
token_mint: "RewardMint111".to_string(),
amount_raw: "5".to_string(),
source: "test".to_string(),
source_account: Some("Source222".to_string()),
destination_account: Some("Destination222".to_string()),
},
];
let resolved = super::single_mint_amount_from_actual_amounts(
legs.as_slice(),
"allowlisted_reward_inner_spl_transfer",
);
let resolved = match resolved {
Some(resolved) => resolved,
None => panic!("expected single mint reward amount"),
};
assert_eq!(resolved.token_mint, "RewardMint111");
assert_eq!(resolved.amount_raw, "12");
assert_eq!(resolved.source, "allowlisted_reward_inner_spl_transfer");
let mixed_mint_legs = vec![
super::MeteoraDbcActualAmount {
token_mint: "RewardMint111".to_string(),
amount_raw: "7".to_string(),
source: "test".to_string(),
source_account: None,
destination_account: None,
},
super::MeteoraDbcActualAmount {
token_mint: "OtherMint111".to_string(),
amount_raw: "5".to_string(),
source: "test".to_string(),
source_account: None,
destination_account: None,
},
];
assert!(
super::single_mint_amount_from_actual_amounts(
mixed_mint_legs.as_slice(),
"allowlisted_reward_inner_spl_transfer",
)
.is_none()
);
}
#[test]
fn decoded_payload_with_materialized_reward_amount_marks_source() {
let payload = serde_json::json!({
"eventKind": "meteora_dlmm.claim_reward"
});
let reward_amount = super::MeteoraDbcActualAmount {
token_mint: "RewardMint111".to_string(),
amount_raw: "42".to_string(),
source: "allowlisted_reward_inner_spl_transfer".to_string(),
source_account: Some("Source111".to_string()),
destination_account: Some("Destination111".to_string()),
};
let materialized =
super::decoded_payload_with_materialized_reward_amount(&payload, &reward_amount);
assert_eq!(
materialized.get("rewardTokenMint").and_then(serde_json::Value::as_str),
Some("RewardMint111")
);
assert_eq!(
materialized.get("rewardAmountRaw").and_then(serde_json::Value::as_str),
Some("42")
);
assert_eq!(
materialized
.get("rewardMaterializationSource")
.and_then(serde_json::Value::as_str),
Some("allowlisted_reward_inner_spl_transfer")
);
assert_eq!(
materialized.get("eventActionability").and_then(serde_json::Value::as_str),
Some("reward_materialized")
);
}
#[test]