This commit is contained in:
2026-05-11 11:02:47 +02:00
parent d66afede28
commit 7f130dba6b
49 changed files with 10301 additions and 8481 deletions

View File

@@ -0,0 +1,650 @@
// 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>,
}
/// 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;
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_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_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 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,
});
}
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(());
}
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_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),
};
match instruction_option {
Some(instruction) => return Ok(Some(instruction.instruction_index)),
None => return Ok(None),
}
}
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;
}