0.7.24-pre.0

This commit is contained in:
2026-05-02 11:27:10 +02:00
parent 60db521a88
commit aaff2dbd94
38 changed files with 3074 additions and 207 deletions

View File

@@ -74,7 +74,7 @@ impl KbTradeAggregationService {
};
let mut results = std::vec::Vec::new();
for decoded_event in &decoded_events {
if !decoded_event.event_kind.ends_with(".swap") {
if !kb_is_trade_event_kind(decoded_event.event_kind.as_str()) {
continue;
}
let decoded_event_id = match decoded_event.id {
@@ -135,6 +135,16 @@ impl KbTradeAggregationService {
)));
}
};
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 {
Ok(pool_tokens) => pool_tokens,
Err(error) => return Err(error),
};
let base_vault_address =
kb_find_pool_token_vault_address_by_token_id(&pool_tokens, pair.base_token_id);
let quote_vault_address =
kb_find_pool_token_vault_address_by_token_id(&pool_tokens, pair.quote_token_id);
let payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let payload = match payload_result {
@@ -146,8 +156,8 @@ impl KbTradeAggregationService {
)));
}
};
let trade_side = kb_extract_trade_side(&payload);
let base_amount_raw = kb_extract_amount_string(
let trade_side = kb_extract_trade_side(decoded_event.event_kind.as_str(), &payload);
let mut base_amount_raw = kb_extract_amount_string(
&payload,
&[
"baseAmountRaw",
@@ -157,7 +167,7 @@ impl KbTradeAggregationService {
"amountInBase",
],
);
let quote_amount_raw = kb_extract_amount_string(
let mut quote_amount_raw = kb_extract_amount_string(
&payload,
&[
"quoteAmountRaw",
@@ -167,45 +177,85 @@ impl KbTradeAggregationService {
"amountOutQuote",
],
);
let price_quote_per_base =
kb_compute_price_quote_per_base(base_amount_raw.clone(), quote_amount_raw.clone());
let slot_i64 = kb_convert_slot_to_i64(transaction.slot);
let created_trade_event = existing_trade_option.is_none();
let trade_event_id = if let Some(existing_trade) = existing_trade_option {
match existing_trade.id {
Some(trade_event_id) => trade_event_id,
None => {
return Err(crate::KbError::InvalidState(
"trade event has no internal id".to_string(),
));
}
}
} else {
let trade_event_dto = crate::KbTradeEventDto::new(
pool.dex_id,
pool_id,
pair_id,
transaction_id,
decoded_event_id,
transaction.signature.clone(),
slot_i64,
trade_side,
pair.base_token_id,
pair.quote_token_id,
base_amount_raw.clone(),
quote_amount_raw.clone(),
price_quote_per_base,
crate::KbObservationSourceKind::Dex,
transaction.source_endpoint_name.clone(),
decoded_event.payload_json.clone(),
let mut price_quote_per_base = None;
if 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 inferred_result = kb_extract_pump_swap_amounts_from_transaction(
transaction.transaction_json.as_str(),
transaction.meta_json.as_deref(),
base_vault_address.as_deref(),
quote_vault_address.as_deref(),
);
let upsert_result =
crate::upsert_trade_event(self.database.as_ref(), &trade_event_dto).await;
match upsert_result {
Ok(trade_event_id) => trade_event_id,
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 price_quote_per_base.is_none() {
price_quote_per_base = kb_compute_price_quote_per_base_with_decimals(
transaction.meta_json.as_deref(),
transaction.transaction_json.as_str(),
base_vault_address.as_deref(),
quote_vault_address.as_deref(),
);
}
let slot_i64 = kb_convert_slot_to_i64(transaction.slot);
let existing_trade_was_empty = match &existing_trade_option {
Some(existing_trade) => {
existing_trade.base_amount_raw.is_none()
&& existing_trade.quote_amount_raw.is_none()
&& existing_trade.price_quote_per_base.is_none()
}
None => false,
};
let trade_event_dto = crate::KbTradeEventDto::new(
pool.dex_id,
pool_id,
pair_id,
transaction_id,
decoded_event_id,
transaction.signature.clone(),
slot_i64,
trade_side,
pair.base_token_id,
pair.quote_token_id,
base_amount_raw.clone(),
quote_amount_raw.clone(),
price_quote_per_base,
crate::KbObservationSourceKind::Dex,
transaction.source_endpoint_name.clone(),
decoded_event.payload_json.clone(),
);
tracing::debug!(
event_kind = %decoded_event.event_kind,
pool_account = ?decoded_event.pool_account,
decoded_event_id = ?decoded_event.id,
"trade aggregation candidate"
);
let upsert_result =
crate::upsert_trade_event(self.database.as_ref(), &trade_event_dto).await;
let trade_event_id = match upsert_result {
Ok(trade_event_id) => trade_event_id,
Err(error) => return Err(error),
};
let created_trade_event = existing_trade_option.is_none();
let repaired_trade_event = !created_trade_event
&& existing_trade_was_empty
&& (base_amount_raw.is_some()
|| quote_amount_raw.is_some()
|| price_quote_per_base.is_some());
let pair_metric_result =
crate::get_pair_metric_by_pair_id(self.database.as_ref(), pair_id).await;
let pair_metric_option = match pair_metric_result {
@@ -221,7 +271,7 @@ impl KbTradeAggregationService {
));
}
};
if created_trade_event {
if created_trade_event || repaired_trade_event {
let mut updated_metric = existing_metric.clone();
kb_apply_trade_to_pair_metric(
&mut updated_metric,
@@ -310,6 +360,19 @@ impl KbTradeAggregationService {
}
}
fn kb_is_trade_event_kind(event_kind: &str) -> bool {
if event_kind.ends_with(".swap") {
return true;
}
if event_kind.ends_with(".buy") {
return true;
}
if event_kind.ends_with(".sell") {
return true;
}
false
}
fn kb_convert_slot_to_i64(slot: std::option::Option<u64>) -> std::option::Option<i64> {
match slot {
Some(slot) => match i64::try_from(slot) {
@@ -320,13 +383,20 @@ fn kb_convert_slot_to_i64(slot: std::option::Option<u64>) -> std::option::Option
}
}
fn kb_extract_trade_side(payload: &serde_json::Value) -> crate::KbSwapTradeSide {
fn kb_extract_trade_side(event_kind: &str, payload: &serde_json::Value) -> crate::KbSwapTradeSide {
let trade_side_option = kb_extract_string_by_candidate_keys(payload, &["tradeSide"]);
match trade_side_option.as_deref() {
Some("BuyBase") => crate::KbSwapTradeSide::BuyBase,
Some("SellBase") => crate::KbSwapTradeSide::SellBase,
_ => crate::KbSwapTradeSide::Unknown,
Some("BuyBase") => return crate::KbSwapTradeSide::BuyBase,
Some("SellBase") => return crate::KbSwapTradeSide::SellBase,
_ => {}
}
if event_kind.ends_with(".buy") {
return crate::KbSwapTradeSide::BuyBase;
}
if event_kind.ends_with(".sell") {
return crate::KbSwapTradeSide::SellBase;
}
crate::KbSwapTradeSide::Unknown
}
fn kb_extract_amount_string(
@@ -336,34 +406,6 @@ fn kb_extract_amount_string(
kb_extract_scalar_as_string_by_candidate_keys(payload, candidate_keys)
}
fn kb_compute_price_quote_per_base(
base_amount_raw: std::option::Option<std::string::String>,
quote_amount_raw: std::option::Option<std::string::String>,
) -> std::option::Option<f64> {
let base_amount_text = match base_amount_raw {
Some(base_amount_text) => base_amount_text,
None => return None,
};
let quote_amount_text = match quote_amount_raw {
Some(quote_amount_text) => quote_amount_text,
None => return None,
};
let base_amount_result = base_amount_text.parse::<f64>();
let base_amount = match base_amount_result {
Ok(base_amount) => base_amount,
Err(_) => return None,
};
if base_amount <= 0.0 {
return None;
}
let quote_amount_result = quote_amount_text.parse::<f64>();
let quote_amount = match quote_amount_result {
Ok(quote_amount) => quote_amount,
Err(_) => return None,
};
Some(quote_amount / base_amount)
}
fn kb_apply_trade_to_pair_metric(
metric: &mut crate::KbPairMetricDto,
slot: std::option::Option<i64>,
@@ -495,10 +537,292 @@ fn kb_extract_scalar_as_string_by_candidate_keys(
}
}
}
None
}
fn kb_find_pool_token_vault_address_by_token_id(
pool_tokens: &[crate::KbPoolTokenDto],
token_id: i64,
) -> std::option::Option<std::string::String> {
for pool_token in pool_tokens {
if pool_token.token_id != token_id {
continue;
}
let vault_address_option = pool_token.vault_address.clone();
let vault_address = match vault_address_option {
Some(vault_address) => vault_address.trim().to_string(),
None => continue,
};
if vault_address.is_empty() {
continue;
}
return Some(vault_address);
}
None
}
fn kb_extract_pump_swap_amounts_from_transaction(
transaction_json: &str,
meta_json: std::option::Option<&str>,
base_vault_address: std::option::Option<&str>,
quote_vault_address: std::option::Option<&str>,
) -> Result<
(
std::option::Option<std::string::String>,
std::option::Option<std::string::String>,
std::option::Option<f64>,
),
crate::KbError,
> {
let meta_json = match meta_json {
Some(meta_json) => meta_json,
None => return Ok((None, None, None)),
};
let transaction_value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
let transaction_value = match transaction_value_result {
Ok(transaction_value) => transaction_value,
Err(error) => {
return Err(crate::KbError::Json(format!(
"cannot parse transaction_json for pump_swap amount extraction: {}",
error
)));
}
};
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 pump_swap amount extraction: {}",
error
)));
}
};
let account_keys_result = kb_extract_transaction_account_keys(&transaction_value);
let account_keys = match account_keys_result {
Ok(account_keys) => account_keys,
Err(error) => return Err(error),
};
let pre_balances_result =
kb_extract_token_balance_map(&meta_value, &account_keys, "preTokenBalances");
let pre_balances = match pre_balances_result {
Ok(pre_balances) => pre_balances,
Err(error) => return Err(error),
};
let post_balances_result =
kb_extract_token_balance_map(&meta_value, &account_keys, "postTokenBalances");
let post_balances = match post_balances_result {
Ok(post_balances) => post_balances,
Err(error) => return Err(error),
};
let mut base_amount_raw = None;
let mut quote_amount_raw = None;
let mut price_quote_per_base = None;
if let Some(base_vault_address) = base_vault_address {
let base_pre = pre_balances.get(base_vault_address);
let base_post = post_balances.get(base_vault_address);
let base_pre_raw = base_pre.map(|value| value.0.clone());
let base_post_raw = base_post.map(|value| value.0.clone());
base_amount_raw = kb_compute_amount_delta_abs(base_pre_raw, base_post_raw);
let base_pre_ui = base_pre.and_then(|value| value.1);
let base_post_ui = base_post.and_then(|value| value.1);
let base_delta_ui = kb_compute_ui_delta_abs(base_pre_ui, base_post_ui);
if let Some(quote_vault_address) = quote_vault_address {
let quote_pre = pre_balances.get(quote_vault_address);
let quote_post = post_balances.get(quote_vault_address);
let quote_pre_raw = quote_pre.map(|value| value.0.clone());
let quote_post_raw = quote_post.map(|value| value.0.clone());
quote_amount_raw = kb_compute_amount_delta_abs(quote_pre_raw, quote_post_raw);
let quote_pre_ui = quote_pre.and_then(|value| value.1);
let quote_post_ui = quote_post.and_then(|value| value.1);
let quote_delta_ui = kb_compute_ui_delta_abs(quote_pre_ui, quote_post_ui);
match (base_delta_ui, quote_delta_ui) {
(Some(base_delta_ui), Some(quote_delta_ui)) => {
if base_delta_ui > 0.0 {
price_quote_per_base = Some(quote_delta_ui / base_delta_ui);
}
}
_ => {}
}
}
}
Ok((base_amount_raw, quote_amount_raw, price_quote_per_base))
}
fn kb_extract_transaction_account_keys(
transaction_value: &serde_json::Value,
) -> Result<std::vec::Vec<std::string::String>, crate::KbError> {
let candidate_arrays = [
transaction_value
.get("message")
.and_then(|value| value.get("accountKeys")),
transaction_value
.get("transaction")
.and_then(|value| value.get("message"))
.and_then(|value| value.get("accountKeys")),
transaction_value.get("accountKeys"),
];
for candidate_array_option in candidate_arrays {
let candidate_array = match candidate_array_option {
Some(candidate_array) => candidate_array,
None => continue,
};
let array = match candidate_array.as_array() {
Some(array) => array,
None => continue,
};
let mut account_keys = std::vec::Vec::new();
for item in array {
if let Some(value) = item.as_str() {
account_keys.push(value.to_string());
continue;
}
let pubkey_option = item.get("pubkey").and_then(|value| value.as_str());
if let Some(pubkey) = pubkey_option {
account_keys.push(pubkey.to_string());
continue;
}
}
if !account_keys.is_empty() {
return Ok(account_keys);
}
}
Err(crate::KbError::Json(
"cannot extract accountKeys from transaction_json".to_string(),
))
}
fn kb_extract_token_balance_map(
meta_value: &serde_json::Value,
account_keys: &[std::string::String],
field_name: &str,
) -> Result<
std::collections::BTreeMap<
std::string::String,
(std::string::String, std::option::Option<f64>),
>,
crate::KbError,
> {
let mut result = std::collections::BTreeMap::<
std::string::String,
(std::string::String, std::option::Option<f64>),
>::new();
let balances_option = meta_value
.get(field_name)
.and_then(|value| value.as_array());
let balances = match balances_option {
Some(balances) => balances,
None => return Ok(result),
};
for balance in balances {
let account_index_option = balance.get("accountIndex").and_then(|value| value.as_u64());
let account_index = match account_index_option {
Some(account_index) => account_index as usize,
None => continue,
};
if account_index >= account_keys.len() {
continue;
}
let account_address = account_keys[account_index].clone();
let ui_token_amount_option = balance.get("uiTokenAmount");
let ui_token_amount = match ui_token_amount_option {
Some(ui_token_amount) => ui_token_amount,
None => continue,
};
let raw_amount_option = ui_token_amount
.get("amount")
.and_then(|value| value.as_str());
let raw_amount = match raw_amount_option {
Some(raw_amount) => raw_amount.to_string(),
None => continue,
};
let ui_amount_string_option = ui_token_amount
.get("uiAmountString")
.and_then(|value| value.as_str());
let ui_amount = match ui_amount_string_option {
Some(ui_amount_string) => {
let parse_result = ui_amount_string.parse::<f64>();
match parse_result {
Ok(ui_amount) => Some(ui_amount),
Err(_) => None,
}
}
None => None,
};
result.insert(account_address, (raw_amount, ui_amount));
}
Ok(result)
}
fn kb_compute_amount_delta_abs(
pre_amount: std::option::Option<std::string::String>,
post_amount: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
let pre_amount = match pre_amount {
Some(pre_amount) => pre_amount,
None => "0".to_string(),
};
let post_amount = match post_amount {
Some(post_amount) => post_amount,
None => "0".to_string(),
};
let pre_value_result = pre_amount.parse::<i128>();
let pre_value = match pre_value_result {
Ok(pre_value) => pre_value,
Err(_) => return None,
};
let post_value_result = post_amount.parse::<i128>();
let post_value = match post_value_result {
Ok(post_value) => post_value,
Err(_) => return None,
};
let delta = if post_value >= pre_value {
post_value - pre_value
} else {
pre_value - post_value
};
Some(delta.to_string())
}
fn kb_compute_ui_delta_abs(
pre_amount: std::option::Option<f64>,
post_amount: std::option::Option<f64>,
) -> std::option::Option<f64> {
let pre_amount = match pre_amount {
Some(pre_amount) => pre_amount,
None => 0.0,
};
let post_amount = match post_amount {
Some(post_amount) => post_amount,
None => 0.0,
};
let delta = if post_amount >= pre_amount {
post_amount - pre_amount
} else {
pre_amount - post_amount
};
Some(delta)
}
fn kb_compute_price_quote_per_base_with_decimals(
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<f64> {
let inferred_result = kb_extract_pump_swap_amounts_from_transaction(
transaction_json,
meta_json,
base_vault_address,
quote_vault_address,
);
let inferred = match inferred_result {
Ok(inferred) => inferred,
Err(_) => return None,
};
inferred.2
}
#[cfg(test)]
mod tests {
async fn make_database() -> std::sync::Arc<crate::KbDatabase> {