From 4a657a942929e2dc92a022862c54e2647626a209 Mon Sep 17 00:00:00 2001 From: Andrei De Stefani Date: Sat, 29 Nov 2025 17:24:43 +0100 Subject: [PATCH] feat: add UserOpEvent types for audit pipeline --- crates/audit/src/types.rs | 172 +++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/crates/audit/src/types.rs b/crates/audit/src/types.rs index 6b0d7d1c..f27241a0 100644 --- a/crates/audit/src/types.rs +++ b/crates/audit/src/types.rs @@ -1,5 +1,5 @@ use alloy_consensus::transaction::{SignerRecoverable, Transaction as ConsensusTransaction}; -use alloy_primitives::{Address, TxHash, U256}; +use alloy_primitives::{Address, B256, TxHash, U256}; use bytes::Bytes; use serde::{Deserialize, Serialize}; use tips_core::AcceptedBundle; @@ -26,6 +26,15 @@ pub struct Transaction { pub data: Bytes, } +pub type UserOpHash = B256; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum UserOpDropReason { + Invalid(String), + Expired, + ReplacedByHigherFee, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "event", content = "data")] pub enum BundleEvent { @@ -104,3 +113,164 @@ impl BundleEvent { } } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum UserOpEvent { + AddedToMempool { + user_op_hash: UserOpHash, + sender: Address, + entry_point: Address, + nonce: U256, + }, + Dropped { + user_op_hash: UserOpHash, + reason: UserOpDropReason, + }, + Included { + user_op_hash: UserOpHash, + block_number: u64, + tx_hash: TxHash, + }, +} + +impl UserOpEvent { + pub fn user_op_hash(&self) -> UserOpHash { + match self { + UserOpEvent::AddedToMempool { user_op_hash, .. } => *user_op_hash, + UserOpEvent::Dropped { user_op_hash, .. } => *user_op_hash, + UserOpEvent::Included { user_op_hash, .. } => *user_op_hash, + } + } + + pub fn generate_event_key(&self) -> String { + match self { + UserOpEvent::Included { + user_op_hash, + tx_hash, + .. + } => { + format!("{user_op_hash}-{tx_hash}") + } + _ => { + format!("{}-{}", self.user_op_hash(), Uuid::new_v4()) + } + } + } +} + +#[cfg(test)] +mod user_op_event_tests { + use super::*; + use alloy_primitives::{address, b256}; + + fn create_test_user_op_hash() -> UserOpHash { + b256!("1111111111111111111111111111111111111111111111111111111111111111") + } + + #[test] + fn test_user_op_event_added_to_mempool_serialization() { + let event = UserOpEvent::AddedToMempool { + user_op_hash: create_test_user_op_hash(), + sender: address!("2222222222222222222222222222222222222222"), + entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), + nonce: U256::from(1), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"AddedToMempool\"")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_event_dropped_serialization() { + let event = UserOpEvent::Dropped { + user_op_hash: create_test_user_op_hash(), + reason: UserOpDropReason::Invalid("gas too low".to_string()), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"Dropped\"")); + assert!(json.contains("gas too low")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_event_included_serialization() { + let event = UserOpEvent::Included { + user_op_hash: create_test_user_op_hash(), + block_number: 12345, + tx_hash: b256!("3333333333333333333333333333333333333333333333333333333333333333"), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"Included\"")); + assert!(json.contains("\"block_number\":12345")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_hash_accessor() { + let hash = create_test_user_op_hash(); + + let added = UserOpEvent::AddedToMempool { + user_op_hash: hash, + sender: address!("2222222222222222222222222222222222222222"), + entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), + nonce: U256::from(1), + }; + assert_eq!(added.user_op_hash(), hash); + + let dropped = UserOpEvent::Dropped { + user_op_hash: hash, + reason: UserOpDropReason::Expired, + }; + assert_eq!(dropped.user_op_hash(), hash); + + let included = UserOpEvent::Included { + user_op_hash: hash, + block_number: 100, + tx_hash: b256!("4444444444444444444444444444444444444444444444444444444444444444"), + }; + assert_eq!(included.user_op_hash(), hash); + } + + #[test] + fn test_generate_event_key_included() { + let user_op_hash = + b256!("1111111111111111111111111111111111111111111111111111111111111111"); + let tx_hash = b256!("2222222222222222222222222222222222222222222222222222222222222222"); + + let event = UserOpEvent::Included { + user_op_hash, + block_number: 100, + tx_hash, + }; + + let key = event.generate_event_key(); + assert!(key.contains(&format!("{user_op_hash}"))); + assert!(key.contains(&format!("{tx_hash}"))); + } + + #[test] + fn test_user_op_drop_reason_variants() { + let invalid = UserOpDropReason::Invalid("test reason".to_string()); + let json = serde_json::to_string(&invalid).unwrap(); + assert!(json.contains("Invalid")); + assert!(json.contains("test reason")); + + let expired = UserOpDropReason::Expired; + let json = serde_json::to_string(&expired).unwrap(); + assert!(json.contains("Expired")); + + let replaced = UserOpDropReason::ReplacedByHigherFee; + let json = serde_json::to_string(&replaced).unwrap(); + assert!(json.contains("ReplacedByHigherFee")); + } +}