1use std::{num::ParseIntError, str::FromStr};
17
18use alloy::primitives::{Address, I256, U160, U256};
19use nautilus_core::{
20 UnixNanos,
21 datetime::{NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND},
22};
23use nautilus_model::{
24 defi::{
25 PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap, SharedChain, SharedDex,
26 data::{
27 DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate, PoolFlash,
28 },
29 validation::validate_address,
30 },
31 identifiers::InstrumentId,
32};
33use sqlx::{FromRow, Row, postgres::PgRow};
34
35const MAX_UNIX_SECONDS_TIMESTAMP: u64 = 9_999_999_999;
36const MAX_UNIX_MILLISECONDS_TIMESTAMP: u64 = MAX_UNIX_SECONDS_TIMESTAMP * 1_000 + 999;
37const MAX_UNIX_MICROSECONDS_TIMESTAMP: u64 = MAX_UNIX_SECONDS_TIMESTAMP * 1_000_000 + 999_999;
38
39#[derive(Debug)]
44pub struct TokenRow {
45 pub address: Address,
46 pub name: String,
47 pub symbol: String,
48 pub decimals: i32,
49}
50
51impl<'r> FromRow<'r, PgRow> for TokenRow {
52 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
53 let address = validate_address(row.try_get::<String, _>("address")?.as_str()).unwrap();
54 let name = row.try_get::<String, _>("name")?;
55 let symbol = row.try_get::<String, _>("symbol")?;
56 let decimals = row.try_get::<i32, _>("decimals")?;
57
58 let token = Self {
59 address,
60 name,
61 symbol,
62 decimals,
63 };
64 Ok(token)
65 }
66}
67
68#[derive(Debug)]
69pub struct PoolRow {
70 pub address: Address,
71 pub pool_identifier: String,
72 pub dex_name: String,
73 pub creation_block: i64,
74 pub creation_block_timestamp: Option<UnixNanos>,
75 pub token0_chain: i32,
76 pub token0_address: Address,
77 pub token1_chain: i32,
78 pub token1_address: Address,
79 pub fee: Option<i32>,
80 pub tick_spacing: Option<i32>,
81 pub initial_tick: Option<i32>,
82 pub initial_sqrt_price_x96: Option<String>,
83 pub hook_address: Option<String>,
84}
85
86impl<'r> FromRow<'r, PgRow> for PoolRow {
87 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
88 let address = validate_address(row.try_get::<String, _>("address")?.as_str()).unwrap();
89 let pool_identifier = row.try_get::<String, _>("pool_identifier")?;
90 let dex_name = row.try_get::<String, _>("dex_name")?;
91 let creation_block = row.try_get::<i64, _>("creation_block")?;
92 let creation_block_timestamp =
93 row.try_get::<Option<String>, _>("creation_block_timestamp")?;
94 let creation_block_timestamp = creation_block_timestamp
95 .as_deref()
96 .map(parse_cached_block_timestamp)
97 .transpose()
98 .map_err(|e| {
99 sqlx::Error::Decode(
100 format!("Invalid creation block timestamp '{creation_block_timestamp:?}': {e}")
101 .into(),
102 )
103 })?;
104 let token0_chain = row.try_get::<i32, _>("token0_chain")?;
105 let token0_address =
106 validate_address(row.try_get::<String, _>("token0_address")?.as_str()).unwrap();
107 let token1_chain = row.try_get::<i32, _>("token1_chain")?;
108 let token1_address =
109 validate_address(row.try_get::<String, _>("token1_address")?.as_str()).unwrap();
110 let fee = row.try_get::<Option<i32>, _>("fee")?;
111 let tick_spacing = row.try_get::<Option<i32>, _>("tick_spacing")?;
112 let initial_tick = row.try_get::<Option<i32>, _>("initial_tick")?;
113 let initial_sqrt_price_x96 = row.try_get::<Option<String>, _>("initial_sqrt_price_x96")?;
114 let hook_address = row.try_get::<Option<String>, _>("hook_address")?;
115
116 Ok(Self {
117 address,
118 pool_identifier,
119 dex_name,
120 creation_block,
121 creation_block_timestamp,
122 token0_chain,
123 token0_address,
124 token1_chain,
125 token1_address,
126 fee,
127 tick_spacing,
128 initial_tick,
129 initial_sqrt_price_x96,
130 hook_address,
131 })
132 }
133}
134
135#[derive(Debug)]
137pub struct BlockTimestampRow {
138 pub number: u64,
140 pub timestamp: UnixNanos,
142}
143
144impl FromRow<'_, PgRow> for BlockTimestampRow {
145 fn from_row(row: &PgRow) -> Result<Self, sqlx::Error> {
146 let number = row.try_get::<i64, _>("number")? as u64;
147 let timestamp = row.try_get::<String, _>("timestamp")?;
148 let timestamp = parse_cached_block_timestamp(×tamp).map_err(|e| {
149 sqlx::Error::Decode(format!("Invalid block timestamp '{timestamp}': {e}").into())
150 })?;
151 Ok(Self { number, timestamp })
152 }
153}
154
155pub(crate) fn parse_cached_block_timestamp(value: &str) -> Result<UnixNanos, ParseIntError> {
156 let timestamp = value.parse::<u64>()?;
157 if timestamp <= MAX_UNIX_SECONDS_TIMESTAMP {
158 return Ok(UnixNanos::from(timestamp * NANOSECONDS_IN_SECOND));
159 }
160
161 if timestamp <= MAX_UNIX_MILLISECONDS_TIMESTAMP {
162 return Ok(UnixNanos::from(timestamp * NANOSECONDS_IN_MILLISECOND));
163 }
164
165 if timestamp <= MAX_UNIX_MICROSECONDS_TIMESTAMP {
166 return Ok(UnixNanos::from(timestamp * NANOSECONDS_IN_MICROSECOND));
167 }
168
169 Ok(UnixNanos::from(timestamp))
170}
171
172pub fn transform_row_to_dex_pool_data(
181 row: &PgRow,
182 chain: SharedChain,
183 dex: SharedDex,
184 instrument_id: InstrumentId,
185) -> Result<DexPoolData, sqlx::Error> {
186 let event_type = row.try_get::<String, _>("event_type")?;
187 let pool_identifier_str = row.try_get::<String, _>("pool_identifier")?;
188 let pool_identifier = pool_identifier_str
189 .parse()
190 .map_err(|e| sqlx::Error::Decode(format!("Invalid pool identifier: {e}").into()))?;
191 let block = row.try_get::<i64, _>("block")? as u64;
192 let transaction_hash = row.try_get::<String, _>("transaction_hash")?;
193 let transaction_index = row.try_get::<i32, _>("transaction_index")? as u32;
194 let log_index = row.try_get::<i32, _>("log_index")? as u32;
195 let block_timestamp = row.try_get::<String, _>("block_timestamp")?;
196 let timestamp = parse_cached_block_timestamp(&block_timestamp).map_err(|e| {
197 sqlx::Error::Decode(format!("Invalid block timestamp '{block_timestamp}': {e}").into())
198 })?;
199
200 match event_type.as_str() {
201 "swap" => {
202 let sender_str = row
203 .try_get::<Option<String>, _>("sender")?
204 .ok_or_else(|| sqlx::Error::Decode("Missing sender for swap event".into()))?;
205 let sender = validate_address(&sender_str)
206 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
207
208 let recipient_str = row
209 .try_get::<Option<String>, _>("recipient")?
210 .ok_or_else(|| sqlx::Error::Decode("Missing recipient for swap event".into()))?;
211 let recipient = validate_address(&recipient_str)
212 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
213
214 let sqrt_price_x96_str = row
215 .try_get::<Option<String>, _>("sqrt_price_x96")?
216 .ok_or_else(|| {
217 sqlx::Error::Decode("Missing sqrt_price_x96 for swap event".into())
218 })?;
219 let sqrt_price_x96 = U160::from_str(&sqrt_price_x96_str).map_err(|e| {
220 sqlx::Error::Decode(
221 format!("Invalid sqrt_price_x96 '{sqrt_price_x96_str}': {e}").into(),
222 )
223 })?;
224
225 let swap_liquidity_str = row.try_get::<String, _>("swap_liquidity")?;
226 let swap_liquidity = u128::from_str(&swap_liquidity_str)
227 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
228
229 let swap_tick = row.try_get::<i32, _>("swap_tick")?;
230
231 let swap_amount0_str = row
232 .try_get::<Option<String>, _>("swap_amount0")?
233 .ok_or_else(|| sqlx::Error::Decode("Missing swap_amount0 for swap event".into()))?;
234 let amount0 = I256::from_str(&swap_amount0_str).map_err(|e| {
235 sqlx::Error::Decode(
236 format!("Invalid swap_amount0 '{swap_amount0_str}': {e}").into(),
237 )
238 })?;
239
240 let swap_amount1_str = row
241 .try_get::<Option<String>, _>("swap_amount1")?
242 .ok_or_else(|| sqlx::Error::Decode("Missing swap_amount1 for swap event".into()))?;
243 let amount1 = I256::from_str(&swap_amount1_str).map_err(|e| {
244 sqlx::Error::Decode(
245 format!("Invalid swap_amount1 '{swap_amount1_str}': {e}").into(),
246 )
247 })?;
248
249 let pool_swap = PoolSwap::new(
250 chain,
251 dex,
252 instrument_id,
253 pool_identifier,
254 block,
255 transaction_hash,
256 transaction_index,
257 log_index,
258 timestamp, timestamp, sender,
261 recipient,
262 amount0,
263 amount1,
264 sqrt_price_x96,
265 swap_liquidity,
266 swap_tick,
267 );
268
269 Ok(DexPoolData::Swap(pool_swap))
270 }
271 "liquidity" => {
272 let kind_str = row
273 .try_get::<Option<String>, _>("liquidity_event_type")?
274 .ok_or_else(|| {
275 sqlx::Error::Decode("Missing liquidity_event_type for liquidity event".into())
276 })?;
277
278 let kind = match kind_str.as_str() {
279 "Mint" => PoolLiquidityUpdateType::Mint,
280 "Burn" => PoolLiquidityUpdateType::Burn,
281 _ => {
282 return Err(sqlx::Error::Decode(
283 format!("Unknown liquidity update type: {kind_str}").into(),
284 ));
285 }
286 };
287
288 let sender = row
289 .try_get::<Option<String>, _>("sender")?
290 .map(|s| validate_address(&s))
291 .transpose()
292 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
293
294 let owner_str = row
295 .try_get::<Option<String>, _>("owner")?
296 .ok_or_else(|| sqlx::Error::Decode("Missing owner for liquidity event".into()))?;
297 let owner = validate_address(&owner_str)
298 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
299
300 let position_liquidity_str = row.try_get::<String, _>("position_liquidity")?;
302 let position_liquidity = position_liquidity_str.parse::<u128>().map_err(|e| {
303 sqlx::Error::Decode(
304 format!("Invalid position_liquidity '{position_liquidity_str}': {e}").into(),
305 )
306 })?;
307
308 let amount0_str = row.try_get::<String, _>("amount0")?;
309 let amount0 = U256::from_str_radix(&amount0_str, 10).map_err(|e| {
310 sqlx::Error::Decode(format!("Invalid amount0 '{amount0_str}': {e}").into())
311 })?;
312
313 let amount1_str = row.try_get::<String, _>("amount1")?;
314 let amount1 = U256::from_str_radix(&amount1_str, 10).map_err(|e| {
315 sqlx::Error::Decode(format!("Invalid amount1 '{amount1_str}': {e}").into())
316 })?;
317
318 let tick_lower = row
319 .try_get::<Option<i32>, _>("tick_lower")?
320 .ok_or_else(|| {
321 sqlx::Error::Decode("Missing tick_lower for liquidity event".into())
322 })?;
323
324 let tick_upper = row
325 .try_get::<Option<i32>, _>("tick_upper")?
326 .ok_or_else(|| {
327 sqlx::Error::Decode("Missing tick_upper for liquidity event".into())
328 })?;
329
330 let pool_liquidity_update = PoolLiquidityUpdate::new(
331 chain,
332 dex,
333 instrument_id,
334 pool_identifier,
335 kind,
336 block,
337 transaction_hash,
338 transaction_index,
339 log_index,
340 sender,
341 owner,
342 position_liquidity,
343 amount0,
344 amount1,
345 tick_lower,
346 tick_upper,
347 timestamp, timestamp, );
350
351 Ok(DexPoolData::LiquidityUpdate(pool_liquidity_update))
352 }
353 "collect" => {
354 let owner_str = row
355 .try_get::<Option<String>, _>("owner")?
356 .ok_or_else(|| sqlx::Error::Decode("Missing owner for collect event".into()))?;
357 let owner = validate_address(&owner_str)
358 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
359
360 let amount0_str = row.try_get::<String, _>("amount0")?;
362 let amount0 = amount0_str.parse::<u128>().map_err(|e| {
363 sqlx::Error::Decode(format!("Invalid amount0 '{amount0_str}': {e}").into())
364 })?;
365
366 let amount1_str = row.try_get::<String, _>("amount1")?;
367 let amount1 = amount1_str.parse::<u128>().map_err(|e| {
368 sqlx::Error::Decode(format!("Invalid amount1 '{amount1_str}': {e}").into())
369 })?;
370
371 let tick_lower = row
372 .try_get::<Option<i32>, _>("tick_lower")?
373 .ok_or_else(|| {
374 sqlx::Error::Decode("Missing tick_lower for collect event".into())
375 })?;
376
377 let tick_upper = row
378 .try_get::<Option<i32>, _>("tick_upper")?
379 .ok_or_else(|| {
380 sqlx::Error::Decode("Missing tick_upper for collect event".into())
381 })?;
382
383 let pool_fee_collect = PoolFeeCollect::new(
384 chain,
385 dex,
386 instrument_id,
387 pool_identifier,
388 block,
389 transaction_hash,
390 transaction_index,
391 log_index,
392 owner,
393 amount0,
394 amount1,
395 tick_lower,
396 tick_upper,
397 timestamp, timestamp, );
400
401 Ok(DexPoolData::FeeCollect(pool_fee_collect))
402 }
403 "fee_protocol_update" => {
404 let fee_protocol0_new = row.try_get::<i16, _>("fee_protocol0_new")?;
405 let fee_protocol1_new = row.try_get::<i16, _>("fee_protocol1_new")?;
406 let fee_protocol0_new = u8::try_from(fee_protocol0_new).map_err(|e| {
407 sqlx::Error::Decode(
408 format!("Invalid fee_protocol0_new '{fee_protocol0_new}': {e}").into(),
409 )
410 })?;
411 let fee_protocol1_new = u8::try_from(fee_protocol1_new).map_err(|e| {
412 sqlx::Error::Decode(
413 format!("Invalid fee_protocol1_new '{fee_protocol1_new}': {e}").into(),
414 )
415 })?;
416
417 let pool_fee_protocol_update = PoolFeeProtocolUpdate::new(
418 chain,
419 dex,
420 instrument_id,
421 pool_identifier,
422 block,
423 transaction_hash,
424 transaction_index,
425 log_index,
426 fee_protocol0_new,
427 fee_protocol1_new,
428 timestamp, timestamp, );
431
432 Ok(DexPoolData::FeeProtocolUpdate(pool_fee_protocol_update))
433 }
434 "fee_protocol_collect" => {
435 let sender_str = row.try_get::<Option<String>, _>("sender")?.ok_or_else(|| {
436 sqlx::Error::Decode("Missing sender for fee_protocol_collect event".into())
437 })?;
438 let sender = validate_address(&sender_str)
439 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
440
441 let recipient_str =
442 row.try_get::<Option<String>, _>("recipient")?
443 .ok_or_else(|| {
444 sqlx::Error::Decode(
445 "Missing recipient for fee_protocol_collect event".into(),
446 )
447 })?;
448 let recipient = validate_address(&recipient_str)
449 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
450
451 let amount0_str = row.try_get::<String, _>("amount0")?;
453 let amount0 = amount0_str.parse::<u128>().map_err(|e| {
454 sqlx::Error::Decode(format!("Invalid amount0 '{amount0_str}': {e}").into())
455 })?;
456
457 let amount1_str = row.try_get::<String, _>("amount1")?;
458 let amount1 = amount1_str.parse::<u128>().map_err(|e| {
459 sqlx::Error::Decode(format!("Invalid amount1 '{amount1_str}': {e}").into())
460 })?;
461
462 let pool_fee_protocol_collect = PoolFeeProtocolCollect::new(
463 chain,
464 dex,
465 instrument_id,
466 pool_identifier,
467 block,
468 transaction_hash,
469 transaction_index,
470 log_index,
471 sender,
472 recipient,
473 amount0,
474 amount1,
475 timestamp, timestamp, );
478
479 Ok(DexPoolData::FeeProtocolCollect(pool_fee_protocol_collect))
480 }
481 "flash" => {
482 let sender_str = row
483 .try_get::<Option<String>, _>("sender")?
484 .ok_or_else(|| sqlx::Error::Decode("Missing sender for flash event".into()))?;
485 let sender = validate_address(&sender_str)
486 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
487
488 let recipient_str = row
489 .try_get::<Option<String>, _>("recipient")?
490 .ok_or_else(|| sqlx::Error::Decode("Missing recipient for flash event".into()))?;
491 let recipient = validate_address(&recipient_str)
492 .map_err(|e| sqlx::Error::Decode(e.to_string().into()))?;
493
494 let flash_amount0_str = row.try_get::<String, _>("flash_amount0")?;
496 let amount0 = U256::from_str_radix(&flash_amount0_str, 10).map_err(|e| {
497 sqlx::Error::Decode(
498 format!("Invalid flash_amount0 '{flash_amount0_str}': {e}").into(),
499 )
500 })?;
501
502 let flash_amount1_str = row.try_get::<String, _>("flash_amount1")?;
503 let amount1 = U256::from_str_radix(&flash_amount1_str, 10).map_err(|e| {
504 sqlx::Error::Decode(
505 format!("Invalid flash_amount1 '{flash_amount1_str}': {e}").into(),
506 )
507 })?;
508
509 let flash_paid0_str = row.try_get::<String, _>("flash_paid0")?;
510 let paid0 = U256::from_str_radix(&flash_paid0_str, 10).map_err(|e| {
511 sqlx::Error::Decode(format!("Invalid flash_paid0 '{flash_paid0_str}': {e}").into())
512 })?;
513
514 let flash_paid1_str = row.try_get::<String, _>("flash_paid1")?;
515 let paid1 = U256::from_str_radix(&flash_paid1_str, 10).map_err(|e| {
516 sqlx::Error::Decode(format!("Invalid flash_paid1 '{flash_paid1_str}': {e}").into())
517 })?;
518
519 let pool_flash = PoolFlash::new(
520 chain,
521 dex,
522 instrument_id,
523 pool_identifier,
524 block,
525 transaction_hash,
526 transaction_index,
527 log_index,
528 timestamp, timestamp, sender,
531 recipient,
532 amount0,
533 amount1,
534 paid0,
535 paid1,
536 );
537
538 Ok(DexPoolData::Flash(pool_flash))
539 }
540 _ => Err(sqlx::Error::Decode(
541 format!("Unknown event type: {event_type}").into(),
542 )),
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use nautilus_core::datetime::{
549 NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND,
550 };
551 use rstest::rstest;
552
553 use super::*;
554
555 #[rstest]
556 #[case("1700000000", 1_700_000_000 * NANOSECONDS_IN_SECOND)]
557 #[case("9999999999", 9_999_999_999 * NANOSECONDS_IN_SECOND)]
558 #[case("1700000000123", 1_700_000_000_123 * NANOSECONDS_IN_MILLISECOND)]
559 #[case("9999999999999", 9_999_999_999_999 * NANOSECONDS_IN_MILLISECOND)]
560 #[case("1700000000123456", 1_700_000_000_123_456 * NANOSECONDS_IN_MICROSECOND)]
561 #[case("9999999999999999", 9_999_999_999_999_999 * NANOSECONDS_IN_MICROSECOND)]
562 #[case("1700000000123456789", 1_700_000_000_123_456_789)]
563 fn parse_cached_block_timestamp_returns_unix_nanos(#[case] value: &str, #[case] expected: u64) {
564 let timestamp = parse_cached_block_timestamp(value).unwrap();
565
566 assert_eq!(timestamp, UnixNanos::from(expected));
567 }
568
569 #[rstest]
570 fn parse_cached_block_timestamp_rejects_invalid_text() {
571 let result = parse_cached_block_timestamp("not-a-timestamp");
572
573 assert!(result.is_err());
574 }
575}