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