1use serde::{Deserialize, Deserializer};
17
18use crate::defi::{chain::Chain, hex::deserialize_hex_number};
19
20#[derive(Debug, Clone, Deserialize)]
22pub struct Transaction {
23 #[serde(rename = "chainId", deserialize_with = "deserialize_chain")]
25 pub chain: Chain,
26 pub hash: String,
28 #[serde(rename = "blockHash")]
30 pub block_hash: String,
31 #[serde(rename = "blockNumber", deserialize_with = "deserialize_hex_number")]
33 pub block_number: u64,
34 pub from: String,
36 pub to: String,
38 #[serde(deserialize_with = "deserialize_hex_number")]
40 pub value: u64,
41 #[serde(
43 rename = "transactionIndex",
44 deserialize_with = "deserialize_hex_number"
45 )]
46 pub transaction_index: u64,
47 #[serde(deserialize_with = "deserialize_hex_number")]
49 pub gas: u64,
50 #[serde(rename = "gasPrice", deserialize_with = "deserialize_hex_number")]
52 pub gas_price: u64,
53}
54
55impl Transaction {
56 #[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: String,
63 to: String,
64 gas: u64,
65 gas_price: u64,
66 transaction_index: u64,
67 value: u64,
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,
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!(tx.from, "0xd6a8749e224ecdfcc79d473d3355b1b0eb51d423");
188 assert_eq!(tx.to, "0x3c9af20c7b7809a825373881f61b5a69ef8bc6bd");
189 assert_eq!(tx.gas, 21000);
190 assert_eq!(tx.gas_price, 762999156);
191 assert_eq!(tx.transaction_index, 153);
192 assert_eq!(tx.value, 100000000);
193 }
194
195 #[rstest]
196 fn test_smart_contract_interaction_tx(eth_rpc_response_smart_contract_interaction_tx: String) {
197 let tx = match serde_json::from_str::<RpcNodeHttpResponse<Transaction>>(
198 ð_rpc_response_smart_contract_interaction_tx,
199 ) {
200 Ok(rpc_response) => rpc_response.result,
201 Err(e) => panic!("Failed to deserialize transaction RPC response: {}", e),
202 };
203 assert_eq!(tx.chain.name, Blockchain::Ethereum);
204 assert_eq!(
205 tx.hash,
206 "0x6ba6dd4a82101d8a0387f4cb4ce57a2eb64a1e1bd0679a9d4ea8448a27004a57"
207 );
208 assert_eq!(
209 tx.block_hash,
210 "0xfdba50e306d1b0ebd1971ec0440799b324229841637d8c56afbd1d6950bb09f0"
211 );
212 assert_eq!(tx.from, "0x2b711ee00b50d67667c4439c28aeaf7b75cb6e0d");
213 assert_eq!(tx.to, "0x8c0bfc04ada21fd496c55b8c50331f904306f564");
214 assert_eq!(tx.gas, 15000000);
215 assert_eq!(tx.gas_price, 1399572700);
216 assert_eq!(tx.transaction_index, 74);
217 assert_eq!(tx.value, 0);
218 }
219}