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