1use alloy::primitives::{B256, U256};
25use alloy::rpc::types::Log;
26use alloy::sol_types::SolEvent;
27use serde::{Deserialize, Serialize};
28
29use crate::contracts::{IBeacon, PerpManager};
30use crate::convert::{price_x96_to_f64, scale_from_6dec, sqrt_price_x96_to_price};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34#[allow(missing_docs)]
35pub enum MarketEvent {
36 PositionOpened {
38 perp_id: B256,
39 mark_price: f64,
40 long_oi: f64,
41 short_oi: f64,
42 pos_id: U256,
43 is_maker: bool,
44 perp_delta: f64,
45 usd_delta: f64,
46 tick_lower: i32,
47 tick_upper: i32,
48 },
49 NotionalAdjusted {
51 perp_id: B256,
52 mark_price: f64,
53 long_oi: f64,
54 short_oi: f64,
55 pos_id: U256,
56 new_perp_delta: f64,
57 swap_perp_delta: f64,
58 swap_usd_delta: f64,
59 funding: f64,
60 trading_fees: f64,
61 },
62 PositionClosed {
64 perp_id: B256,
65 mark_price: f64,
66 long_oi: f64,
67 short_oi: f64,
68 pos_id: U256,
69 was_maker: bool,
70 was_liquidated: bool,
71 was_partial_close: bool,
72 exit_perp_delta: f64,
73 exit_usd_delta: f64,
74 net_margin: f64,
75 funding: f64,
76 },
77 IndexUpdated { index: f64 },
79}
80
81pub fn decode_log(log: &Log) -> Option<MarketEvent> {
91 let topic0 = *log.topic0()?;
92
93 if topic0 == PerpManager::PositionOpened::SIGNATURE_HASH {
94 decode_position_opened(log)
95 } else if topic0 == PerpManager::NotionalAdjusted::SIGNATURE_HASH {
96 decode_notional_adjusted(log)
97 } else if topic0 == PerpManager::PositionClosed::SIGNATURE_HASH {
98 decode_position_closed(log)
99 } else if topic0 == IBeacon::IndexUpdated::SIGNATURE_HASH {
100 decode_index_updated(log)
101 } else {
102 None
103 }
104}
105
106fn decode_position_opened(log: &Log) -> Option<MarketEvent> {
107 let decoded = PerpManager::PositionOpened::decode_raw_log(
108 log.inner.data.topics().iter().copied(),
109 log.inner.data.data.as_ref(),
110 )
111 .ok()?;
112
113 Some(MarketEvent::PositionOpened {
114 perp_id: decoded.perpId,
115 mark_price: sqrt_price_x96_to_price(decoded.sqrtPriceX96).ok()?,
116 long_oi: scale_from_6dec(decoded.longOI.try_into().ok()?),
117 short_oi: scale_from_6dec(decoded.shortOI.try_into().ok()?),
118 pos_id: decoded.posId,
119 is_maker: decoded.isMaker,
120 perp_delta: scale_from_6dec(decoded.perpDelta.try_into().ok()?),
121 usd_delta: scale_from_6dec(decoded.usdDelta.try_into().ok()?),
122 tick_lower: decoded.tickLower.as_i32(),
123 tick_upper: decoded.tickUpper.as_i32(),
124 })
125}
126
127fn decode_notional_adjusted(log: &Log) -> Option<MarketEvent> {
128 let decoded = PerpManager::NotionalAdjusted::decode_raw_log(
129 log.inner.data.topics().iter().copied(),
130 log.inner.data.data.as_ref(),
131 )
132 .ok()?;
133
134 Some(MarketEvent::NotionalAdjusted {
135 perp_id: decoded.perpId,
136 mark_price: sqrt_price_x96_to_price(decoded.sqrtPriceX96).ok()?,
137 long_oi: scale_from_6dec(decoded.longOI.try_into().ok()?),
138 short_oi: scale_from_6dec(decoded.shortOI.try_into().ok()?),
139 pos_id: decoded.posId,
140 new_perp_delta: scale_from_6dec(decoded.newPerpDelta.try_into().ok()?),
141 swap_perp_delta: scale_from_6dec(decoded.swapPerpDelta.try_into().ok()?),
142 swap_usd_delta: scale_from_6dec(decoded.swapUsdDelta.try_into().ok()?),
143 funding: scale_from_6dec(decoded.funding.try_into().ok()?),
144 trading_fees: scale_from_6dec(decoded.tradingFees.try_into().ok()?),
145 })
146}
147
148fn decode_position_closed(log: &Log) -> Option<MarketEvent> {
149 let decoded = PerpManager::PositionClosed::decode_raw_log(
150 log.inner.data.topics().iter().copied(),
151 log.inner.data.data.as_ref(),
152 )
153 .ok()?;
154
155 Some(MarketEvent::PositionClosed {
156 perp_id: decoded.perpId,
157 mark_price: sqrt_price_x96_to_price(decoded.sqrtPriceX96).ok()?,
158 long_oi: scale_from_6dec(decoded.longOI.try_into().ok()?),
159 short_oi: scale_from_6dec(decoded.shortOI.try_into().ok()?),
160 pos_id: decoded.posId,
161 was_maker: decoded.wasMaker,
162 was_liquidated: decoded.wasLiquidated,
163 was_partial_close: decoded.wasPartialClose,
164 exit_perp_delta: scale_from_6dec(decoded.exitPerpDelta.try_into().ok()?),
165 exit_usd_delta: scale_from_6dec(decoded.exitUsdDelta.try_into().ok()?),
166 net_margin: scale_from_6dec(decoded.netMargin.try_into().ok()?),
167 funding: scale_from_6dec(decoded.funding.try_into().ok()?),
168 })
169}
170
171fn decode_index_updated(log: &Log) -> Option<MarketEvent> {
172 let decoded = IBeacon::IndexUpdated::decode_raw_log(
173 log.inner.data.topics().iter().copied(),
174 log.inner.data.data.as_ref(),
175 )
176 .ok()?;
177
178 Some(MarketEvent::IndexUpdated {
179 index: price_x96_to_f64(decoded.index).ok()?,
180 })
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use alloy::primitives::{Address, LogData, Signed, U256};
187 use alloy::rpc::types::Log as RpcLog;
188
189 use crate::constants::{Q96, Q96_PRECISION};
190
191 fn make_log<E: SolEvent>(event: &E, address: Address) -> RpcLog {
193 let log_data = event.encode_log_data();
194 RpcLog {
195 inner: alloy::primitives::Log {
196 address,
197 data: log_data,
198 },
199 block_hash: None,
200 block_number: None,
201 block_timestamp: None,
202 transaction_hash: None,
203 transaction_index: None,
204 log_index: None,
205 removed: false,
206 }
207 }
208
209 #[test]
210 fn decode_position_opened_event() {
211 let perp_id = B256::repeat_byte(0x01);
212 let event = PerpManager::PositionOpened {
213 perpId: perp_id,
214 sqrtPriceX96: Q96, longOI: U256::from(1_000_000u64),
216 shortOI: U256::from(500_000u64),
217 posId: U256::from(42u64),
218 isMaker: false,
219 perpDelta: alloy::primitives::I256::try_from(100_000_000i64).unwrap(),
220 usdDelta: alloy::primitives::I256::try_from(-100_000_000i64).unwrap(),
221 tickLower: Signed::try_from(-100i32).unwrap(),
222 tickUpper: Signed::try_from(100i32).unwrap(),
223 };
224
225 let log = make_log(&event, Address::ZERO);
226 let decoded = decode_log(&log).expect("should decode PositionOpened");
227
228 match decoded {
229 MarketEvent::PositionOpened {
230 perp_id: pid,
231 mark_price,
232 pos_id,
233 is_maker,
234 ..
235 } => {
236 assert_eq!(pid, perp_id);
237 assert!((mark_price - 1.0).abs() < Q96_PRECISION);
238 assert_eq!(pos_id, U256::from(42u64));
239 assert!(!is_maker);
240 }
241 _ => panic!("expected PositionOpened"),
242 }
243 }
244
245 #[test]
246 fn decode_index_updated_event() {
247 let event = IBeacon::IndexUpdated {
248 index: Q96 * U256::from(100u64), };
250
251 let log = make_log(&event, Address::ZERO);
252 let decoded = decode_log(&log).expect("should decode IndexUpdated");
253
254 match decoded {
255 MarketEvent::IndexUpdated { index } => {
256 assert!((index - 100.0).abs() < Q96_PRECISION);
257 }
258 _ => panic!("expected IndexUpdated"),
259 }
260 }
261
262 #[test]
263 fn decode_position_closed_event() {
264 let perp_id = B256::repeat_byte(0x02);
265 let event = PerpManager::PositionClosed {
266 perpId: perp_id,
267 sqrtPriceX96: Q96 * U256::from(10u64), longOI: U256::from(2_000_000u64),
269 shortOI: U256::from(1_000_000u64),
270 posId: U256::from(7u64),
271 wasMaker: false,
272 wasLiquidated: true,
273 wasPartialClose: false,
274 exitPerpDelta: alloy::primitives::I256::try_from(-50_000_000i64).unwrap(),
275 exitUsdDelta: alloy::primitives::I256::try_from(50_000_000i64).unwrap(),
276 tickLower: Signed::try_from(0i32).unwrap(),
277 tickUpper: Signed::try_from(0i32).unwrap(),
278 netUsdDelta: alloy::primitives::I256::try_from(48_000_000i64).unwrap(),
279 funding: alloy::primitives::I256::try_from(-1_000_000i64).unwrap(),
280 utilizationFee: U256::from(500_000u64),
281 adl: U256::ZERO,
282 liquidationFee: U256::from(1_000_000u64),
283 netMargin: alloy::primitives::I256::try_from(45_000_000i64).unwrap(),
284 };
285
286 let log = make_log(&event, Address::ZERO);
287 let decoded = decode_log(&log).expect("should decode PositionClosed");
288
289 match decoded {
290 MarketEvent::PositionClosed {
291 perp_id: pid,
292 mark_price,
293 pos_id,
294 was_liquidated,
295 net_margin,
296 funding,
297 ..
298 } => {
299 assert_eq!(pid, perp_id);
300 assert!((mark_price - 100.0).abs() < Q96_PRECISION);
301 assert_eq!(pos_id, U256::from(7u64));
302 assert!(was_liquidated);
303 assert!((net_margin - 45.0).abs() < Q96_PRECISION);
304 assert!((funding - (-1.0)).abs() < Q96_PRECISION);
305 }
306 _ => panic!("expected PositionClosed"),
307 }
308 }
309
310 #[test]
311 fn unrecognized_event_returns_none() {
312 let log = RpcLog {
314 inner: alloy::primitives::Log {
315 address: Address::ZERO,
316 data: LogData::new_unchecked(vec![B256::repeat_byte(0xFF)], vec![].into()),
317 },
318 block_hash: None,
319 block_number: None,
320 block_timestamp: None,
321 transaction_hash: None,
322 transaction_index: None,
323 log_index: None,
324 removed: false,
325 };
326 assert!(decode_log(&log).is_none());
327 }
328
329 #[test]
330 fn empty_log_returns_none() {
331 let log = RpcLog {
332 inner: alloy::primitives::Log {
333 address: Address::ZERO,
334 data: LogData::new_unchecked(vec![], vec![].into()),
335 },
336 block_hash: None,
337 block_number: None,
338 block_timestamp: None,
339 transaction_hash: None,
340 transaction_index: None,
341 log_index: None,
342 removed: false,
343 };
344 assert!(decode_log(&log).is_none());
345 }
346}