0.7.30
This commit is contained in:
@@ -42,6 +42,95 @@ impl DexEventCategory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fine-grained lifecycle kind assigned to one decoded DEX event kind.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DexEventLifecycleKind {
|
||||
/// Swap-like trade event.
|
||||
TradeSwap,
|
||||
/// Pool creation or initialization event.
|
||||
PoolCreation,
|
||||
/// Pair creation event when it can be distinguished from pool creation.
|
||||
PairCreation,
|
||||
/// Liquidity deposit or add-liquidity event.
|
||||
LiquidityAdd,
|
||||
/// Liquidity withdraw or remove-liquidity event.
|
||||
LiquidityRemove,
|
||||
/// Concentrated-liquidity position open event.
|
||||
PositionOpen,
|
||||
/// Concentrated-liquidity position close event.
|
||||
PositionClose,
|
||||
/// Migration event, for example launch surface to AMM/CLMM/DLMM.
|
||||
Migration,
|
||||
/// Launch or bonding-curve initialization event.
|
||||
Launch,
|
||||
/// Token mint event detected through a DEX or launch surface decoder.
|
||||
Mint,
|
||||
/// Token burn event detected through a DEX or launch surface decoder.
|
||||
Burn,
|
||||
/// Fee collection event.
|
||||
FeeCollection,
|
||||
/// Reward or emission event.
|
||||
Reward,
|
||||
/// Administration, configuration or permission update event.
|
||||
AdminConfig,
|
||||
/// Event kind that is not classified yet.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl DexEventLifecycleKind {
|
||||
/// Returns the stable string code persisted inside decoded payload metadata.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::TradeSwap => return "trade_swap",
|
||||
Self::PoolCreation => return "pool_creation",
|
||||
Self::PairCreation => return "pair_creation",
|
||||
Self::LiquidityAdd => return "liquidity_add",
|
||||
Self::LiquidityRemove => return "liquidity_remove",
|
||||
Self::PositionOpen => return "position_open",
|
||||
Self::PositionClose => return "position_close",
|
||||
Self::Migration => return "migration",
|
||||
Self::Launch => return "launch",
|
||||
Self::Mint => return "mint",
|
||||
Self::Burn => return "burn",
|
||||
Self::FeeCollection => return "fee_collection",
|
||||
Self::Reward => return "reward",
|
||||
Self::AdminConfig => return "admin_config",
|
||||
Self::Unknown => return "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable actionability class assigned to one decoded DEX event.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DexEventActionability {
|
||||
/// Direct swap-like event that can feed trade/candle materialization.
|
||||
TradeCandidate,
|
||||
/// Swap-like event detected but not materializable as trade/candle yet.
|
||||
NonActionableTrade,
|
||||
/// Useful non-trade event that should remain visible for future materialization.
|
||||
NonTradeUseful,
|
||||
/// Failed transaction event retained for diagnostics but never actionable.
|
||||
FailedTransaction,
|
||||
/// Classified event that is informational only for the current pipeline.
|
||||
Informational,
|
||||
/// Event that is not classified yet.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl DexEventActionability {
|
||||
/// Returns the stable string code persisted inside decoded payload metadata.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::TradeCandidate => return "trade_candidate",
|
||||
Self::NonActionableTrade => return "non_actionable_trade",
|
||||
Self::NonTradeUseful => return "non_trade_useful",
|
||||
Self::FailedTransaction => return "failed_transaction",
|
||||
Self::Informational => return "informational",
|
||||
Self::Unknown => return "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies a DEX event kind into a stable business category.
|
||||
pub fn classify_dex_event_category(event_kind: &str) -> DexEventCategory {
|
||||
if is_dex_reward_event_kind(event_kind) {
|
||||
@@ -70,6 +159,95 @@ pub fn classify_dex_event_category_code(event_kind: &str) -> &'static str {
|
||||
return classify_dex_event_category(event_kind).as_str();
|
||||
}
|
||||
|
||||
/// Classifies a DEX event kind into a fine-grained lifecycle kind.
|
||||
pub fn classify_dex_event_lifecycle_kind(event_kind: &str) -> DexEventLifecycleKind {
|
||||
if is_dex_token_burn_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::Burn;
|
||||
}
|
||||
if is_dex_token_mint_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::Mint;
|
||||
}
|
||||
if is_dex_migration_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::Migration;
|
||||
}
|
||||
if is_dex_launch_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::Launch;
|
||||
}
|
||||
if is_dex_pair_creation_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::PairCreation;
|
||||
}
|
||||
if is_dex_pool_creation_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::PoolCreation;
|
||||
}
|
||||
if is_dex_liquidity_add_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::LiquidityAdd;
|
||||
}
|
||||
if is_dex_liquidity_remove_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::LiquidityRemove;
|
||||
}
|
||||
if is_dex_position_open_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::PositionOpen;
|
||||
}
|
||||
if is_dex_position_close_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::PositionClose;
|
||||
}
|
||||
if is_dex_fee_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::FeeCollection;
|
||||
}
|
||||
if is_dex_reward_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::Reward;
|
||||
}
|
||||
if is_dex_admin_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::AdminConfig;
|
||||
}
|
||||
if is_dex_trade_event_kind(event_kind) {
|
||||
return DexEventLifecycleKind::TradeSwap;
|
||||
}
|
||||
return DexEventLifecycleKind::Unknown;
|
||||
}
|
||||
|
||||
/// Classifies a DEX event kind and returns the persisted lifecycle kind code.
|
||||
pub fn classify_dex_event_lifecycle_kind_code(event_kind: &str) -> &'static str {
|
||||
return classify_dex_event_lifecycle_kind(event_kind).as_str();
|
||||
}
|
||||
|
||||
/// Classifies one decoded DEX event actionability from its kind and candidate flags.
|
||||
pub fn classify_dex_event_actionability(
|
||||
event_kind: &str,
|
||||
trade_candidate: bool,
|
||||
transaction_failed: bool,
|
||||
) -> DexEventActionability {
|
||||
if transaction_failed {
|
||||
return DexEventActionability::FailedTransaction;
|
||||
}
|
||||
if trade_candidate {
|
||||
return DexEventActionability::TradeCandidate;
|
||||
}
|
||||
if is_dex_trade_event_kind(event_kind) {
|
||||
return DexEventActionability::NonActionableTrade;
|
||||
}
|
||||
let category = classify_dex_event_category(event_kind);
|
||||
match category {
|
||||
DexEventCategory::Liquidity => return DexEventActionability::NonTradeUseful,
|
||||
DexEventCategory::Fee => return DexEventActionability::NonTradeUseful,
|
||||
DexEventCategory::Reward => return DexEventActionability::NonTradeUseful,
|
||||
DexEventCategory::PoolLifecycle => return DexEventActionability::NonTradeUseful,
|
||||
DexEventCategory::Admin => return DexEventActionability::NonTradeUseful,
|
||||
DexEventCategory::Trade => return DexEventActionability::NonActionableTrade,
|
||||
DexEventCategory::Unknown => return DexEventActionability::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifies one decoded DEX event actionability and returns its persisted code.
|
||||
pub fn classify_dex_event_actionability_code(
|
||||
event_kind: &str,
|
||||
trade_candidate: bool,
|
||||
transaction_failed: bool,
|
||||
) -> &'static str {
|
||||
return classify_dex_event_actionability(event_kind, trade_candidate, transaction_failed)
|
||||
.as_str();
|
||||
}
|
||||
|
||||
/// Returns true when the event kind represents a swap-like event.
|
||||
pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.ends_with(".buy") {
|
||||
@@ -127,6 +305,50 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for liquidity add-like DEX events.
|
||||
pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".deposit") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".add_liquidity") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".increase_liquidity") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for liquidity remove-like DEX events.
|
||||
pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".withdraw") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".remove_liquidity") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".decrease_liquidity") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for concentrated-liquidity position open events.
|
||||
pub fn is_dex_position_open_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".open_position") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for concentrated-liquidity position close events.
|
||||
pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".close_position") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for fee collection events.
|
||||
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains("collect_creator_fee") {
|
||||
@@ -155,8 +377,81 @@ pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for pool creation, initialization or migration events.
|
||||
/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
|
||||
pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
|
||||
if is_dex_pool_creation_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
if is_dex_pair_creation_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
if is_dex_launch_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
if is_dex_token_mint_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
if is_dex_token_burn_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
if is_dex_migration_event_kind(event_kind) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for launch or bonding-curve creation events.
|
||||
pub fn is_dex_launch_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains("pump_fun.create") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".launch") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".create_v2_token") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".create_bonding_curve") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for token mint events detected by DEX or launch-surface decoders.
|
||||
pub fn is_dex_token_mint_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".mint") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".token_mint") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for token burn events detected by DEX or launch-surface decoders.
|
||||
pub fn is_dex_token_burn_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".burn") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".token_burn") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for launch-surface or pool migration events.
|
||||
pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".migrate") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".migration") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for pool creation or initialization events.
|
||||
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".initialize") {
|
||||
return true;
|
||||
}
|
||||
@@ -166,10 +461,18 @@ pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".create_pool") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".create_v2_token") {
|
||||
if event_kind.contains(".create_amm") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".migrate") {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true for pair creation events when they are distinguishable from pool creation.
|
||||
pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
|
||||
if event_kind.contains(".create_pair") {
|
||||
return true;
|
||||
}
|
||||
if event_kind.contains(".pair_create") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -264,8 +567,7 @@ pub fn enrich_dex_decoded_payload(
|
||||
payload_json: serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let event_category = classify_dex_event_category_code(event_kind);
|
||||
let trade_candidate = is_dex_trade_event_kind(event_kind);
|
||||
let candle_candidate = is_dex_candle_candidate_event_kind(event_kind);
|
||||
let event_lifecycle_kind = classify_dex_event_lifecycle_kind_code(event_kind);
|
||||
let mut object = match payload_json {
|
||||
serde_json::Value::Object(object) => object,
|
||||
other => {
|
||||
@@ -274,14 +576,54 @@ pub fn enrich_dex_decoded_payload(
|
||||
object
|
||||
},
|
||||
};
|
||||
let payload_snapshot = serde_json::Value::Object(object.clone());
|
||||
let explicit_trade_candidate = extract_top_level_bool_by_candidate_keys(
|
||||
&payload_snapshot,
|
||||
&["tradeCandidate", "trade_candidate"],
|
||||
);
|
||||
let trade_candidate = match explicit_trade_candidate {
|
||||
Some(trade_candidate) => trade_candidate,
|
||||
None => is_dex_trade_event_kind(event_kind),
|
||||
};
|
||||
let explicit_candle_candidate = extract_top_level_bool_by_candidate_keys(
|
||||
&payload_snapshot,
|
||||
&["candleCandidate", "candle_candidate"],
|
||||
);
|
||||
let candle_candidate = match explicit_candle_candidate {
|
||||
Some(candle_candidate) => candle_candidate,
|
||||
None => {
|
||||
if !trade_candidate {
|
||||
false
|
||||
} else {
|
||||
is_dex_candle_candidate_event_kind(event_kind)
|
||||
}
|
||||
},
|
||||
};
|
||||
let transaction_failed = match extract_top_level_bool_by_candidate_keys(
|
||||
&payload_snapshot,
|
||||
&["transactionFailed", "transaction_failed"],
|
||||
) {
|
||||
Some(transaction_failed) => transaction_failed,
|
||||
None => false,
|
||||
};
|
||||
let event_actionability =
|
||||
classify_dex_event_actionability_code(event_kind, trade_candidate, transaction_failed);
|
||||
let non_trade_useful = event_actionability == DexEventActionability::NonTradeUseful.as_str();
|
||||
json_insert_string_if_missing(&mut object, "protocolName", protocol_name);
|
||||
json_insert_string_if_missing(&mut object, "eventKind", event_kind);
|
||||
json_insert_string_if_missing(&mut object, "eventCategory", event_category);
|
||||
json_insert_string_if_missing(&mut object, "eventLifecycleKind", event_lifecycle_kind);
|
||||
json_insert_string_if_missing(&mut object, "eventActionability", event_actionability);
|
||||
json_insert_bool_if_missing(&mut object, "nonTradeUseful", non_trade_useful);
|
||||
json_insert_bool_if_missing(&mut object, "tradeCandidate", trade_candidate);
|
||||
json_insert_bool_if_missing(&mut object, "candleCandidate", candle_candidate);
|
||||
json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 1);
|
||||
json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 2);
|
||||
if !trade_candidate {
|
||||
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event");
|
||||
if is_dex_trade_event_kind(event_kind) {
|
||||
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_actionable_trade");
|
||||
} else {
|
||||
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event");
|
||||
}
|
||||
} else if !candle_candidate {
|
||||
json_insert_string_if_missing(
|
||||
&mut object,
|
||||
@@ -525,6 +867,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_fine_grained_non_trade_lifecycle_kinds() {
|
||||
assert_eq!(
|
||||
super::classify_dex_event_lifecycle_kind_code("raydium_cpmm.initialize"),
|
||||
"pool_creation"
|
||||
);
|
||||
assert_eq!(super::classify_dex_event_lifecycle_kind_code("pump_fun.create"), "launch");
|
||||
assert_eq!(
|
||||
super::classify_dex_event_lifecycle_kind_code("meteora_dbc.migrate"),
|
||||
"migration"
|
||||
);
|
||||
assert_eq!(
|
||||
super::classify_dex_event_lifecycle_kind_code("raydium_clmm.increase_liquidity_v2"),
|
||||
"liquidity_add"
|
||||
);
|
||||
assert_eq!(
|
||||
super::classify_dex_event_lifecycle_kind_code("raydium_clmm.decrease_liquidity_v2"),
|
||||
"liquidity_remove"
|
||||
);
|
||||
assert_eq!(
|
||||
super::classify_dex_event_actionability_code(
|
||||
"raydium_clmm.increase_liquidity_v2",
|
||||
false,
|
||||
false,
|
||||
),
|
||||
"non_trade_useful"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enriched_payload_keeps_existing_fields() {
|
||||
let payload_json = serde_json::json!({
|
||||
@@ -558,6 +929,45 @@ mod tests {
|
||||
);
|
||||
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(true)));
|
||||
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(true)));
|
||||
assert_eq!(
|
||||
object.get("eventLifecycleKind"),
|
||||
Some(&serde_json::Value::String("trade_swap".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
object.get("eventActionability"),
|
||||
Some(&serde_json::Value::String("trade_candidate".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enriched_non_trade_payload_is_visible_but_not_trade_candidate() {
|
||||
let enriched_payload = super::enrich_dex_decoded_payload(
|
||||
"raydium_clmm",
|
||||
"raydium_clmm.increase_liquidity_v2",
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let object_option = enriched_payload.as_object();
|
||||
let object = match object_option {
|
||||
Some(object) => object,
|
||||
None => {
|
||||
panic!("expected enriched payload object");
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
object.get("eventCategory"),
|
||||
Some(&serde_json::Value::String("liquidity".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
object.get("eventLifecycleKind"),
|
||||
Some(&serde_json::Value::String("liquidity_add".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
object.get("eventActionability"),
|
||||
Some(&serde_json::Value::String("non_trade_useful".to_owned()))
|
||||
);
|
||||
assert_eq!(object.get("nonTradeUseful"), Some(&serde_json::Value::Bool(true)));
|
||||
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
|
||||
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user