Skip to main content

nautilus_model/python/defi/
data.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for DeFi data types.
17
18use std::{
19    collections::hash_map::DefaultHasher,
20    hash::{Hash, Hasher},
21    sync::Arc,
22};
23
24use nautilus_core::python::to_pyvalue_err;
25use pyo3::{basic::CompareOp, prelude::*};
26
27use crate::{
28    defi::{
29        Chain, Dex,
30        chain::Blockchain,
31        data::{
32            Block, PoolFeeCollect, PoolFlash, PoolLiquidityUpdate, PoolLiquidityUpdateType,
33            PoolSwap, Transaction,
34        },
35    },
36    identifiers::InstrumentId,
37};
38
39#[pymethods]
40#[pyo3_stub_gen::derive::gen_stub_pymethods]
41impl Block {
42    /// Represents an Ethereum-compatible blockchain block with essential metadata.
43    #[new]
44    #[expect(clippy::too_many_arguments)]
45    fn py_new(
46        chain: Blockchain,
47        hash: String,
48        parent_hash: String,
49        number: u64,
50        miner: String,
51        gas_limit: u64,
52        gas_used: u64,
53        timestamp: u64,
54    ) -> Self {
55        Self::new(
56            hash,
57            parent_hash,
58            number,
59            miner.into(),
60            gas_limit,
61            gas_used,
62            timestamp.into(),
63            Some(chain),
64        )
65    }
66
67    /// Returns the blockchain for this block.
68    #[getter]
69    #[pyo3(name = "chain")]
70    fn py_chain(&self) -> Option<Blockchain> {
71        self.chain
72    }
73
74    #[getter]
75    #[pyo3(name = "hash")]
76    fn py_hash(&self) -> &str {
77        &self.hash
78    }
79
80    #[getter]
81    #[pyo3(name = "number")]
82    fn py_number(&self) -> u64 {
83        self.number
84    }
85
86    #[getter]
87    #[pyo3(name = "parent_hash")]
88    fn py_parent_hash(&self) -> &str {
89        &self.parent_hash
90    }
91
92    #[getter]
93    #[pyo3(name = "miner")]
94    fn py_miner(&self) -> &str {
95        &self.miner
96    }
97
98    #[getter]
99    #[pyo3(name = "gas_limit")]
100    fn py_gas_limit(&self) -> u64 {
101        self.gas_limit
102    }
103
104    #[getter]
105    #[pyo3(name = "gas_used")]
106    fn py_gas_used(&self) -> u64 {
107        self.gas_used
108    }
109
110    #[getter]
111    #[pyo3(name = "base_fee_per_gas")]
112    fn py_base_fee_per_gas(&self) -> Option<String> {
113        self.base_fee_per_gas.map(|x| x.to_string())
114    }
115
116    #[getter]
117    #[pyo3(name = "blob_gas_used")]
118    fn py_blob_gas_used(&self) -> Option<String> {
119        self.blob_gas_used.map(|x| x.to_string())
120    }
121
122    #[getter]
123    #[pyo3(name = "excess_blob_gas")]
124    fn py_excess_blob_gas(&self) -> Option<String> {
125        self.excess_blob_gas.map(|x| x.to_string())
126    }
127
128    #[getter]
129    #[pyo3(name = "l1_gas_price")]
130    fn py_l1_gas_price(&self) -> Option<String> {
131        self.l1_gas_price.map(|x| x.to_string())
132    }
133
134    #[getter]
135    #[pyo3(name = "l1_gas_used")]
136    fn py_l1_gas_used(&self) -> Option<u64> {
137        self.l1_gas_used
138    }
139
140    #[getter]
141    #[pyo3(name = "l1_fee_scalar")]
142    fn py_l1_fee_scalar(&self) -> Option<u64> {
143        self.l1_fee_scalar
144    }
145
146    #[getter]
147    #[pyo3(name = "timestamp")]
148    fn py_timestamp(&self) -> u64 {
149        self.timestamp.as_u64()
150    }
151
152    fn __str__(&self) -> String {
153        self.to_string()
154    }
155
156    fn __repr__(&self) -> String {
157        format!("{self:?}")
158    }
159
160    fn __hash__(&self) -> u64 {
161        let mut hasher = DefaultHasher::new();
162        self.hash.hash(&mut hasher);
163        hasher.finish()
164    }
165}
166
167#[pymethods]
168#[pyo3_stub_gen::derive::gen_stub_pymethods]
169impl PoolSwap {
170    /// Represents a token swap transaction on a decentralized exchange (DEX).
171    ///
172    /// This structure captures both the raw blockchain data from a swap event and
173    /// optionally includes computed market-oriented trade information. It serves as
174    /// the primary data structure for tracking and analyzing DEX swap activity.
175    #[new]
176    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
177    fn py_new(
178        chain: Chain,
179        dex: Dex,
180        instrument_id: InstrumentId,
181        pool_identifier: String,
182        block: u64,
183        transaction_hash: String,
184        transaction_index: u32,
185        log_index: u32,
186        timestamp: u64,
187        sender: String,
188        receiver: String,
189        amount0: String,
190        amount1: String,
191        sqrt_price_x96: String,
192        liquidity: u128,
193        tick: i32,
194    ) -> PyResult<Self> {
195        let sender = sender.parse().map_err(to_pyvalue_err)?;
196        let receiver = receiver.parse().map_err(to_pyvalue_err)?;
197        let amount0 = amount0.parse().map_err(to_pyvalue_err)?;
198        let amount1 = amount1.parse().map_err(to_pyvalue_err)?;
199        let sqrt_price_x96 = sqrt_price_x96.parse().map_err(to_pyvalue_err)?;
200        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
201        Ok(Self::new(
202            Arc::new(chain),
203            Arc::new(dex),
204            instrument_id,
205            pool_identifier,
206            block,
207            transaction_hash,
208            transaction_index,
209            log_index,
210            Some(timestamp.into()),
211            sender,
212            receiver,
213            amount0,
214            amount1,
215            sqrt_price_x96,
216            liquidity,
217            tick,
218        ))
219    }
220
221    fn __str__(&self) -> String {
222        self.to_string()
223    }
224
225    fn __repr__(&self) -> String {
226        format!("{self:?}")
227    }
228
229    fn __hash__(&self) -> u64 {
230        let mut hasher = DefaultHasher::new();
231        self.chain.chain_id.hash(&mut hasher);
232        self.transaction_hash.hash(&mut hasher);
233        self.log_index.hash(&mut hasher);
234        hasher.finish()
235    }
236
237    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
238        match op {
239            CompareOp::Eq => self == other,
240            CompareOp::Ne => self != other,
241            _ => panic!("Unsupported comparison for PoolSwap"),
242        }
243    }
244
245    #[getter]
246    #[pyo3(name = "chain")]
247    fn py_chain(&self) -> Chain {
248        self.chain.as_ref().clone()
249    }
250
251    #[getter]
252    #[pyo3(name = "dex")]
253    fn py_dex(&self) -> Dex {
254        self.dex.as_ref().clone()
255    }
256
257    #[getter]
258    #[pyo3(name = "instrument_id")]
259    fn py_instrument_id(&self) -> InstrumentId {
260        self.instrument_id
261    }
262
263    #[getter]
264    #[pyo3(name = "pool_identifier")]
265    fn py_pool_identifier(&self) -> String {
266        self.pool_identifier.to_string()
267    }
268
269    #[getter]
270    #[pyo3(name = "block")]
271    fn py_block(&self) -> u64 {
272        self.block
273    }
274
275    #[getter]
276    #[pyo3(name = "transaction_hash")]
277    fn py_transaction_hash(&self) -> &str {
278        &self.transaction_hash
279    }
280
281    #[getter]
282    #[pyo3(name = "transaction_index")]
283    fn py_transaction_index(&self) -> u32 {
284        self.transaction_index
285    }
286
287    #[getter]
288    #[pyo3(name = "log_index")]
289    fn py_log_index(&self) -> u32 {
290        self.log_index
291    }
292
293    #[getter]
294    #[pyo3(name = "sender")]
295    fn py_sender(&self) -> String {
296        self.sender.to_string()
297    }
298
299    #[getter]
300    #[pyo3(name = "timestamp")]
301    fn py_timestamp(&self) -> Option<u64> {
302        self.timestamp.map(|x| x.as_u64())
303    }
304
305    #[getter]
306    #[pyo3(name = "ts_init")]
307    fn py_ts_init(&self) -> Option<u64> {
308        self.ts_init.map(|x| x.as_u64())
309    }
310}
311
312#[pymethods]
313#[pyo3_stub_gen::derive::gen_stub_pymethods]
314impl PoolLiquidityUpdate {
315    /// Represents a liquidity update event in a decentralized exchange (DEX) pool.
316    #[new]
317    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
318    fn py_new(
319        chain: Chain,
320        dex: Dex,
321        pool_identifier: String,
322        instrument_id: InstrumentId,
323        kind: PoolLiquidityUpdateType,
324        block: u64,
325        transaction_hash: String,
326        transaction_index: u32,
327        log_index: u32,
328        sender: Option<String>,
329        owner: String,
330        position_liquidity: String,
331        amount0: String,
332        amount1: String,
333        tick_lower: i32,
334        tick_upper: i32,
335        timestamp: u64,
336    ) -> PyResult<Self> {
337        let sender = sender
338            .map(|s| s.parse())
339            .transpose()
340            .map_err(to_pyvalue_err)?;
341        let owner = owner.parse().map_err(to_pyvalue_err)?;
342        let position_liquidity = position_liquidity.parse().map_err(to_pyvalue_err)?;
343        let amount0 = amount0.parse().map_err(to_pyvalue_err)?;
344        let amount1 = amount1.parse().map_err(to_pyvalue_err)?;
345        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
346        Ok(Self::new(
347            Arc::new(chain),
348            Arc::new(dex),
349            instrument_id,
350            pool_identifier,
351            kind,
352            block,
353            transaction_hash,
354            transaction_index,
355            log_index,
356            sender,
357            owner,
358            position_liquidity,
359            amount0,
360            amount1,
361            tick_lower,
362            tick_upper,
363            Some(timestamp.into()),
364        ))
365    }
366
367    fn __str__(&self) -> String {
368        self.to_string()
369    }
370
371    fn __repr__(&self) -> String {
372        format!("{self:?}")
373    }
374
375    fn __hash__(&self) -> u64 {
376        let mut hasher = DefaultHasher::new();
377        self.chain.chain_id.hash(&mut hasher);
378        self.transaction_hash.hash(&mut hasher);
379        self.log_index.hash(&mut hasher);
380        hasher.finish()
381    }
382
383    fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp) -> bool {
384        match op {
385            CompareOp::Eq => self == other,
386            CompareOp::Ne => self != other,
387            _ => panic!("Unsupported comparison for PoolLiquidityUpdate"),
388        }
389    }
390
391    #[getter]
392    #[pyo3(name = "chain")]
393    fn py_chain(&self) -> Chain {
394        self.chain.as_ref().clone()
395    }
396
397    #[getter]
398    #[pyo3(name = "dex")]
399    fn py_dex(&self) -> Dex {
400        self.dex.as_ref().clone()
401    }
402
403    #[getter]
404    #[pyo3(name = "instrument_id")]
405    fn py_instrument_id(&self) -> InstrumentId {
406        self.instrument_id
407    }
408
409    #[getter]
410    #[pyo3(name = "pool_identifier")]
411    fn py_pool_identifier(&self) -> String {
412        self.pool_identifier.to_string()
413    }
414
415    #[getter]
416    #[pyo3(name = "kind")]
417    fn py_kind(&self) -> PoolLiquidityUpdateType {
418        self.kind
419    }
420
421    #[getter]
422    #[pyo3(name = "block")]
423    fn py_block(&self) -> u64 {
424        self.block
425    }
426
427    #[getter]
428    #[pyo3(name = "transaction_hash")]
429    fn py_transaction_hash(&self) -> &str {
430        &self.transaction_hash
431    }
432
433    #[getter]
434    #[pyo3(name = "transaction_index")]
435    fn py_transaction_index(&self) -> u32 {
436        self.transaction_index
437    }
438
439    #[getter]
440    #[pyo3(name = "log_index")]
441    fn py_log_index(&self) -> u32 {
442        self.log_index
443    }
444
445    #[getter]
446    #[pyo3(name = "sender")]
447    fn py_sender(&self) -> Option<String> {
448        self.sender.map(|s| s.to_string())
449    }
450
451    #[getter]
452    #[pyo3(name = "owner")]
453    fn py_owner(&self) -> String {
454        self.owner.to_string()
455    }
456
457    #[getter]
458    #[pyo3(name = "position_liquidity")]
459    fn py_position_liquidity(&self) -> String {
460        self.position_liquidity.to_string()
461    }
462
463    #[getter]
464    #[pyo3(name = "amount0")]
465    fn py_amount0(&self) -> String {
466        self.amount0.to_string()
467    }
468
469    #[getter]
470    #[pyo3(name = "amount1")]
471    fn py_amount1(&self) -> String {
472        self.amount1.to_string()
473    }
474
475    #[getter]
476    #[pyo3(name = "tick_lower")]
477    fn py_tick_lower(&self) -> i32 {
478        self.tick_lower
479    }
480
481    #[getter]
482    #[pyo3(name = "tick_upper")]
483    fn py_tick_upper(&self) -> i32 {
484        self.tick_upper
485    }
486
487    #[getter]
488    #[pyo3(name = "timestamp")]
489    fn py_timestamp(&self) -> Option<u64> {
490        self.timestamp.map(|x| x.as_u64())
491    }
492
493    #[getter]
494    #[pyo3(name = "ts_init")]
495    fn py_ts_init(&self) -> Option<u64> {
496        self.ts_init.map(|x| x.as_u64())
497    }
498}
499
500#[pymethods]
501#[pyo3_stub_gen::derive::gen_stub_pymethods]
502impl PoolFeeCollect {
503    /// Represents a fee collection event in a decentralized exchange (DEX) pool.
504    #[new]
505    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
506    fn py_new(
507        chain: Chain,
508        dex: Dex,
509        pool_identifier: String,
510        instrument_id: InstrumentId,
511        block: u64,
512        transaction_hash: String,
513        transaction_index: u32,
514        log_index: u32,
515        owner: String,
516        amount0: String,
517        amount1: String,
518        tick_lower: i32,
519        tick_upper: i32,
520        timestamp: u64,
521    ) -> PyResult<Self> {
522        let owner = owner.parse().map_err(to_pyvalue_err)?;
523        let amount0 = amount0.parse().map_err(to_pyvalue_err)?;
524        let amount1 = amount1.parse().map_err(to_pyvalue_err)?;
525        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
526        Ok(Self::new(
527            Arc::new(chain),
528            Arc::new(dex),
529            instrument_id,
530            pool_identifier,
531            block,
532            transaction_hash,
533            transaction_index,
534            log_index,
535            owner,
536            amount0,
537            amount1,
538            tick_lower,
539            tick_upper,
540            Some(timestamp.into()),
541        ))
542    }
543
544    fn __str__(&self) -> String {
545        self.to_string()
546    }
547
548    fn __repr__(&self) -> String {
549        format!("{self:?}")
550    }
551
552    fn __hash__(&self) -> u64 {
553        let mut hasher = DefaultHasher::new();
554        self.chain.chain_id.hash(&mut hasher);
555        self.transaction_hash.hash(&mut hasher);
556        self.log_index.hash(&mut hasher);
557        hasher.finish()
558    }
559
560    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
561        match op {
562            CompareOp::Eq => self == other,
563            CompareOp::Ne => self != other,
564            _ => panic!("Unsupported comparison for PoolFeeCollect"),
565        }
566    }
567
568    #[getter]
569    #[pyo3(name = "chain")]
570    fn py_chain(&self) -> Chain {
571        self.chain.as_ref().clone()
572    }
573
574    #[getter]
575    #[pyo3(name = "dex")]
576    fn py_dex(&self) -> Dex {
577        self.dex.as_ref().clone()
578    }
579
580    #[getter]
581    #[pyo3(name = "instrument_id")]
582    fn py_instrument_id(&self) -> InstrumentId {
583        self.instrument_id
584    }
585
586    #[getter]
587    #[pyo3(name = "pool_identifier")]
588    fn py_pool_identifier(&self) -> String {
589        self.pool_identifier.to_string()
590    }
591
592    #[getter]
593    #[pyo3(name = "block")]
594    fn py_block(&self) -> u64 {
595        self.block
596    }
597
598    #[getter]
599    #[pyo3(name = "transaction_hash")]
600    fn py_transaction_hash(&self) -> &str {
601        &self.transaction_hash
602    }
603
604    #[getter]
605    #[pyo3(name = "transaction_index")]
606    fn py_transaction_index(&self) -> u32 {
607        self.transaction_index
608    }
609
610    #[getter]
611    #[pyo3(name = "log_index")]
612    fn py_log_index(&self) -> u32 {
613        self.log_index
614    }
615
616    #[getter]
617    #[pyo3(name = "owner")]
618    fn py_owner(&self) -> String {
619        self.owner.to_string()
620    }
621
622    #[getter]
623    #[pyo3(name = "amount0")]
624    fn py_amount0(&self) -> String {
625        self.amount0.to_string()
626    }
627
628    #[getter]
629    #[pyo3(name = "amount1")]
630    fn py_amount1(&self) -> String {
631        self.amount1.to_string()
632    }
633
634    #[getter]
635    #[pyo3(name = "tick_lower")]
636    fn py_tick_lower(&self) -> i32 {
637        self.tick_lower
638    }
639
640    #[getter]
641    #[pyo3(name = "tick_upper")]
642    fn py_tick_upper(&self) -> i32 {
643        self.tick_upper
644    }
645
646    #[getter]
647    #[pyo3(name = "timestamp")]
648    fn py_timestamp(&self) -> Option<u64> {
649        self.timestamp.map(|x| x.as_u64())
650    }
651
652    #[getter]
653    #[pyo3(name = "ts_init")]
654    fn py_ts_init(&self) -> Option<u64> {
655        self.ts_init.map(|x| x.as_u64())
656    }
657}
658
659#[pymethods]
660#[pyo3_stub_gen::derive::gen_stub_pymethods]
661impl PoolFlash {
662    /// Represents a flash loan event from a Uniswap V3 pool.
663    ///
664    /// Flash loans allow users to borrow tokens without collateral as long as they are returned
665    /// within the same transaction. Fees are paid on the borrowed amount, which are added to
666    /// the pool's fee growth accumulators.
667    #[new]
668    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
669    fn py_new(
670        chain: Chain,
671        dex: Dex,
672        pool_identifier: String,
673        instrument_id: InstrumentId,
674        block: u64,
675        transaction_hash: String,
676        transaction_index: u32,
677        log_index: u32,
678        sender: String,
679        recipient: String,
680        amount0: String,
681        amount1: String,
682        paid0: String,
683        paid1: String,
684        timestamp: u64,
685    ) -> PyResult<Self> {
686        let sender = sender.parse().map_err(to_pyvalue_err)?;
687        let recipient = recipient.parse().map_err(to_pyvalue_err)?;
688        let amount0 = amount0.parse().map_err(to_pyvalue_err)?;
689        let amount1 = amount1.parse().map_err(to_pyvalue_err)?;
690        let paid0 = paid0.parse().map_err(to_pyvalue_err)?;
691        let paid1 = paid1.parse().map_err(to_pyvalue_err)?;
692        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
693        Ok(Self::new(
694            Arc::new(chain),
695            Arc::new(dex),
696            instrument_id,
697            pool_identifier,
698            block,
699            transaction_hash,
700            transaction_index,
701            log_index,
702            Some(timestamp.into()),
703            sender,
704            recipient,
705            amount0,
706            amount1,
707            paid0,
708            paid1,
709        ))
710    }
711
712    fn __str__(&self) -> String {
713        self.to_string()
714    }
715
716    fn __repr__(&self) -> String {
717        format!("{self:?}")
718    }
719
720    fn __hash__(&self) -> u64 {
721        let mut hasher = DefaultHasher::new();
722        self.chain.chain_id.hash(&mut hasher);
723        self.transaction_hash.hash(&mut hasher);
724        self.log_index.hash(&mut hasher);
725        hasher.finish()
726    }
727
728    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
729        match op {
730            CompareOp::Eq => self == other,
731            CompareOp::Ne => self != other,
732            _ => panic!("Unsupported comparison for PoolFlash"),
733        }
734    }
735
736    #[getter]
737    #[pyo3(name = "chain")]
738    fn py_chain(&self) -> Chain {
739        self.chain.as_ref().clone()
740    }
741
742    #[getter]
743    #[pyo3(name = "dex")]
744    fn py_dex(&self) -> Dex {
745        self.dex.as_ref().clone()
746    }
747
748    #[getter]
749    #[pyo3(name = "instrument_id")]
750    fn py_instrument_id(&self) -> InstrumentId {
751        self.instrument_id
752    }
753
754    #[getter]
755    #[pyo3(name = "pool_identifier")]
756    fn py_pool_identifier(&self) -> String {
757        self.pool_identifier.to_string()
758    }
759
760    #[getter]
761    #[pyo3(name = "block")]
762    fn py_block(&self) -> u64 {
763        self.block
764    }
765
766    #[getter]
767    #[pyo3(name = "transaction_hash")]
768    fn py_transaction_hash(&self) -> &str {
769        &self.transaction_hash
770    }
771
772    #[getter]
773    #[pyo3(name = "transaction_index")]
774    fn py_transaction_index(&self) -> u32 {
775        self.transaction_index
776    }
777
778    #[getter]
779    #[pyo3(name = "log_index")]
780    fn py_log_index(&self) -> u32 {
781        self.log_index
782    }
783
784    #[getter]
785    #[pyo3(name = "sender")]
786    fn py_sender(&self) -> String {
787        self.sender.to_string()
788    }
789
790    #[getter]
791    #[pyo3(name = "recipient")]
792    fn py_recipient(&self) -> String {
793        self.recipient.to_string()
794    }
795
796    #[getter]
797    #[pyo3(name = "amount0")]
798    fn py_amount0(&self) -> String {
799        self.amount0.to_string()
800    }
801
802    #[getter]
803    #[pyo3(name = "amount1")]
804    fn py_amount1(&self) -> String {
805        self.amount1.to_string()
806    }
807
808    #[getter]
809    #[pyo3(name = "paid0")]
810    fn py_paid0(&self) -> String {
811        self.paid0.to_string()
812    }
813
814    #[getter]
815    #[pyo3(name = "paid1")]
816    fn py_paid1(&self) -> String {
817        self.paid1.to_string()
818    }
819
820    #[getter]
821    #[pyo3(name = "timestamp")]
822    fn py_timestamp(&self) -> Option<u64> {
823        self.ts_event.map(|x| x.as_u64())
824    }
825}
826
827#[pymethods]
828#[pyo3_stub_gen::derive::gen_stub_pymethods]
829impl Transaction {
830    /// Represents a transaction on an EVM based blockchain.
831    #[new]
832    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
833    fn py_new(
834        chain: Chain,
835        hash: String,
836        block_hash: String,
837        block_number: u64,
838        from: String,
839        to: String,
840        gas: String,
841        gas_price: String,
842        transaction_index: u64,
843        value: String,
844    ) -> PyResult<Self> {
845        let from = from.parse().map_err(to_pyvalue_err)?;
846        let to = to.parse().map_err(to_pyvalue_err)?;
847        let gas = gas.parse().map_err(to_pyvalue_err)?;
848        let gas_price = gas_price.parse().map_err(to_pyvalue_err)?;
849        let value = value.parse().map_err(to_pyvalue_err)?;
850        Ok(Self::new(
851            chain,
852            hash,
853            block_hash,
854            block_number,
855            from,
856            to,
857            gas,
858            gas_price,
859            transaction_index,
860            value,
861        ))
862    }
863
864    fn __str__(&self) -> String {
865        format!(
866            "Transaction(chain={}, hash={}, block_number={}, from={}, to={}, value={})",
867            self.chain.name, self.hash, self.block_number, self.from, self.to, self.value
868        )
869    }
870
871    fn __repr__(&self) -> String {
872        format!("{self:?}")
873    }
874
875    fn __hash__(&self) -> u64 {
876        let mut hasher = DefaultHasher::new();
877        self.hash.hash(&mut hasher);
878        hasher.finish()
879    }
880
881    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
882        match op {
883            CompareOp::Eq => self.hash == other.hash,
884            CompareOp::Ne => self.hash != other.hash,
885            _ => panic!("Unsupported comparison for Transaction"),
886        }
887    }
888
889    #[getter]
890    #[pyo3(name = "chain")]
891    fn py_chain(&self) -> Chain {
892        self.chain.clone()
893    }
894
895    #[getter]
896    #[pyo3(name = "hash")]
897    fn py_hash(&self) -> &str {
898        &self.hash
899    }
900
901    #[getter]
902    #[pyo3(name = "block_hash")]
903    fn py_block_hash(&self) -> &str {
904        &self.block_hash
905    }
906
907    #[getter]
908    #[pyo3(name = "block_number")]
909    fn py_block_number(&self) -> u64 {
910        self.block_number
911    }
912
913    #[getter]
914    #[pyo3(name = "from")]
915    fn py_from(&self) -> String {
916        self.from.to_string()
917    }
918
919    #[getter]
920    #[pyo3(name = "to")]
921    fn py_to(&self) -> String {
922        self.to.to_string()
923    }
924
925    #[getter]
926    #[pyo3(name = "value")]
927    fn py_value(&self) -> String {
928        self.value.to_string()
929    }
930
931    #[getter]
932    #[pyo3(name = "transaction_index")]
933    fn py_transaction_index(&self) -> u64 {
934        self.transaction_index
935    }
936
937    #[getter]
938    #[pyo3(name = "gas")]
939    fn py_gas(&self) -> String {
940        self.gas.to_string()
941    }
942
943    #[getter]
944    #[pyo3(name = "gas_price")]
945    fn py_gas_price(&self) -> String {
946        self.gas_price.to_string()
947    }
948}