Files
khadhroony-bobobot/kb_lib/src/trade_amount_resolution.rs
2026-05-21 09:46:54 +02:00

1704 lines
62 KiB
Rust

// 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<u8>,
/// Quote token decimals, when known.
pub(crate) quote_token_decimals: std::option::Option<u8>,
/// 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<std::string::String>,
/// Quote amount in raw token units.
pub(crate) quote_amount_raw: std::option::Option<std::string::String>,
/// Quote/base price.
pub(crate) price_quote_per_base: std::option::Option<f64>,
/// Trade side resolved from balance deltas, when available.
pub(crate) resolved_trade_side: std::option::Option<crate::SwapTradeSide>,
}
/// Resolves trade amounts from payload and protocol-specific fallbacks.
pub(crate) async fn resolve_trade_amounts(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
) -> Result<crate::trade_amount_resolution::TradeAmountResolution, crate::Error> {
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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
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<std::string::String>,
quote_amount_raw: std::option::Option<std::string::String>,
resolved_trade_side: std::option::Option<crate::SwapTradeSide>,
}
async fn apply_meteora_dlmm_flattened_cpi_amount_fallback(
input: &crate::trade_amount_resolution::TradeAmountResolutionInput<'_>,
base_amount_raw: &mut std::option::Option<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
) -> 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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
) -> 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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
resolved_trade_side: &mut std::option::Option<crate::SwapTradeSide>,
) -> 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<crate::trade_amount_resolution::FlattenedCpiTransferAmountResolution, crate::Error> {
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<u32>,
) -> 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<u32> {
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::trade_amount_resolution::ParsedTransferCheckedInstruction>,
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::<serde_json::Value>(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<crate::trade_amount_resolution::VaultTransferDirection> {
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<crate::SwapTradeSide> {
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<i64>) -> std::string::String {
match instruction_id {
Some(instruction_id) => return instruction_id.to_string(),
None => return "<none>".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<std::string::String>,
quote_amount_raw: &mut std::option::Option<std::string::String>,
price_quote_per_base: &mut std::option::Option<f64>,
) -> Result<(), crate::Error> {
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<std::option::Option<u32>, 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<std::option::Option<crate::ChainInstructionDto>, 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<std::string::String> {
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<std::string::String> {
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<std::string::String> {
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<crate::SwapTradeSide> {
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::<serde_json::Value>(meta_text);
let meta = match meta_result {
Ok(meta) => meta,
Err(_) => return None,
};
let transaction_result = serde_json::from_str::<serde_json::Value>(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<std::string::String> {
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<std::string::String>,
) {
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<std::string::String>,
) {
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<std::string::String>,
) {
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<usize> {
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<i128> {
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<i128> {
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::<i128>();
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<i64>,
instruction_index: u32,
inner_instruction_index: std::option::Option<u32>,
stack_height: std::option::Option<u32>,
parsed_type: std::option::Option<&str>,
parsed_json: std::option::Option<std::string::String>,
) -> 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);
}
}