0.7.34
This commit is contained in:
490
kb_lib/src/non_trade_event_materialization.rs
Normal file
490
kb_lib/src/non_trade_event_materialization.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
// file: kb_lib/src/non_trade_event_materialization.rs
|
||||
|
||||
//! Materialization of useful non-trade DEX events.
|
||||
//!
|
||||
//! This service persists liquidity and pool lifecycle events from already
|
||||
//! decoded DEX events. It deliberately does not feed trade, metric or candle
|
||||
//! materialization.
|
||||
|
||||
/// Result of non-trade event materialization for one transaction.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NonTradeEventMaterializationResult {
|
||||
/// Number of liquidity events inserted or refreshed.
|
||||
pub liquidity_event_count: usize,
|
||||
/// Number of pool lifecycle events inserted or refreshed.
|
||||
pub pool_lifecycle_event_count: usize,
|
||||
}
|
||||
|
||||
/// Materializes useful non-trade decoded DEX events.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NonTradeEventMaterializationService {
|
||||
database: std::sync::Arc<crate::Database>,
|
||||
}
|
||||
|
||||
struct NonTradeDecodedEventContext {
|
||||
dex_id: std::option::Option<i64>,
|
||||
pool_id: std::option::Option<i64>,
|
||||
pair_id: std::option::Option<i64>,
|
||||
pair: std::option::Option<crate::PairDto>,
|
||||
}
|
||||
|
||||
impl NonTradeEventMaterializationService {
|
||||
/// Creates a new non-trade event materialization service.
|
||||
pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
|
||||
return Self { database };
|
||||
}
|
||||
|
||||
/// Materializes useful non-trade events for one persisted transaction signature.
|
||||
pub async fn record_transaction_by_signature(
|
||||
&self,
|
||||
signature: &str,
|
||||
) -> Result<crate::NonTradeEventMaterializationResult, crate::Error> {
|
||||
let transaction_result =
|
||||
crate::query_chain_transactions_get_by_signature(self.database.as_ref(), signature)
|
||||
.await;
|
||||
let transaction_option = match transaction_result {
|
||||
Ok(transaction_option) => transaction_option,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let transaction = match transaction_option {
|
||||
Some(transaction) => transaction,
|
||||
None => {
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"cannot materialize non-trade events for unknown transaction '{}'",
|
||||
signature
|
||||
)));
|
||||
},
|
||||
};
|
||||
if transaction.err_json.is_some() {
|
||||
tracing::debug!(
|
||||
signature = %transaction.signature,
|
||||
"skipping non-trade materialization for failed transaction"
|
||||
);
|
||||
return Ok(crate::NonTradeEventMaterializationResult::default());
|
||||
}
|
||||
let transaction_id = match transaction.id {
|
||||
Some(transaction_id) => transaction_id,
|
||||
None => {
|
||||
return Err(crate::Error::InvalidState(format!(
|
||||
"transaction '{}' has no internal id",
|
||||
transaction.signature
|
||||
)));
|
||||
},
|
||||
};
|
||||
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
|
||||
self.database.as_ref(),
|
||||
transaction_id,
|
||||
)
|
||||
.await;
|
||||
let decoded_events = match decoded_events_result {
|
||||
Ok(decoded_events) => decoded_events,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut result = crate::NonTradeEventMaterializationResult::default();
|
||||
for decoded_event in &decoded_events {
|
||||
let payload_result =
|
||||
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
|
||||
let payload = match payload_result {
|
||||
Ok(payload) => payload,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
signature = %transaction.signature,
|
||||
event_kind = %decoded_event.event_kind,
|
||||
error = %error,
|
||||
"skipping non-trade materialization for invalid decoded payload"
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str()) {
|
||||
let materialized = self
|
||||
.materialize_liquidity_event(
|
||||
&transaction,
|
||||
transaction_id,
|
||||
decoded_event,
|
||||
&payload,
|
||||
)
|
||||
.await;
|
||||
match materialized {
|
||||
Ok(was_materialized) => {
|
||||
if was_materialized {
|
||||
result.liquidity_event_count += 1;
|
||||
}
|
||||
},
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
|
||||
let materialized = self
|
||||
.materialize_pool_lifecycle_event(&transaction, transaction_id, decoded_event)
|
||||
.await;
|
||||
match materialized {
|
||||
Ok(was_materialized) => {
|
||||
if was_materialized {
|
||||
result.pool_lifecycle_event_count += 1;
|
||||
}
|
||||
},
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
async fn materialize_pool_lifecycle_event(
|
||||
&self,
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
transaction_id: i64,
|
||||
decoded_event: &crate::DexDecodedEventDto,
|
||||
) -> 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 dto = crate::PoolLifecycleEventDto::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(),
|
||||
decoded_event.pool_account.clone(),
|
||||
decoded_event.token_a_mint.clone(),
|
||||
decoded_event.token_b_mint.clone(),
|
||||
decoded_event.payload_json.clone(),
|
||||
);
|
||||
let upsert_result =
|
||||
crate::query_pool_lifecycle_events_upsert(self.database.as_ref(), &dto).await;
|
||||
match upsert_result {
|
||||
Ok(_) => return Ok(true),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn materialize_liquidity_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 dex_id = match context.dex_id {
|
||||
Some(dex_id) => dex_id,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let pool_id = match context.pool_id {
|
||||
Some(pool_id) => pool_id,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let pair = match context.pair {
|
||||
Some(pair) => pair,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let pair_id = match pair.id {
|
||||
Some(pair_id) => Some(pair_id),
|
||||
None => None,
|
||||
};
|
||||
let event_kind = if crate::is_dex_position_open_event_kind(decoded_event.event_kind.as_str()) {
|
||||
crate::LiquidityEventKind::PositionOpen
|
||||
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
|
||||
crate::LiquidityEventKind::PositionClose
|
||||
} else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) {
|
||||
crate::LiquidityEventKind::Remove
|
||||
} else {
|
||||
crate::LiquidityEventKind::Add
|
||||
};
|
||||
let actor_wallet = extract_first_string(
|
||||
payload,
|
||||
&[
|
||||
"actorWallet",
|
||||
"actor_wallet",
|
||||
"user",
|
||||
"owner",
|
||||
"payer",
|
||||
"authority",
|
||||
"liquidityProvider",
|
||||
"liquidity_provider",
|
||||
],
|
||||
);
|
||||
let base_amount = extract_first_amount_string(
|
||||
payload,
|
||||
&[
|
||||
"baseAmountRaw",
|
||||
"base_amount_raw",
|
||||
"baseAmount",
|
||||
"base_amount",
|
||||
"amountBase",
|
||||
"amount_base",
|
||||
"tokenAAmount",
|
||||
"token_a_amount",
|
||||
"amountA",
|
||||
"amount_a",
|
||||
],
|
||||
);
|
||||
let quote_amount = extract_first_amount_string(
|
||||
payload,
|
||||
&[
|
||||
"quoteAmountRaw",
|
||||
"quote_amount_raw",
|
||||
"quoteAmount",
|
||||
"quote_amount",
|
||||
"amountQuote",
|
||||
"amount_quote",
|
||||
"tokenBAmount",
|
||||
"token_b_amount",
|
||||
"amountB",
|
||||
"amount_b",
|
||||
],
|
||||
);
|
||||
let lp_amount = extract_first_amount_string(
|
||||
payload,
|
||||
&[
|
||||
"lpAmountRaw",
|
||||
"lp_amount_raw",
|
||||
"lpAmount",
|
||||
"lp_amount",
|
||||
"liquidity",
|
||||
"liquidityAmount",
|
||||
"liquidity_amount",
|
||||
],
|
||||
);
|
||||
let amounts_are_complete = base_amount.is_some() && quote_amount.is_some();
|
||||
let base_amount_value = match base_amount {
|
||||
Some(base_amount_value) => base_amount_value,
|
||||
None => "0".to_string(),
|
||||
};
|
||||
let quote_amount_value = match quote_amount {
|
||||
Some(quote_amount_value) => quote_amount_value,
|
||||
None => "0".to_string(),
|
||||
};
|
||||
let dto = crate::LiquidityEventDto::new(
|
||||
dex_id,
|
||||
pool_id,
|
||||
pair_id,
|
||||
transaction.signature.clone(),
|
||||
decoded_event_id,
|
||||
transaction.slot,
|
||||
event_kind,
|
||||
actor_wallet,
|
||||
pair.base_token_id,
|
||||
pair.quote_token_id,
|
||||
None,
|
||||
base_amount_value,
|
||||
quote_amount_value,
|
||||
lp_amount,
|
||||
)
|
||||
.with_decoded_event_metadata(
|
||||
Some(transaction_id),
|
||||
Some(decoded_event_id),
|
||||
Some(decoded_event.program_id.clone()),
|
||||
Some(decoded_event.event_kind.clone()),
|
||||
Some(decoded_event.payload_json.clone()),
|
||||
amounts_are_complete,
|
||||
);
|
||||
let upsert_result =
|
||||
crate::query_liquidity_events_upsert(self.database.as_ref(), &dto).await;
|
||||
match upsert_result {
|
||||
Ok(_) => return Ok(true),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_decoded_event_context(
|
||||
&self,
|
||||
decoded_event: &crate::DexDecodedEventDto,
|
||||
) -> Result<NonTradeDecodedEventContext, crate::Error> {
|
||||
let dex_result = crate::query_dexs_get_by_code(
|
||||
self.database.as_ref(),
|
||||
decoded_event.protocol_name.as_str(),
|
||||
)
|
||||
.await;
|
||||
let dex_id = match dex_result {
|
||||
Ok(Some(dex)) => dex.id,
|
||||
Ok(None) => None,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let pool_address = match decoded_event.pool_account.clone() {
|
||||
Some(pool_address) => pool_address,
|
||||
None => {
|
||||
return Ok(NonTradeDecodedEventContext {
|
||||
dex_id,
|
||||
pool_id: None,
|
||||
pair_id: None,
|
||||
pair: None,
|
||||
});
|
||||
},
|
||||
};
|
||||
let pool_result =
|
||||
crate::query_pools_get_by_address(self.database.as_ref(), pool_address.as_str()).await;
|
||||
let pool = match pool_result {
|
||||
Ok(Some(pool)) => pool,
|
||||
Ok(None) => {
|
||||
return Ok(NonTradeDecodedEventContext {
|
||||
dex_id,
|
||||
pool_id: None,
|
||||
pair_id: None,
|
||||
pair: None,
|
||||
});
|
||||
},
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let pool_id = match pool.id {
|
||||
Some(pool_id) => pool_id,
|
||||
None => {
|
||||
return Ok(NonTradeDecodedEventContext {
|
||||
dex_id,
|
||||
pool_id: None,
|
||||
pair_id: None,
|
||||
pair: None,
|
||||
});
|
||||
},
|
||||
};
|
||||
let pair_result = crate::query_pairs_get_by_pool_id(self.database.as_ref(), pool_id).await;
|
||||
let pair = match pair_result {
|
||||
Ok(pair) => pair,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let pair_id = match pair.as_ref() {
|
||||
Some(pair) => pair.id,
|
||||
None => None,
|
||||
};
|
||||
return Ok(NonTradeDecodedEventContext {
|
||||
dex_id,
|
||||
pool_id: Some(pool_id),
|
||||
pair_id,
|
||||
pair,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_first_amount_string(
|
||||
value: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let text = extract_first_string(value, candidate_keys);
|
||||
if text.is_some() {
|
||||
return text;
|
||||
}
|
||||
return extract_first_number_as_string(value, candidate_keys);
|
||||
}
|
||||
|
||||
fn extract_first_string(
|
||||
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 value_option = object.get(*candidate_key);
|
||||
let candidate = match value_option {
|
||||
Some(candidate) => candidate,
|
||||
None => continue,
|
||||
};
|
||||
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_first_string(nested_value, candidate_keys);
|
||||
if nested.is_some() {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested = extract_first_string(nested_value, candidate_keys);
|
||||
if nested.is_some() {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn extract_first_number_as_string(
|
||||
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 value_option = object.get(*candidate_key);
|
||||
let candidate = match value_option {
|
||||
Some(candidate) => candidate,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(number) = candidate.as_i64() {
|
||||
return Some(number.to_string());
|
||||
}
|
||||
if let Some(number) = candidate.as_u64() {
|
||||
return Some(number.to_string());
|
||||
}
|
||||
if let Some(number) = candidate.as_f64() {
|
||||
return Some(number.to_string());
|
||||
}
|
||||
}
|
||||
for nested_value in object.values() {
|
||||
let nested = extract_first_number_as_string(nested_value, candidate_keys);
|
||||
if nested.is_some() {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
for nested_value in array {
|
||||
let nested = extract_first_number_as_string(nested_value, candidate_keys);
|
||||
if nested.is_some() {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn extracts_nested_liquidity_amounts() {
|
||||
let payload = serde_json::json!({
|
||||
"event": {
|
||||
"baseAmountRaw": "100",
|
||||
"quoteAmountRaw": 25,
|
||||
"owner": "Owner111111111111111111111111111111111111"
|
||||
}
|
||||
});
|
||||
assert_eq!(
|
||||
super::extract_first_amount_string(&payload, &["baseAmountRaw"]),
|
||||
Some("100".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
super::extract_first_amount_string(&payload, &["quoteAmountRaw"]),
|
||||
Some("25".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
super::extract_first_string(&payload, &["owner"]),
|
||||
Some("Owner111111111111111111111111111111111111".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user