This commit is contained in:
2026-05-05 20:49:45 +02:00
parent f2c227e08f
commit 348e76660c
28 changed files with 3279 additions and 210 deletions

View File

@@ -138,6 +138,20 @@ impl KbTradeAggregationService {
)));
},
};
let base_token_result =
crate::get_token_by_id(self.database.as_ref(), pair.base_token_id).await;
let base_token_decimals = match base_token_result {
Ok(Some(token)) => token.decimals,
Ok(None) => None,
Err(error) => return Err(error),
};
let quote_token_result =
crate::get_token_by_id(self.database.as_ref(), pair.quote_token_id).await;
let quote_token_decimals = match quote_token_result {
Ok(Some(token)) => token.decimals,
Ok(None) => None,
Err(error) => return Err(error),
};
let pool_tokens_result =
crate::list_pool_tokens_by_pool_id(self.database.as_ref(), pool_id).await;
let pool_tokens = match pool_tokens_result {
@@ -247,10 +261,80 @@ impl KbTradeAggregationService {
price_quote_per_base = inferred.2;
}
}
if decoded_event.event_kind.starts_with("raydium_cpmm.")
if (decoded_event.event_kind.starts_with("raydium_cpmm.")
|| 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 decoded_instruction_index = match decoded_event.instruction_id {
Some(instruction_id) => {
let instruction_result = crate::get_chain_instruction_by_id(
self.database.as_ref(),
instruction_id,
)
.await;
let instruction_option = match instruction_result {
Ok(instruction_option) => instruction_option,
Err(error) => return Err(error),
};
match instruction_option {
Some(instruction) => Some(instruction.instruction_index),
None => None,
}
},
None => None,
};
let payload_input_vault_address =
kb_extract_string_by_candidate_keys(&payload, &["inputVault", "input_vault"]);
let payload_output_vault_address =
kb_extract_string_by_candidate_keys(&payload, &["outputVault", "output_vault"]);
let payload_input_token_account = kb_extract_string_by_candidate_keys(
&payload,
&["inputTokenAccount", "input_token_account"],
);
let payload_output_token_account = kb_extract_string_by_candidate_keys(
&payload,
&["outputTokenAccount", "output_token_account"],
);
let payload_base_vault_address =
kb_extract_string_by_candidate_keys(&payload, &["baseVault", "base_vault"]);
let payload_quote_vault_address =
kb_extract_string_by_candidate_keys(&payload, &["quoteVault", "quote_vault"]);
let effective_base_vault_address = match base_vault_address.as_deref() {
Some(base_vault_address) => Some(base_vault_address),
None => payload_base_vault_address.as_deref(),
};
let effective_quote_vault_address = match quote_vault_address.as_deref() {
Some(quote_vault_address) => Some(quote_vault_address),
None => payload_quote_vault_address.as_deref(),
};
let inferred_result = kb_extract_trade_amounts_from_instruction_token_transfers(
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;
}
}
if decoded_event.event_kind.starts_with("raydium_cpmm.")
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
let inferred_result = kb_extract_trade_amounts_from_vault_balance_deltas(
transaction.transaction_json.as_str(),
@@ -273,9 +357,7 @@ impl KbTradeAggregationService {
}
}
if decoded_event.event_kind.starts_with("raydium_clmm.")
&& (base_amount_raw.is_none()
|| quote_amount_raw.is_none()
|| price_quote_per_base.is_none())
&& (base_amount_raw.is_none() || quote_amount_raw.is_none())
{
let inferred_result = kb_extract_trade_amounts_from_vault_balance_deltas(
transaction.transaction_json.as_str(),
@@ -297,6 +379,15 @@ impl KbTradeAggregationService {
price_quote_per_base = inferred.2;
}
}
if price_quote_per_base.is_none() {
price_quote_per_base =
kb_compute_price_quote_per_base_from_raw_amounts_with_decimals(
base_amount_raw.as_deref(),
quote_amount_raw.as_deref(),
base_token_decimals,
quote_token_decimals,
);
}
if price_quote_per_base.is_none() {
price_quote_per_base = kb_compute_price_quote_per_base_with_decimals(
transaction.meta_json.as_deref(),
@@ -884,6 +975,182 @@ fn kb_extract_trade_amounts_from_vault_balance_deltas(
return Ok((base_amount_raw, quote_amount_raw, price_quote_per_base));
}
fn kb_extract_trade_amounts_from_instruction_token_transfers(
meta_json: std::option::Option<&str>,
instruction_index: std::option::Option<u32>,
input_vault_address: std::option::Option<&str>,
output_vault_address: std::option::Option<&str>,
input_token_account: std::option::Option<&str>,
output_token_account: std::option::Option<&str>,
base_vault_address: std::option::Option<&str>,
quote_vault_address: std::option::Option<&str>,
) -> Result<KbExtractedTradeAmounts, crate::KbError> {
let meta_json = match meta_json {
Some(meta_json) => meta_json,
None => return Ok((None, None, None)),
};
let instruction_index = match instruction_index {
Some(instruction_index) => u64::from(instruction_index),
None => return Ok((None, None, None)),
};
let input_vault_address = match input_vault_address {
Some(input_vault_address) => input_vault_address.trim(),
None => return Ok((None, None, None)),
};
let output_vault_address = match output_vault_address {
Some(output_vault_address) => output_vault_address.trim(),
None => return Ok((None, None, None)),
};
let input_token_account = match input_token_account {
Some(input_token_account) => input_token_account.trim(),
None => return Ok((None, None, None)),
};
let output_token_account = match output_token_account {
Some(output_token_account) => output_token_account.trim(),
None => return Ok((None, None, None)),
};
let base_vault_address = match base_vault_address {
Some(base_vault_address) => base_vault_address.trim(),
None => return Ok((None, None, None)),
};
let quote_vault_address = match quote_vault_address {
Some(quote_vault_address) => quote_vault_address.trim(),
None => return Ok((None, None, None)),
};
if input_vault_address.is_empty()
|| output_vault_address.is_empty()
|| input_token_account.is_empty()
|| output_token_account.is_empty()
|| base_vault_address.is_empty()
|| quote_vault_address.is_empty()
{
return Ok((None, None, None));
}
let meta_value_result = serde_json::from_str::<serde_json::Value>(meta_json);
let meta_value = match meta_value_result {
Ok(meta_value) => meta_value,
Err(error) => {
return Err(crate::KbError::Json(format!(
"cannot parse meta_json for instruction-scoped token transfer amount extraction: {}",
error
)));
},
};
let inner_groups_option =
meta_value.get("innerInstructions").and_then(|value| return value.as_array());
let inner_groups = match inner_groups_option {
Some(inner_groups) => inner_groups,
None => return Ok((None, None, None)),
};
let mut input_amount_raw = None;
let mut output_amount_raw = None;
for inner_group in inner_groups {
let group_index_option = inner_group.get("index").and_then(|value| return value.as_u64());
let group_index = match group_index_option {
Some(group_index) => group_index,
None => continue,
};
if group_index != instruction_index {
continue;
}
let instructions_option =
inner_group.get("instructions").and_then(|value| return value.as_array());
let instructions = match instructions_option {
Some(instructions) => instructions,
None => continue,
};
for instruction in instructions {
if !kb_is_spl_token_transfer_instruction(instruction) {
continue;
}
let parsed_option = instruction.get("parsed");
let parsed = match parsed_option {
Some(parsed) => parsed,
None => continue,
};
let info_option = parsed.get("info");
let info = match info_option {
Some(info) => info,
None => continue,
};
let source_option = kb_extract_string_by_candidate_keys(info, &["source"]);
let source = match source_option {
Some(source) => source,
None => continue,
};
let destination_option = kb_extract_string_by_candidate_keys(info, &["destination"]);
let destination = match destination_option {
Some(destination) => destination,
None => continue,
};
let amount_option = kb_extract_scalar_as_string_by_candidate_keys(info, &["amount"]);
let amount = match amount_option {
Some(amount) => amount,
None => continue,
};
if input_amount_raw.is_none()
&& kb_account_equals(source.as_str(), input_token_account)
&& kb_account_equals(destination.as_str(), input_vault_address)
{
input_amount_raw = Some(amount.clone());
continue;
}
if output_amount_raw.is_none()
&& kb_account_equals(source.as_str(), output_vault_address)
&& kb_account_equals(destination.as_str(), output_token_account)
{
output_amount_raw = Some(amount);
continue;
}
}
}
if input_amount_raw.is_none() && output_amount_raw.is_none() {
return Ok((None, None, None));
}
if kb_account_equals(input_vault_address, base_vault_address)
&& kb_account_equals(output_vault_address, quote_vault_address)
{
return Ok((input_amount_raw, output_amount_raw, None));
}
if kb_account_equals(input_vault_address, quote_vault_address)
&& kb_account_equals(output_vault_address, base_vault_address)
{
return Ok((output_amount_raw, input_amount_raw, None));
}
return Ok((None, None, None));
}
fn kb_is_spl_token_transfer_instruction(instruction: &serde_json::Value) -> bool {
let program_id_option = instruction.get("programId").and_then(|value| return value.as_str());
if let Some(program_id) = program_id_option {
let spl_token_program_id = crate::SPL_TOKEN_PROGRAM_ID.to_string();
let spl_token_2022_program_id = crate::SPL_TOKEN_2022_PROGRAM_ID.to_string();
if program_id != spl_token_program_id.as_str()
&& program_id != spl_token_2022_program_id.as_str()
{
return false;
}
}
let parsed_type_option = instruction
.get("parsed")
.and_then(|parsed| return parsed.get("type"))
.and_then(|value| return value.as_str());
match parsed_type_option {
Some("transfer") => return true,
Some("transferChecked") => return true,
_ => return false,
}
}
fn kb_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 kb_extract_pump_fun_amounts_from_transaction(
transaction_json: &str,
meta_json: std::option::Option<&str>,
@@ -1167,6 +1434,57 @@ fn kb_compute_ui_delta_abs(
return Some(delta);
}
fn kb_compute_price_quote_per_base_from_raw_amounts_with_decimals(
base_amount_raw: std::option::Option<&str>,
quote_amount_raw: std::option::Option<&str>,
base_decimals: std::option::Option<u8>,
quote_decimals: std::option::Option<u8>,
) -> std::option::Option<f64> {
let base_decimals = match base_decimals {
Some(base_decimals) => base_decimals,
None => return None,
};
let quote_decimals = match quote_decimals {
Some(quote_decimals) => quote_decimals,
None => return None,
};
let base_amount_raw = match base_amount_raw {
Some(base_amount_raw) => base_amount_raw.trim(),
None => return None,
};
let quote_amount_raw = match quote_amount_raw {
Some(quote_amount_raw) => quote_amount_raw.trim(),
None => return None,
};
if base_amount_raw.is_empty() || quote_amount_raw.is_empty() {
return None;
}
let base_amount_result = base_amount_raw.parse::<f64>();
let base_amount = match base_amount_result {
Ok(base_amount) => base_amount,
Err(_) => return None,
};
let quote_amount_result = quote_amount_raw.parse::<f64>();
let quote_amount = match quote_amount_result {
Ok(quote_amount) => quote_amount,
Err(_) => return None,
};
if base_amount <= 0.0 || quote_amount <= 0.0 {
return None;
}
let base_scale = 10_f64.powi(i32::from(base_decimals));
let quote_scale = 10_f64.powi(i32::from(quote_decimals));
if base_scale <= 0.0 || quote_scale <= 0.0 {
return None;
}
let base_ui_amount = base_amount / base_scale;
let quote_ui_amount = quote_amount / quote_scale;
if base_ui_amount <= 0.0 || quote_ui_amount <= 0.0 {
return None;
}
return Some(quote_ui_amount / base_ui_amount);
}
fn kb_compute_price_quote_per_base_from_raw_amounts(
base_amount_raw: std::option::Option<&str>,
quote_amount_raw: std::option::Option<&str>,