nautilus_model/defi/data/
transaction.rs1use alloy_primitives::{Address, U256};
17use serde::{Deserialize, Deserializer};
18
19use crate::defi::{chain::Chain, hex::deserialize_hex_number};
20
21#[derive(Debug, Clone, Deserialize)]
23#[serde(rename_all = "camelCase")]
24#[cfg_attr(
25 feature = "python",
26 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
27)]
28#[cfg_attr(
29 feature = "python",
30 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
31)]
32pub struct Transaction {
33 #[serde(rename = "chainId", deserialize_with = "deserialize_chain")]
35 pub chain: Chain,
36 pub hash: String,
38 pub block_hash: String,
40 #[serde(deserialize_with = "deserialize_hex_number")]
42 pub block_number: u64,
43 pub from: Address,
45 pub to: Address,
47 pub value: U256,
49 #[serde(deserialize_with = "deserialize_hex_number")]
51 pub transaction_index: u64,
52 pub gas: U256,
54 pub gas_price: U256,
56}
57
58impl Transaction {
59 #[expect(clippy::too_many_arguments)]
61 #[must_use]
62 pub const fn new(
63 chain: Chain,
64 hash: String,
65 block_hash: String,
66 block_number: u64,
67 from: Address,
68 to: Address,
69 gas: U256,
70 gas_price: U256,
71 transaction_index: u64,
72 value: U256,
73 ) -> Self {
74 Self {
75 chain,
76 hash,
77 block_hash,
78 block_number,
79 from,
80 to,
81 value,
82 transaction_index,
83 gas,
84 gas_price,
85 }
86 }
87}
88
89pub fn deserialize_chain<'de, D>(deserializer: D) -> Result<Chain, D::Error>
95where
96 D: Deserializer<'de>,
97{
98 let hex_string = String::deserialize(deserializer)?;
99 let without_prefix = hex_string.trim_start_matches("0x");
100 let chain_id = u32::from_str_radix(without_prefix, 16).map_err(serde::de::Error::custom)?;
101
102 Chain::from_chain_id(chain_id)
103 .cloned()
104 .ok_or_else(|| serde::de::Error::custom(format!("Unknown chain ID: {chain_id}")))
105}
106
107#[cfg(test)]
108mod tests {
109 use rstest::{fixture, rstest};
110
111 use super::*;
112 use crate::defi::{chain::Blockchain, rpc::RpcNodeHttpResponse};
113
114 #[fixture]
115 fn eth_rpc_response_eth_transfer_tx() -> String {
116 r#"{
118 "jsonrpc": "2.0",
119 "id": 1,
120 "result": {
121 "blockHash": "0xfdba50e306d1b0ebd1971ec0440799b324229841637d8c56afbd1d6950bb09f0",
122 "blockNumber": "0x154a1d6",
123 "chainId": "0x1",
124 "from": "0xd6a8749e224ecdfcc79d473d3355b1b0eb51d423",
125 "gas": "0x5208",
126 "gasPrice": "0x2d7a7174",
127 "hash": "0x6d0b33a68953fdfa280a3a3d7a21e9513aed38d8587682f03728bc178b52b824",
128 "input": "0x",
129 "nonce": "0x0",
130 "r": "0x6de16d6254956674d5075951a0a814e2333c6d430e9ab21113fd0c8a11ea8435",
131 "s": "0x14c67075d1371f22936ee173d9fbd7e0284c37dd93e482df334be3a3dbd93fe9",
132 "to": "0x3c9af20c7b7809a825373881f61b5a69ef8bc6bd",
133 "transactionIndex": "0x99",
134 "type": "0x0",
135 "v": "0x25",
136 "value": "0x5f5e100"
137 }
138 }"#
139 .to_string()
140 }
141
142 #[fixture]
143 fn eth_rpc_response_smart_contract_interaction_tx() -> String {
144 r#"{
147 "jsonrpc": "2.0",
148 "id": 1,
149 "result": {
150 "accessList": [],
151 "blockHash": "0xfdba50e306d1b0ebd1971ec0440799b324229841637d8c56afbd1d6950bb09f0",
152 "blockNumber": "0x154a1d6",
153 "chainId": "0x1",
154 "from": "0x2b711ee00b50d67667c4439c28aeaf7b75cb6e0d",
155 "gas": "0xe4e1c0",
156 "gasPrice": "0x536bc8dc",
157 "hash": "0x6ba6dd4a82101d8a0387f4cb4ce57a2eb64a1e1bd0679a9d4ea8448a27004a57",
158 "maxFeePerGas": "0x559d2c91",
159 "maxPriorityFeePerGas": "0x3b9aca00",
160 "nonce": "0x4c5",
161 "r": "0x65f9cf4bb1e53b0a9c04e75f8ffb3d62872d872944d660056a5ebb92a2620e0c",
162 "s": "0x3dbab5a679327019488237def822f38566cad066ea50be5f53bc06d741a9404e",
163 "to": "0x8c0bfc04ada21fd496c55b8c50331f904306f564",
164 "transactionIndex": "0x4a",
165 "type": "0x2",
166 "v": "0x1",
167 "value": "0x0",
168 "yParity": "0x1"
169 }
170 }"#
171 .to_string()
172 }
173
174 #[rstest]
175 fn test_eth_transfer_tx(eth_rpc_response_eth_transfer_tx: String) {
176 let tx = match serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(
177 ð_rpc_response_eth_transfer_tx,
178 ) {
179 Ok(rpc_response) => rpc_response.result.unwrap(),
180 Err(e) => panic!("Failed to deserialize transaction RPC response: {e}"),
181 };
182 assert_eq!(tx.chain.name, Blockchain::Ethereum);
183 assert_eq!(
184 tx.hash,
185 "0x6d0b33a68953fdfa280a3a3d7a21e9513aed38d8587682f03728bc178b52b824"
186 );
187 assert_eq!(
188 tx.block_hash,
189 "0xfdba50e306d1b0ebd1971ec0440799b324229841637d8c56afbd1d6950bb09f0"
190 );
191 assert_eq!(tx.block_number, 22_323_670);
192 assert_eq!(
193 tx.from,
194 "0xd6a8749e224ecdfcc79d473d3355b1b0eb51d423"
195 .parse::<Address>()
196 .unwrap()
197 );
198 assert_eq!(
199 tx.to,
200 "0x3c9af20c7b7809a825373881f61b5a69ef8bc6bd"
201 .parse::<Address>()
202 .unwrap()
203 );
204 assert_eq!(tx.gas, U256::from(21000));
205 assert_eq!(tx.gas_price, U256::from(762_999_156));
206 assert_eq!(tx.transaction_index, 153);
207 assert_eq!(tx.value, U256::from(100_000_000));
208 }
209
210 #[rstest]
211 fn test_smart_contract_interaction_tx(eth_rpc_response_smart_contract_interaction_tx: String) {
212 let tx = match serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(
213 ð_rpc_response_smart_contract_interaction_tx,
214 ) {
215 Ok(rpc_response) => rpc_response.result.unwrap(),
216 Err(e) => panic!("Failed to deserialize transaction RPC response: {e}"),
217 };
218 assert_eq!(tx.chain.name, Blockchain::Ethereum);
219 assert_eq!(
220 tx.hash,
221 "0x6ba6dd4a82101d8a0387f4cb4ce57a2eb64a1e1bd0679a9d4ea8448a27004a57"
222 );
223 assert_eq!(
224 tx.block_hash,
225 "0xfdba50e306d1b0ebd1971ec0440799b324229841637d8c56afbd1d6950bb09f0"
226 );
227 assert_eq!(
228 tx.from,
229 "0x2b711ee00b50d67667c4439c28aeaf7b75cb6e0d"
230 .parse::<Address>()
231 .unwrap()
232 );
233 assert_eq!(
234 tx.to,
235 "0x8c0bfc04ada21fd496c55b8c50331f904306f564"
236 .parse::<Address>()
237 .unwrap()
238 );
239 assert_eq!(tx.gas, U256::from(15_000_000));
240 assert_eq!(tx.gas_price, U256::from(1_399_572_700));
241 assert_eq!(tx.transaction_index, 74);
242 assert_eq!(tx.value, U256::ZERO);
243 }
244
245 #[rstest]
246 fn test_transaction_with_large_values() {
247 let large_value_tx = r#"{
249 "jsonrpc": "2.0",
250 "id": 1,
251 "result": {
252 "blockHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
253 "blockNumber": "0x1000000",
254 "chainId": "0x1",
255 "from": "0x0000000000000000000000000000000000000001",
256 "gas": "0xffffffffffffffff",
257 "gasPrice": "0xde0b6b3a7640000",
258 "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
259 "to": "0x0000000000000000000000000000000000000002",
260 "transactionIndex": "0x0",
261 "value": "0xde0b6b3a7640000"
262 }
263 }"#;
264
265 let tx = serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(large_value_tx)
266 .expect("Should parse large value transaction")
267 .result
268 .unwrap();
269
270 assert_eq!(tx.gas, U256::from(u64::MAX));
272 assert_eq!(tx.gas_price, U256::from(1_000_000_000_000_000_000u64)); assert_eq!(tx.value, U256::from(1_000_000_000_000_000_000u64)); assert_eq!(tx.block_number, 16_777_216); }
276
277 #[rstest]
278 fn test_transaction_parsing_with_invalid_address_should_fail() {
279 let invalid_address_tx = r#"{
280 "jsonrpc": "2.0",
281 "id": 1,
282 "result": {
283 "blockHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
284 "blockNumber": "0x1",
285 "chainId": "0x1",
286 "from": "0xinvalid_address",
287 "gas": "0x5208",
288 "gasPrice": "0x2d7a7174",
289 "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
290 "to": "0x0000000000000000000000000000000000000002",
291 "transactionIndex": "0x0",
292 "value": "0x0"
293 }
294 }"#;
295
296 let result = serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(invalid_address_tx);
297 assert!(result.is_err(), "Should fail to parse invalid address");
298 }
299
300 #[rstest]
301 fn test_transaction_parsing_with_unknown_chain_should_fail() {
302 let unknown_chain_tx = r#"{
303 "jsonrpc": "2.0",
304 "id": 1,
305 "result": {
306 "blockHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
307 "blockNumber": "0x1",
308 "chainId": "0x999999",
309 "from": "0x0000000000000000000000000000000000000001",
310 "gas": "0x5208",
311 "gasPrice": "0x2d7a7174",
312 "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
313 "to": "0x0000000000000000000000000000000000000002",
314 "transactionIndex": "0x0",
315 "value": "0x0"
316 }
317 }"#;
318
319 let result = serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(unknown_chain_tx);
320 assert!(result.is_err(), "Should fail to parse unknown chain ID");
321 }
322
323 #[rstest]
324 fn test_transaction_creation_with_constructor() {
325 use crate::defi::chain::chains;
326
327 let chain = chains::ETHEREUM.clone();
328 let from_addr = "0x0000000000000000000000000000000000000001"
329 .parse::<Address>()
330 .unwrap();
331 let to_addr = "0x0000000000000000000000000000000000000002"
332 .parse::<Address>()
333 .unwrap();
334
335 let tx = Transaction::new(
336 chain,
337 "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
338 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
339 123_456,
340 from_addr,
341 to_addr,
342 U256::from(21_000),
343 U256::from(20_000_000_000u64), 0,
345 U256::from(1_000_000_000_000_000_000u64), );
347
348 assert_eq!(tx.from, from_addr);
349 assert_eq!(tx.to, to_addr);
350 assert_eq!(tx.gas, U256::from(21_000));
351 assert_eq!(tx.gas_price, U256::from(20_000_000_000u64));
352 assert_eq!(tx.value, U256::from(1_000_000_000_000_000_000u64));
353 }
354}