// file: kb_lib/src/trade_amount_resolution.rs //! Trade amount resolution orchestration. //! //! This module resolves base/quote raw amounts and quote/base price for one //! decoded trade candidate by applying protocol-specific and generic fallback //! strategies in deterministic order. /// Input context required to resolve trade amounts. pub(crate) struct TradeAmountResolutionInput<'a> { /// Database connection. pub(crate) database: &'a crate::Database, /// Persisted transaction row. pub(crate) transaction: &'a crate::ChainTransactionDto, /// Decoded DEX event row. pub(crate) decoded_event: &'a crate::DexDecodedEventDto, /// Decoded event payload. pub(crate) payload: &'a serde_json::Value, /// Pool account address. pub(crate) pool_address: &'a str, /// Base token mint, when known. pub(crate) base_token_mint: std::option::Option<&'a str>, /// Quote token mint, when known. pub(crate) quote_token_mint: std::option::Option<&'a str>, /// Base token decimals, when known. pub(crate) base_token_decimals: std::option::Option, /// Quote token decimals, when known. pub(crate) quote_token_decimals: std::option::Option, /// Base token vault address, when known. pub(crate) base_vault_address: std::option::Option<&'a str>, /// Quote token vault address, when known. pub(crate) quote_vault_address: std::option::Option<&'a str>, } /// Resolved raw trade amounts and quote/base price. #[derive(Debug, Clone)] pub(crate) struct TradeAmountResolution { /// Base amount in raw token units. pub(crate) base_amount_raw: std::option::Option, /// Quote amount in raw token units. pub(crate) quote_amount_raw: std::option::Option, /// Quote/base price. pub(crate) price_quote_per_base: std::option::Option, /// Trade side resolved from balance deltas, when available. pub(crate) resolved_trade_side: std::option::Option, } /// Resolves trade amounts from payload and protocol-specific fallbacks. pub(crate) async fn resolve_trade_amounts( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, ) -> Result { let mut base_amount_raw = crate::trade_amount_resolution::extract_amount_string( input.payload, &["baseAmountRaw", "base_amount_raw", "baseAmount", "amountBase", "amountInBase"], ); let mut quote_amount_raw = crate::trade_amount_resolution::extract_amount_string( input.payload, &[ "quoteAmountRaw", "quote_amount_raw", "quoteAmount", "amountQuote", "amountOutQuote", ], ); let mut price_quote_per_base = None; let mut resolved_trade_side = None; if input.decoded_event.event_kind.starts_with("pump_swap.") && (base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_pump_swap_amount_fallbacks( input, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ) .await; if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("pump_fun.") && (base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_pump_fun_amount_fallback( input, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if (input.decoded_event.event_kind.starts_with("raydium_amm_v4.") || input.decoded_event.event_kind.starts_with("raydium_cpmm.") || input.decoded_event.event_kind.starts_with("raydium_clmm.")) && (base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_raydium_instruction_amount_fallback( input, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ) .await; if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("raydium_amm_v4.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback( input, input.base_vault_address, input.quote_vault_address, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("raydium_cpmm.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback( input, input.base_vault_address, input.quote_vault_address, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("raydium_clmm.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback( input, input.base_vault_address, input.quote_vault_address, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("meteora_dlmm.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_meteora_dlmm_flattened_cpi_amount_fallback( input, &mut base_amount_raw, &mut quote_amount_raw, &mut resolved_trade_side, ) .await; if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("meteora_damm_v1.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_meteora_damm_v1_flattened_cpi_amount_fallback( input, &mut base_amount_raw, &mut quote_amount_raw, &mut resolved_trade_side, ) .await; if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("meteora_dlmm.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback( input, input.base_vault_address, input.quote_vault_address, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("meteora_damm_v1.") && (base_amount_raw.is_none() || quote_amount_raw.is_none()) { let resolution_result = crate::trade_amount_resolution::apply_vault_balance_delta_fallback( input, input.base_vault_address, input.quote_vault_address, &mut base_amount_raw, &mut quote_amount_raw, &mut price_quote_per_base, ); if let Err(error) = resolution_result { return Err(error); } } if input.decoded_event.event_kind.starts_with("raydium_amm_v4.") { let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas( input.transaction.meta_json.as_deref(), input.transaction.transaction_json.as_str(), input.base_vault_address, input.quote_vault_address, ); if vault_side.is_some() { resolved_trade_side = vault_side; } } if input.decoded_event.event_kind.starts_with("meteora_dlmm.") { let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas( input.transaction.meta_json.as_deref(), input.transaction.transaction_json.as_str(), input.base_vault_address, input.quote_vault_address, ); if vault_side.is_some() { resolved_trade_side = vault_side; } } if input.decoded_event.event_kind.starts_with("meteora_damm_v1.") { let vault_side = crate::trade_amount_resolution::infer_trade_side_from_vault_balance_deltas( input.transaction.meta_json.as_deref(), input.transaction.transaction_json.as_str(), input.base_vault_address, input.quote_vault_address, ); if vault_side.is_some() { resolved_trade_side = vault_side; } } 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, ); } if price_quote_per_base.is_none() { price_quote_per_base = crate::trade_solana_amounts::compute_price_quote_per_base_with_decimals( input.transaction.meta_json.as_deref(), input.transaction.transaction_json.as_str(), input.base_vault_address, input.quote_vault_address, ); } if price_quote_per_base.is_none() { price_quote_per_base = crate::trade_metric_update::compute_price_quote_per_base_from_raw_amounts( base_amount_raw.as_deref(), quote_amount_raw.as_deref(), ); } return Ok(crate::trade_amount_resolution::TradeAmountResolution { base_amount_raw, quote_amount_raw, price_quote_per_base, resolved_trade_side, }); } async fn apply_pump_swap_amount_fallbacks( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, price_quote_per_base: &mut std::option::Option, ) -> Result<(), crate::Error> { let pool_owner_result = match (input.base_token_mint, input.quote_token_mint) { (Some(base_mint), Some(quote_mint)) => { crate::trade_pump_swap_amounts::resolve_pump_swap_trade_amounts_from_pool_balance_deltas( input.transaction.meta_json.as_deref(), input.pool_address, base_mint, quote_mint, input.decoded_event.event_kind.as_str(), input.base_token_decimals, input.quote_token_decimals, ) }, _ => Ok(crate::trade_pump_swap_amounts::PumpSwapPoolBalanceDeltaResolution::MissingData), }; let pool_owner_resolution = match pool_owner_result { Ok(pool_owner_resolution) => pool_owner_resolution, Err(error) => return Err(error), }; let pool_owner_resolution_label = pool_owner_resolution.as_label(); tracing::debug!( event_kind = %input.decoded_event.event_kind, pool_account = ?input.decoded_event.pool_account, decoded_event_id = ?input.decoded_event.id, transaction_signature = %input.transaction.signature, base_mint = ?input.base_token_mint, quote_mint = ?input.quote_token_mint, pool_owner_resolution = %pool_owner_resolution_label, "pump_swap pool-owner delta resolution result" ); match pool_owner_resolution { crate::trade_pump_swap_amounts::PumpSwapPoolBalanceDeltaResolution::Matched(amounts) => { *base_amount_raw = Some(amounts.base_amount_raw); *quote_amount_raw = Some(amounts.quote_amount_raw); *price_quote_per_base = Some(amounts.price_quote_per_base); tracing::debug!( event_kind = %input.decoded_event.event_kind, pool_account = ?input.decoded_event.pool_account, decoded_event_id = ?input.decoded_event.id, base_mint = ?input.base_token_mint, quote_mint = ?input.quote_token_mint, base_amount_raw = ?base_amount_raw, quote_amount_raw = ?quote_amount_raw, price_quote_per_base = ?price_quote_per_base, "pump_swap trade amounts recovered from pool-owner token balance deltas" ); }, crate::trade_pump_swap_amounts::PumpSwapPoolBalanceDeltaResolution::DirectionMismatch => { tracing::debug!( event_kind = %input.decoded_event.event_kind, pool_account = ?input.decoded_event.pool_account, decoded_event_id = ?input.decoded_event.id, transaction_signature = %input.transaction.signature, "pump_swap pool-owner full-transaction delta direction mismatch; continuing with instruction-scoped fallbacks" ); }, crate::trade_pump_swap_amounts::PumpSwapPoolBalanceDeltaResolution::MissingData => {}, } let decoded_instruction_index_result = crate::trade_amount_resolution::load_decoded_instruction_index( input.database, input.decoded_event, ) .await; let decoded_instruction_index = match decoded_instruction_index_result { Ok(decoded_instruction_index) => decoded_instruction_index, Err(error) => return Err(error), }; let payload_user_base_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["userBaseTokenAccount", "user_base_token_account"], ); let payload_user_quote_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["userQuoteTokenAccount", "user_quote_token_account"], ); let payload_pool_base_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["poolBaseTokenAccount", "pool_base_token_account"], ); let payload_pool_quote_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["poolQuoteTokenAccount", "pool_quote_token_account"], ); let effective_base_vault_address = match input.base_vault_address { Some(base_vault_address) => Some(base_vault_address), None => payload_pool_base_token_account.as_deref(), }; let effective_quote_vault_address = match input.quote_vault_address { Some(quote_vault_address) => Some(quote_vault_address), None => payload_pool_quote_token_account.as_deref(), }; let (input_vault_address, output_vault_address, input_token_account, output_token_account) = if input.decoded_event.event_kind.ends_with(".buy") { ( effective_quote_vault_address, effective_base_vault_address, payload_user_quote_token_account.as_deref(), payload_user_base_token_account.as_deref(), ) } else if input.decoded_event.event_kind.ends_with(".sell") { ( effective_base_vault_address, effective_quote_vault_address, payload_user_base_token_account.as_deref(), payload_user_quote_token_account.as_deref(), ) } else { (None, None, None, None) }; let inferred_result = crate::trade_solana_amounts::extract_trade_amounts_from_instruction_token_transfers( input.transaction.meta_json.as_deref(), decoded_instruction_index, input_vault_address, output_vault_address, input_token_account, output_token_account, effective_base_vault_address, effective_quote_vault_address, ); let inferred = match inferred_result { Ok(inferred) => inferred, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = inferred.0; } if quote_amount_raw.is_none() { *quote_amount_raw = inferred.1; } if price_quote_per_base.is_none() { *price_quote_per_base = inferred.2; } if base_amount_raw.is_none() || quote_amount_raw.is_none() { let fallback_result = crate::trade_solana_amounts::extract_trade_amounts_from_vault_balance_deltas( input.transaction.transaction_json.as_str(), input.transaction.meta_json.as_deref(), effective_base_vault_address, effective_quote_vault_address, ); let fallback = match fallback_result { Ok(fallback) => fallback, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = fallback.0; } if quote_amount_raw.is_none() { *quote_amount_raw = fallback.1; } if price_quote_per_base.is_none() { *price_quote_per_base = fallback.2; } } if base_amount_raw.is_none() || quote_amount_raw.is_none() || price_quote_per_base.is_none() { let transaction_value_result = crate::trade_pump_swap_amounts::build_transaction_value_with_meta_json( input.transaction.transaction_json.as_str(), input.transaction.meta_json.as_deref(), ); let transaction_value = match transaction_value_result { Ok(transaction_value) => transaction_value, Err(error) => return Err(error), }; let fallback_amounts = match (input.base_token_mint, input.quote_token_mint) { (Some(base_mint), Some(quote_mint)) => { crate::trade_pump_swap_amounts::try_build_pump_swap_trade_amounts_from_token_balance_deltas( &transaction_value, base_mint, quote_mint, ) }, _ => None, }; if let Some(fallback_amounts) = fallback_amounts { if base_amount_raw.is_none() { *base_amount_raw = crate::trade_pump_swap_amounts::convert_ui_amount_to_raw_string( fallback_amounts.base_amount, input.base_token_decimals, ); } if quote_amount_raw.is_none() { *quote_amount_raw = crate::trade_pump_swap_amounts::convert_ui_amount_to_raw_string( fallback_amounts.quote_amount, input.quote_token_decimals, ); } if price_quote_per_base.is_none() { *price_quote_per_base = Some(fallback_amounts.price_quote_per_base); } tracing::debug!( event_kind = %input.decoded_event.event_kind, pool_account = ?input.decoded_event.pool_account, decoded_event_id = ?input.decoded_event.id, base_mint = ?input.base_token_mint, quote_mint = ?input.quote_token_mint, base_amount_raw = ?base_amount_raw, quote_amount_raw = ?quote_amount_raw, price_quote_per_base = ?price_quote_per_base, "pump_swap trade amounts recovered from token balance deltas" ); } } return Ok(()); } fn apply_pump_fun_amount_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, price_quote_per_base: &mut std::option::Option, ) -> Result<(), crate::Error> { let inferred_result = crate::trade_solana_amounts::extract_pump_fun_amounts_from_transaction( input.transaction.transaction_json.as_str(), input.transaction.meta_json.as_deref(), input.base_vault_address, input.quote_vault_address, ); let inferred = match inferred_result { Ok(inferred) => inferred, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = inferred.0; } if quote_amount_raw.is_none() { *quote_amount_raw = inferred.1; } if price_quote_per_base.is_none() { *price_quote_per_base = inferred.2; } return Ok(()); } async fn apply_raydium_instruction_amount_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, price_quote_per_base: &mut std::option::Option, ) -> Result<(), crate::Error> { let decoded_instruction_index_result = crate::trade_amount_resolution::load_decoded_instruction_index( input.database, input.decoded_event, ) .await; let decoded_instruction_index = match decoded_instruction_index_result { Ok(decoded_instruction_index) => decoded_instruction_index, Err(error) => return Err(error), }; let payload_input_vault_address = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["inputVault", "input_vault"], ); let payload_output_vault_address = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["outputVault", "output_vault"], ); let payload_input_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["inputTokenAccount", "input_token_account"], ); let payload_output_token_account = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["outputTokenAccount", "output_token_account"], ); let payload_base_vault_address = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["baseVault", "base_vault"], ); let payload_quote_vault_address = crate::trade_amount_resolution::extract_string_by_candidate_keys( input.payload, &["quoteVault", "quote_vault"], ); let effective_base_vault_address = match input.base_vault_address { Some(base_vault_address) => Some(base_vault_address), None => payload_base_vault_address.as_deref(), }; let effective_quote_vault_address = match input.quote_vault_address { Some(quote_vault_address) => Some(quote_vault_address), None => payload_quote_vault_address.as_deref(), }; let inferred_result = crate::trade_solana_amounts::extract_trade_amounts_from_instruction_token_transfers( input.transaction.meta_json.as_deref(), decoded_instruction_index, payload_input_vault_address.as_deref(), payload_output_vault_address.as_deref(), payload_input_token_account.as_deref(), payload_output_token_account.as_deref(), effective_base_vault_address, effective_quote_vault_address, ); let inferred = match inferred_result { Ok(inferred) => inferred, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = inferred.0; } if quote_amount_raw.is_none() { *quote_amount_raw = inferred.1; } if price_quote_per_base.is_none() { *price_quote_per_base = inferred.2; } return Ok(()); } #[derive(Debug, Clone)] struct FlattenedCpiTransferAmountResolution { base_amount_raw: std::option::Option, quote_amount_raw: std::option::Option, resolved_trade_side: std::option::Option, } async fn apply_meteora_dlmm_flattened_cpi_amount_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, resolved_trade_side: &mut std::option::Option, ) -> Result<(), crate::Error> { return crate::trade_amount_resolution::apply_flattened_cpi_amount_fallback( input, "meteora_dlmm", base_amount_raw, quote_amount_raw, resolved_trade_side, ) .await; } async fn apply_meteora_damm_v1_flattened_cpi_amount_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, resolved_trade_side: &mut std::option::Option, ) -> Result<(), crate::Error> { return crate::trade_amount_resolution::apply_flattened_cpi_amount_fallback( input, "meteora_damm_v1", base_amount_raw, quote_amount_raw, resolved_trade_side, ) .await; } async fn apply_flattened_cpi_amount_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, protocol_label: &str, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, resolved_trade_side: &mut std::option::Option, ) -> Result<(), crate::Error> { let decoded_instruction_result = crate::trade_amount_resolution::load_decoded_instruction( input.database, input.decoded_event, ) .await; let decoded_instruction = match decoded_instruction_result { Ok(Some(decoded_instruction)) => decoded_instruction, Ok(None) => return Ok(()), Err(error) => return Err(error), }; let instructions_result = crate::query_chain_instructions_list_by_transaction_id( input.database, input.decoded_event.transaction_id, ) .await; let instructions = match instructions_result { Ok(instructions) => instructions, Err(error) => return Err(error), }; let flattened_result = resolve_amounts_from_flattened_cpi_transfer_window( &decoded_instruction, &instructions, input.base_token_mint, input.quote_token_mint, input.base_vault_address, input.quote_vault_address, ); let flattened = match flattened_result { Ok(flattened) => flattened, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = flattened.base_amount_raw; } if quote_amount_raw.is_none() { *quote_amount_raw = flattened.quote_amount_raw; } if resolved_trade_side.is_none() { *resolved_trade_side = flattened.resolved_trade_side; } if base_amount_raw.is_some() || quote_amount_raw.is_some() { tracing::debug!( event_kind = %input.decoded_event.event_kind, pool_account = ?input.decoded_event.pool_account, protocol_label = %protocol_label, decoded_event_id = ?input.decoded_event.id, transaction_signature = %input.transaction.signature, base_mint = ?input.base_token_mint, quote_mint = ?input.quote_token_mint, base_amount_raw = ?base_amount_raw, quote_amount_raw = ?quote_amount_raw, resolved_trade_side = ?resolved_trade_side, "trade amounts recovered from flattened CPI transfer window" ); } return Ok(()); } fn resolve_amounts_from_flattened_cpi_transfer_window( decoded_instruction: &crate::ChainInstructionDto, transaction_instructions: &[crate::ChainInstructionDto], base_token_mint: std::option::Option<&str>, quote_token_mint: std::option::Option<&str>, base_vault_address: std::option::Option<&str>, quote_vault_address: std::option::Option<&str>, ) -> Result { let decoded_stack_height = match decoded_instruction.stack_height { Some(stack_height) => stack_height, None => return Ok(crate::trade_amount_resolution::empty_flattened_cpi_amount_resolution()), }; let stop_inner_instruction_index = crate::trade_amount_resolution::find_flattened_cpi_window_stop_inner_instruction_index( decoded_instruction, transaction_instructions, decoded_stack_height, ); let mut base_transfer_direction = None; let mut quote_transfer_direction = None; let mut base_amount_raw = None; let mut quote_amount_raw = None; for instruction in transaction_instructions { if !crate::trade_amount_resolution::instruction_is_inside_flattened_cpi_window( decoded_instruction, instruction, decoded_stack_height, stop_inner_instruction_index, ) { continue; } let parsed_transfer_result = crate::trade_amount_resolution::parse_transfer_checked_instruction(instruction); let parsed_transfer = match parsed_transfer_result { Ok(Some(parsed_transfer)) => parsed_transfer, Ok(None) => continue, Err(error) => return Err(error), }; if base_amount_raw.is_none() && crate::trade_amount_resolution::string_option_equals( base_token_mint, parsed_transfer.mint.as_str(), ) && crate::trade_amount_resolution::transfer_touches_vault( &parsed_transfer, base_vault_address, ) { base_transfer_direction = crate::trade_amount_resolution::infer_transfer_direction( &parsed_transfer, base_vault_address, ); base_amount_raw = Some(parsed_transfer.amount_raw.clone()); continue; } if quote_amount_raw.is_none() && crate::trade_amount_resolution::string_option_equals( quote_token_mint, parsed_transfer.mint.as_str(), ) && crate::trade_amount_resolution::transfer_touches_vault( &parsed_transfer, quote_vault_address, ) { quote_transfer_direction = crate::trade_amount_resolution::infer_transfer_direction( &parsed_transfer, quote_vault_address, ); quote_amount_raw = Some(parsed_transfer.amount_raw.clone()); continue; } } let resolved_trade_side = crate::trade_amount_resolution::infer_trade_side_from_transfer_directions( base_transfer_direction, quote_transfer_direction, ); return Ok(crate::trade_amount_resolution::FlattenedCpiTransferAmountResolution { base_amount_raw, quote_amount_raw, resolved_trade_side, }); } fn instruction_is_inside_flattened_cpi_window( decoded_instruction: &crate::ChainInstructionDto, candidate_instruction: &crate::ChainInstructionDto, decoded_stack_height: u32, stop_inner_instruction_index: std::option::Option, ) -> bool { if candidate_instruction.transaction_id != decoded_instruction.transaction_id { return false; } if candidate_instruction.instruction_index != decoded_instruction.instruction_index { return false; } let candidate_inner_instruction_index = match candidate_instruction.inner_instruction_index { Some(inner_instruction_index) => inner_instruction_index, None => return false, }; if let Some(decoded_inner_instruction_index) = decoded_instruction.inner_instruction_index { if candidate_inner_instruction_index <= decoded_inner_instruction_index { return false; } } let candidate_stack_height = match candidate_instruction.stack_height { Some(stack_height) => stack_height, None => return false, }; if candidate_stack_height <= decoded_stack_height { return false; } if let Some(stop_inner_instruction_index) = stop_inner_instruction_index { if candidate_inner_instruction_index >= stop_inner_instruction_index { return false; } } return true; } fn find_flattened_cpi_window_stop_inner_instruction_index( decoded_instruction: &crate::ChainInstructionDto, transaction_instructions: &[crate::ChainInstructionDto], decoded_stack_height: u32, ) -> std::option::Option { let decoded_inner_instruction_index = match decoded_instruction.inner_instruction_index { Some(decoded_inner_instruction_index) => decoded_inner_instruction_index, None => return None, }; let mut stop_inner_instruction_index = None; for instruction in transaction_instructions { if instruction.transaction_id != decoded_instruction.transaction_id { continue; } if instruction.instruction_index != decoded_instruction.instruction_index { continue; } let candidate_inner_instruction_index = match instruction.inner_instruction_index { Some(candidate_inner_instruction_index) => candidate_inner_instruction_index, None => continue, }; if candidate_inner_instruction_index <= decoded_inner_instruction_index { continue; } let candidate_stack_height = match instruction.stack_height { Some(candidate_stack_height) => candidate_stack_height, None => continue, }; if candidate_stack_height > decoded_stack_height { continue; } match stop_inner_instruction_index { Some(current_stop) => { if candidate_inner_instruction_index < current_stop { stop_inner_instruction_index = Some(candidate_inner_instruction_index); } }, None => stop_inner_instruction_index = Some(candidate_inner_instruction_index), } } return stop_inner_instruction_index; } #[derive(Debug, Clone)] struct ParsedTransferCheckedInstruction { mint: std::string::String, amount_raw: std::string::String, source: std::string::String, destination: std::string::String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum VaultTransferDirection { IntoVault, OutOfVault, } fn parse_transfer_checked_instruction( instruction: &crate::ChainInstructionDto, ) -> Result< std::option::Option, crate::Error, > { let parsed_type = match instruction.parsed_type.as_deref() { Some(parsed_type) => parsed_type, None => return Ok(None), }; if parsed_type != "transferChecked" { return Ok(None); } let parsed_json_text = match instruction.parsed_json.as_deref() { Some(parsed_json_text) => parsed_json_text, None => return Ok(None), }; let parsed_json_result = serde_json::from_str::(parsed_json_text); let parsed_json = match parsed_json_result { Ok(parsed_json) => parsed_json, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse parsed_json for transferChecked instruction '{}': {}", crate::trade_amount_resolution::format_instruction_id_for_log(instruction.id), error ))); }, }; let info = match parsed_json.get("info") { Some(info) => info, None => return Ok(None), }; let mint = match crate::trade_amount_resolution::extract_string_by_candidate_keys(info, &["mint"]) { Some(mint) => mint, None => return Ok(None), }; let amount_raw = match crate::trade_amount_resolution::extract_scalar_as_string_by_candidate_keys( info, &["amount"], ) { Some(amount_raw) => amount_raw, None => { let token_amount = match info.get("tokenAmount") { Some(token_amount) => token_amount, None => return Ok(None), }; match crate::trade_amount_resolution::extract_scalar_as_string_by_candidate_keys( token_amount, &["amount"], ) { Some(amount_raw) => amount_raw, None => return Ok(None), } }, }; let source = match crate::trade_amount_resolution::extract_string_by_candidate_keys(info, &["source"]) { Some(source) => source, None => return Ok(None), }; let destination = match crate::trade_amount_resolution::extract_string_by_candidate_keys( info, &["destination"], ) { Some(destination) => destination, None => return Ok(None), }; return Ok(Some(crate::trade_amount_resolution::ParsedTransferCheckedInstruction { mint, amount_raw, source, destination, })); } fn transfer_touches_vault( transfer: &crate::trade_amount_resolution::ParsedTransferCheckedInstruction, vault_address: std::option::Option<&str>, ) -> bool { let vault_address = match vault_address { Some(vault_address) => vault_address, None => return true, }; if crate::trade_amount_resolution::account_equals(transfer.source.as_str(), vault_address) { return true; } if crate::trade_amount_resolution::account_equals(transfer.destination.as_str(), vault_address) { return true; } return false; } fn infer_transfer_direction( transfer: &crate::trade_amount_resolution::ParsedTransferCheckedInstruction, vault_address: std::option::Option<&str>, ) -> std::option::Option { let vault_address = match vault_address { Some(vault_address) => vault_address, None => return None, }; if crate::trade_amount_resolution::account_equals(transfer.destination.as_str(), vault_address) { return Some(crate::trade_amount_resolution::VaultTransferDirection::IntoVault); } if crate::trade_amount_resolution::account_equals(transfer.source.as_str(), vault_address) { return Some(crate::trade_amount_resolution::VaultTransferDirection::OutOfVault); } return None; } fn infer_trade_side_from_transfer_directions( base_transfer_direction: std::option::Option< crate::trade_amount_resolution::VaultTransferDirection, >, quote_transfer_direction: std::option::Option< crate::trade_amount_resolution::VaultTransferDirection, >, ) -> std::option::Option { match (base_transfer_direction, quote_transfer_direction) { ( Some(crate::trade_amount_resolution::VaultTransferDirection::OutOfVault), Some(crate::trade_amount_resolution::VaultTransferDirection::IntoVault), ) => return Some(crate::SwapTradeSide::BuyBase), ( Some(crate::trade_amount_resolution::VaultTransferDirection::IntoVault), Some(crate::trade_amount_resolution::VaultTransferDirection::OutOfVault), ) => return Some(crate::SwapTradeSide::SellBase), _ => return None, } } fn string_option_equals(left: std::option::Option<&str>, right: &str) -> bool { let left = match left { Some(left) => left.trim(), None => return false, }; let right = right.trim(); if left.is_empty() || right.is_empty() { return false; } return left == right; } fn account_equals(left: &str, right: &str) -> bool { let left = left.trim(); let right = right.trim(); if left.is_empty() || right.is_empty() { return false; } return left == right; } fn format_instruction_id_for_log(instruction_id: std::option::Option) -> std::string::String { match instruction_id { Some(instruction_id) => return instruction_id.to_string(), None => return "".to_string(), } } fn empty_flattened_cpi_amount_resolution() -> crate::trade_amount_resolution::FlattenedCpiTransferAmountResolution { return crate::trade_amount_resolution::FlattenedCpiTransferAmountResolution { base_amount_raw: None, quote_amount_raw: None, resolved_trade_side: None, }; } fn apply_vault_balance_delta_fallback( input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>, base_vault_address: std::option::Option<&str>, quote_vault_address: std::option::Option<&str>, base_amount_raw: &mut std::option::Option, quote_amount_raw: &mut std::option::Option, price_quote_per_base: &mut std::option::Option, ) -> Result<(), crate::Error> { let inferred_result = crate::trade_solana_amounts::extract_trade_amounts_from_vault_balance_deltas( input.transaction.transaction_json.as_str(), input.transaction.meta_json.as_deref(), base_vault_address, quote_vault_address, ); let inferred = match inferred_result { Ok(inferred) => inferred, Err(error) => return Err(error), }; if base_amount_raw.is_none() { *base_amount_raw = inferred.0; } if quote_amount_raw.is_none() { *quote_amount_raw = inferred.1; } if price_quote_per_base.is_none() { *price_quote_per_base = inferred.2; } return Ok(()); } async fn load_decoded_instruction_index( database: &crate::Database, decoded_event: &crate::DexDecodedEventDto, ) -> Result, crate::Error> { let instruction_result = crate::trade_amount_resolution::load_decoded_instruction(database, decoded_event).await; let instruction_option = match instruction_result { Ok(instruction_option) => instruction_option, Err(error) => return Err(error), }; match instruction_option { Some(instruction) => return Ok(Some(instruction.instruction_index)), None => return Ok(None), } } async fn load_decoded_instruction( database: &crate::Database, decoded_event: &crate::DexDecodedEventDto, ) -> Result, crate::Error> { let instruction_id = match decoded_event.instruction_id { Some(instruction_id) => instruction_id, None => return Ok(None), }; let instruction_result = crate::query_chain_instructions_get_by_id(database, instruction_id).await; let instruction_option = match instruction_result { Ok(instruction_option) => instruction_option, Err(error) => return Err(error), }; return Ok(instruction_option); } fn extract_amount_string( payload: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { return crate::trade_amount_resolution::extract_scalar_as_string_by_candidate_keys( payload, candidate_keys, ); } fn extract_string_by_candidate_keys( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { 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 { let direct_text_option = direct.as_str(); if let Some(direct_text) = direct_text_option { return Some(direct_text.to_string()); } } } for nested_value in object.values() { let nested_result = crate::trade_amount_resolution::extract_string_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_string_by_candidate_keys( nested_value, candidate_keys, ); if nested_result.is_some() { return nested_result; } } } return None; } fn extract_scalar_as_string_by_candidate_keys( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { 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(text) = direct.as_str() { return Some(text.to_string()); } if let Some(number) = direct.as_i64() { return Some(number.to_string()); } if let Some(number) = direct.as_u64() { return Some(number.to_string()); } if let Some(number) = direct.as_f64() { return Some(number.to_string()); } } } for nested_value in object.values() { let nested_result = crate::trade_amount_resolution::extract_scalar_as_string_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_scalar_as_string_by_candidate_keys( nested_value, candidate_keys, ); if nested_result.is_some() { return nested_result; } } } return None; } fn infer_trade_side_from_vault_balance_deltas( meta_json: std::option::Option<&str>, transaction_json: &str, base_vault_address: std::option::Option<&str>, quote_vault_address: std::option::Option<&str>, ) -> std::option::Option { let base_vault_address = match base_vault_address { Some(base_vault_address) => base_vault_address, None => return None, }; let quote_vault_address = match quote_vault_address { Some(quote_vault_address) => quote_vault_address, None => return None, }; let meta_text = match meta_json { Some(meta_text) => meta_text, None => return None, }; let meta_result = serde_json::from_str::(meta_text); let meta = match meta_result { Ok(meta) => meta, Err(_) => return None, }; let transaction_result = serde_json::from_str::(transaction_json); let transaction = match transaction_result { Ok(transaction) => transaction, Err(_) => return None, }; if transaction_failed(&meta) { return None; } let account_keys = collect_transaction_account_keys(&transaction, &meta); let base_account_index = find_account_index(&account_keys, base_vault_address); let base_account_index = match base_account_index { Some(base_account_index) => base_account_index, None => return None, }; let quote_account_index = find_account_index(&account_keys, quote_vault_address); let quote_account_index = match quote_account_index { Some(quote_account_index) => quote_account_index, None => return None, }; let base_delta = token_balance_delta_for_account_index(&meta, base_account_index); let base_delta = match base_delta { Some(base_delta) => base_delta, None => return None, }; let quote_delta = token_balance_delta_for_account_index(&meta, quote_account_index); let quote_delta = match quote_delta { Some(quote_delta) => quote_delta, None => return None, }; if base_delta < 0_i128 && quote_delta > 0_i128 { return Some(crate::SwapTradeSide::BuyBase); } if base_delta > 0_i128 && quote_delta < 0_i128 { return Some(crate::SwapTradeSide::SellBase); } return None; } fn transaction_failed(meta: &serde_json::Value) -> bool { if let Some(err_value) = meta.get("err") { if !err_value.is_null() { return true; } } if let Some(status) = meta.get("status") { if let Some(object) = status.as_object() { if object.get("Err").is_some() { return true; } } } return false; } fn collect_transaction_account_keys( transaction: &serde_json::Value, meta: &serde_json::Value, ) -> std::vec::Vec { let mut account_keys = std::vec::Vec::new(); collect_account_keys_from_candidate_path( transaction, &["transaction", "message", "accountKeys"], &mut account_keys, ); if account_keys.is_empty() { collect_account_keys_from_candidate_path( transaction, &["message", "accountKeys"], &mut account_keys, ); } collect_loaded_addresses(meta, &mut account_keys); return account_keys; } fn collect_account_keys_from_candidate_path( value: &serde_json::Value, path: &[&str], target: &mut std::vec::Vec, ) { let mut current = value; for key in path { let next = current.get(*key); current = match next { Some(next) => next, None => return, }; } let array = match current.as_array() { Some(array) => array, None => return, }; for item in array { if let Some(text) = item.as_str() { target.push(text.to_string()); continue; } if let Some(pubkey) = item.get("pubkey").and_then(|value| return value.as_str()) { target.push(pubkey.to_string()); } } } fn collect_loaded_addresses( meta: &serde_json::Value, target: &mut std::vec::Vec, ) { let loaded_addresses = match meta.get("loadedAddresses") { Some(loaded_addresses) => loaded_addresses, None => return, }; collect_loaded_address_array(loaded_addresses, "writable", target); collect_loaded_address_array(loaded_addresses, "readonly", target); } fn collect_loaded_address_array( loaded_addresses: &serde_json::Value, key: &str, target: &mut std::vec::Vec, ) { let array = match loaded_addresses.get(key).and_then(|value| return value.as_array()) { Some(array) => array, None => return, }; for item in array { if let Some(text) = item.as_str() { target.push(text.to_string()); } } } fn find_account_index( account_keys: &[std::string::String], account_address: &str, ) -> std::option::Option { for (index, account_key) in account_keys.iter().enumerate() { if account_key == account_address { return Some(index); } } return None; } fn token_balance_delta_for_account_index( meta: &serde_json::Value, account_index: usize, ) -> std::option::Option { let pre_amount = token_balance_amount_for_account_index(meta, "preTokenBalances", account_index); let pre_amount = match pre_amount { Some(pre_amount) => pre_amount, None => return None, }; let post_amount = token_balance_amount_for_account_index(meta, "postTokenBalances", account_index); let post_amount = match post_amount { Some(post_amount) => post_amount, None => return None, }; return Some(post_amount - pre_amount); } fn token_balance_amount_for_account_index( meta: &serde_json::Value, key: &str, account_index: usize, ) -> std::option::Option { let balances = match meta.get(key).and_then(|value| return value.as_array()) { Some(balances) => balances, None => return None, }; for balance in balances { let balance_index = balance.get("accountIndex").and_then(|value| return value.as_u64()); let balance_index = match balance_index { Some(balance_index) => balance_index, None => continue, }; let requested_index = match u64::try_from(account_index) { Ok(requested_index) => requested_index, Err(_) => return None, }; if balance_index != requested_index { continue; } let amount_text = balance .get("uiTokenAmount") .and_then(|value| return value.get("amount")) .and_then(|value| return value.as_str()); let amount_text = match amount_text { Some(amount_text) => amount_text, None => return None, }; let amount_result = amount_text.parse::(); match amount_result { Ok(amount) => return Some(amount), Err(_) => return None, } } return None; } #[cfg(test)] mod tests { fn transaction_json_with_vaults(base_vault: &str, quote_vault: &str) -> std::string::String { return serde_json::json!({ "transaction": { "message": { "accountKeys": [ base_vault, quote_vault ] } } }) .to_string(); } fn meta_json_with_vault_amounts( base_pre: &str, base_post: &str, quote_pre: &str, quote_post: &str, ) -> std::string::String { return serde_json::json!({ "err": null, "status": { "Ok": null }, "preTokenBalances": [ { "accountIndex": 0, "uiTokenAmount": { "amount": base_pre } }, { "accountIndex": 1, "uiTokenAmount": { "amount": quote_pre } } ], "postTokenBalances": [ { "accountIndex": 0, "uiTokenAmount": { "amount": base_post } }, { "accountIndex": 1, "uiTokenAmount": { "amount": quote_post } } ] }) .to_string(); } fn make_test_instruction( id: i64, parent_instruction_id: std::option::Option, instruction_index: u32, inner_instruction_index: std::option::Option, stack_height: std::option::Option, parsed_type: std::option::Option<&str>, parsed_json: std::option::Option, ) -> crate::ChainInstructionDto { return crate::ChainInstructionDto { id: Some(id), transaction_id: 10, parent_instruction_id, instruction_index, inner_instruction_index, program_id: None, program_name: None, stack_height, accounts_json: "[]".to_string(), data_json: None, parsed_type: parsed_type.map(|value| return value.to_string()), parsed_json, created_at: chrono::Utc::now(), }; } fn transfer_checked_json( mint: &str, amount: &str, source: &str, destination: &str, ) -> std::string::String { return serde_json::json!({ "info": { "mint": mint, "source": source, "destination": destination, "tokenAmount": { "amount": amount } }, "type": "transferChecked" }) .to_string(); } #[test] fn flattened_cpi_window_extracts_meteora_dlmm_transfer_checked_amounts() { let decoded_instruction = make_test_instruction(100, Some(90), 3, Some(1), Some(3), None, None); let instructions = vec![ decoded_instruction.clone(), make_test_instruction(101, Some(90), 3, Some(2), Some(4), None, None), make_test_instruction( 102, Some(90), 3, Some(3), Some(4), Some("transferChecked"), Some(transfer_checked_json("BASE", "5689283022", "UserBase", "BaseVault")), ), make_test_instruction( 103, Some(90), 3, Some(4), Some(4), Some("transferChecked"), Some(transfer_checked_json("QUOTE", "1322754129", "QuoteVault", "UserQuote")), ), make_test_instruction( 104, Some(90), 3, Some(5), Some(3), Some("transferChecked"), Some(transfer_checked_json("QUOTE", "999", "QuoteVault", "UserQuote")), ), ]; let resolution_result = super::resolve_amounts_from_flattened_cpi_transfer_window( &decoded_instruction, &instructions, Some("BASE"), Some("QUOTE"), Some("BaseVault"), Some("QuoteVault"), ); let resolution = match resolution_result { Ok(resolution) => resolution, Err(error) => panic!("flattened CPI extraction should succeed: {}", error), }; assert_eq!(resolution.base_amount_raw, Some("5689283022".to_string())); assert_eq!(resolution.quote_amount_raw, Some("1322754129".to_string())); assert_eq!(resolution.resolved_trade_side, Some(crate::SwapTradeSide::SellBase)); } #[test] fn flattened_cpi_window_extracts_top_level_meteora_damm_v1_transfer_checked_amounts() { let decoded_instruction = make_test_instruction(200, None, 4, None, Some(1), None, None); let instructions = vec![ decoded_instruction.clone(), make_test_instruction( 201, Some(200), 4, Some(0), Some(2), Some("transferChecked"), Some(transfer_checked_json("BASE", "250000000", "UserBase", "PoolBaseVault")), ), make_test_instruction( 202, Some(200), 4, Some(1), Some(2), Some("transferChecked"), Some(transfer_checked_json("QUOTE", "10000000", "PoolQuoteVault", "UserQuote")), ), ]; let resolution_result = super::resolve_amounts_from_flattened_cpi_transfer_window( &decoded_instruction, &instructions, Some("BASE"), Some("QUOTE"), None, None, ); let resolution = match resolution_result { Ok(resolution) => resolution, Err(error) => panic!("top-level flattened CPI extraction should succeed: {}", error), }; assert_eq!(resolution.base_amount_raw, Some("250000000".to_string())); assert_eq!(resolution.quote_amount_raw, Some("10000000".to_string())); assert_eq!(resolution.resolved_trade_side, None); } #[test] fn buy_base_is_inferred_when_base_vault_decreases_and_quote_vault_increases() { let transaction_json = transaction_json_with_vaults("base_vault", "quote_vault"); let meta_json = meta_json_with_vault_amounts("1000", "900", "5000", "5200"); let side = super::infer_trade_side_from_vault_balance_deltas( Some(meta_json.as_str()), transaction_json.as_str(), Some("base_vault"), Some("quote_vault"), ); assert_eq!(side, Some(crate::SwapTradeSide::BuyBase)); } #[test] fn sell_base_is_inferred_when_base_vault_increases_and_quote_vault_decreases() { let transaction_json = transaction_json_with_vaults("base_vault", "quote_vault"); let meta_json = meta_json_with_vault_amounts("1000", "1200", "5000", "4900"); let side = super::infer_trade_side_from_vault_balance_deltas( Some(meta_json.as_str()), transaction_json.as_str(), Some("base_vault"), Some("quote_vault"), ); assert_eq!(side, Some(crate::SwapTradeSide::SellBase)); } #[test] fn failed_transaction_does_not_infer_trade_side() { let transaction_json = transaction_json_with_vaults("base_vault", "quote_vault"); let meta_json = serde_json::json!({ "err": { "InstructionError": [3, {"Custom": 104}] }, "status": { "Err": { "InstructionError": [3, {"Custom": 104}] } }, "preTokenBalances": [], "postTokenBalances": [] }) .to_string(); let side = super::infer_trade_side_from_vault_balance_deltas( Some(meta_json.as_str()), transaction_json.as_str(), Some("base_vault"), Some("quote_vault"), ); assert_eq!(side, None); } }