This commit is contained in:
2026-06-05 14:53:16 +02:00
parent 27e25d5bf4
commit f81e0f3bea
66 changed files with 7655 additions and 214 deletions

View File

@@ -19,6 +19,8 @@ pub struct NonTradeEventMaterializationResult {
pub reward_event_count: usize,
/// Number of pool administration events inserted or refreshed.
pub pool_admin_event_count: usize,
/// Number of orderbook or limit-order events inserted or refreshed.
pub orderbook_event_count: usize,
}
/// Materializes useful non-trade decoded DEX events.
@@ -61,7 +63,7 @@ impl NonTradeEventMaterializationService {
)));
},
};
if transaction.err_json.is_some() {
if transaction_has_effective_error(&transaction) {
tracing::debug!(
signature = %transaction.signature,
"skipping non-trade materialization for failed transaction"
@@ -189,6 +191,24 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
}
}
if crate::is_dex_orderbook_event_kind(decoded_event.event_kind.as_str()) {
let materialized = self
.materialize_orderbook_event(
&transaction,
transaction_id,
decoded_event,
&payload,
)
.await;
match materialized {
Ok(was_materialized) => {
if was_materialized {
result.orderbook_event_count += 1;
}
},
Err(error) => return Err(error),
}
}
}
for decoded_event in &decoded_events {
if !decoded_event.event_kind.ends_with(".lp_change_event") {
@@ -673,6 +693,86 @@ WHERE decoded_event_id = ?
}
}
async fn materialize_orderbook_event(
&self,
transaction: &crate::ChainTransactionDto,
transaction_id: i64,
decoded_event: &crate::DexDecodedEventDto,
payload: &serde_json::Value,
) -> Result<bool, crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(false),
};
let context = self.resolve_decoded_event_context(decoded_event).await;
let context = match context {
Ok(context) => context,
Err(error) => return Err(error),
};
let order_action = normalize_orderbook_action(decoded_event.event_kind.as_str());
let actor_wallet = extract_first_string(
payload,
&["actorWallet", "actor_wallet", "owner", "authority", "payer", "user"],
);
let order_account = match extract_first_string(
payload,
&["orderAccount", "order_account", "limitOrder", "limit_order", "order"],
) {
Some(order_account) => Some(order_account),
None => fallback_order_account(decoded_event.event_kind.as_str(), payload),
};
let amount_raw = extract_first_amount_string(
payload,
&[
"amountRaw",
"amount_raw",
"amount",
"decreasedAmountRaw",
"decreased_amount_raw",
"decreasedAmount",
"increasedAmountRaw",
"increased_amount_raw",
"increasedAmount",
],
);
let amount_min_raw = extract_first_amount_string(
payload,
&["amountMinRaw", "amount_min_raw", "amountMin", "amount_min"],
);
let tick_index = extract_first_i64(payload, &["tickIndex", "tick_index"]);
let zero_for_one = extract_first_bool(payload, &["zeroForOne", "zero_for_one"]);
let dto = crate::OrderbookEventDto::new(
transaction_id,
Some(decoded_event_id),
context.dex_id,
context.pool_id,
context.pair_id,
transaction.signature.clone(),
transaction.slot,
decoded_event.protocol_name.clone(),
decoded_event.program_id.clone(),
decoded_event.event_kind.clone(),
order_action,
decoded_event.pool_account.clone(),
decoded_event.market_account.clone(),
actor_wallet,
order_account,
decoded_event.token_a_mint.clone(),
decoded_event.token_b_mint.clone(),
amount_raw,
amount_min_raw,
tick_index,
zero_for_one,
decoded_event.payload_json.clone(),
);
let upsert_result =
crate::query_orderbook_events_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => return Ok(true),
Err(error) => return Err(error),
}
}
async fn ensure_liquidity_context_from_decoded_event(
&self,
decoded_event: &crate::DexDecodedEventDto,
@@ -789,6 +889,162 @@ WHERE decoded_event_id = ?
}
}
fn normalize_orderbook_action(event_kind: &str) -> std::string::String {
if event_kind.contains(".open_limit_order") {
return "open_limit_order".to_string();
}
if event_kind.contains(".increase_limit_order") {
return "increase_limit_order".to_string();
}
if event_kind.contains(".decrease_limit_order") {
return "decrease_limit_order".to_string();
}
if event_kind.contains(".close_limit_order") {
return "close_limit_order".to_string();
}
if event_kind.contains(".settle_limit_order") {
return "settle_limit_order".to_string();
}
if event_kind.contains("order_place") {
return "order_place".to_string();
}
if event_kind.contains("order_cancel") {
return "order_cancel".to_string();
}
if event_kind.contains("settle_funds") {
return "settle_funds".to_string();
}
return event_kind.to_string();
}
fn fallback_order_account(
event_kind: &str,
payload: &serde_json::Value,
) -> std::option::Option<std::string::String> {
if event_kind.contains(".close_limit_order") {
return extract_account_at(payload, 2);
}
if event_kind.contains(".open_limit_order")
|| event_kind.contains(".increase_limit_order")
|| event_kind.contains(".decrease_limit_order")
{
return extract_account_at(payload, 3);
}
return None;
}
fn extract_account_at(
value: &serde_json::Value,
index: usize,
) -> std::option::Option<std::string::String> {
if let Some(object) = value.as_object() {
let accounts = object.get("accounts");
if let Some(accounts) = accounts {
if let Some(array) = accounts.as_array() {
let candidate = array.get(index);
if let Some(candidate) = candidate {
if let Some(text) = candidate.as_str() {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
}
}
for nested_value in object.values() {
let nested = extract_account_at(nested_value, index);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn extract_first_i64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<i64> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
if let Some(candidate_value) = candidate_value {
if let Some(number) = candidate_value.as_i64() {
return Some(number);
}
if let Some(number) = candidate_value.as_u64() {
let converted = i64::try_from(number);
if let Ok(converted) = converted {
return Some(converted);
}
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<i64>();
if let Ok(parsed) = parsed {
return Some(parsed);
}
}
}
}
for nested_value in object.values() {
let nested = extract_first_i64(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn extract_first_bool(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<bool> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
if let Some(candidate_value) = candidate_value {
if let Some(flag) = candidate_value.as_bool() {
return Some(flag);
}
if let Some(number) = candidate_value.as_i64() {
return Some(number != 0);
}
if let Some(text) = candidate_value.as_str() {
if text == "true" || text == "1" {
return Some(true);
}
if text == "false" || text == "0" {
return Some(false);
}
}
}
}
for nested_value in object.values() {
let nested = extract_first_bool(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {
Some(err_json) => err_json.trim(),
None => return false,
};
if err_json.is_empty() {
return false;
}
if err_json == "null" {
return false;
}
return true;
}
fn extract_first_u64(
value: &serde_json::Value,
candidate_keys: &[&str],
@@ -902,6 +1158,28 @@ fn extract_first_number_as_string(
#[cfg(test)]
mod tests {
#[test]
fn blank_or_null_err_json_is_not_effective_failure() {
let mut transaction = crate::ChainTransactionDto::new(
"sig-non-trade-effective-error".to_string(),
Some(1),
None,
None,
None,
None,
None,
"{}".to_string(),
);
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("".to_string());
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("null".to_string());
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("{\"InstructionError\":[0,\"Custom\"]}".to_string());
assert!(super::transaction_has_effective_error(&transaction));
}
#[test]
fn extracts_nested_liquidity_amounts() {
let payload = serde_json::json!({