1use std::fmt::{Display, Formatter};
17
18use nautilus_core::UnixNanos;
19use serde::Deserialize;
20use ustr::Ustr;
21
22use crate::defi::{
23 chain::Chain,
24 hex::{deserialize_hex_number, deserialize_hex_timestamp},
25};
26
27#[derive(Debug, Clone, Deserialize)]
29pub struct Block {
30 pub hash: String,
32 #[serde(deserialize_with = "deserialize_hex_number")]
34 pub number: u64,
35 #[serde(rename = "parentHash")]
37 pub parent_hash: String,
38 pub miner: Ustr,
40 #[serde(rename = "gasLimit", deserialize_with = "deserialize_hex_number")]
42 pub gas_limit: u64,
43 #[serde(rename = "gasUsed", deserialize_with = "deserialize_hex_number")]
45 pub gas_used: u64,
46 #[serde(deserialize_with = "deserialize_hex_timestamp")]
48 pub timestamp: UnixNanos,
49 #[serde(skip)]
51 pub chain: Option<Chain>,
52}
53
54impl Block {
55 pub fn new(
56 hash: String,
57 parent_hash: String,
58 number: u64,
59 miner: Ustr,
60 gas_limit: u64,
61 gas_used: u64,
62 timestamp: UnixNanos,
63 ) -> Self {
64 Self {
65 hash,
66 parent_hash,
67 number,
68 miner,
69 gas_used,
70 gas_limit,
71 timestamp,
72 chain: None,
73 }
74 }
75
76 pub fn set_chain(&mut self, chain: Chain) {
78 self.chain = Some(chain);
79 }
80}
81
82impl Display for Block {
83 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
84 write!(
85 f,
86 "Block({}number={}, timestamp={}, hash={})",
87 self.chain
88 .as_ref()
89 .map(|c| format!("chain={}, ", c.name))
90 .unwrap_or_default(),
91 self.number,
92 self.timestamp.to_rfc3339(),
93 self.hash
94 )
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use chrono::{TimeZone, Utc};
101 use nautilus_core::UnixNanos;
102 use rstest::{fixture, rstest};
103 use ustr::Ustr;
104
105 use super::Block;
106 use crate::defi::rpc::RpcNodeWssResponse;
107
108 #[fixture]
109 fn eth_rpc_block_response() -> String {
110 r#"{
112 "jsonrpc":"2.0",
113 "method":"eth_subscription",
114 "params":{
115 "subscription":"0xe06a2375238a4daa8ec823f585a0ef1e",
116 "result":{
117 "baseFeePerGas":"0x1862a795",
118 "blobGasUsed":"0xc0000",
119 "difficulty":"0x0",
120 "excessBlobGas":"0x4840000",
121 "extraData":"0x546974616e2028746974616e6275696c6465722e78797a29",
122 "gasLimit":"0x223b4a1",
123 "gasUsed":"0xde3909",
124 "hash":"0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a",
125 "miner":"0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97",
126 "mixHash":"0x43adbd4692459c8820b0913b0bc70e8a87bed2d40c395cc41059aa108a7cbe84",
127 "nonce":"0x0000000000000000",
128 "number":"0x1542e9f",
129 "parentBeaconBlockRoot":"0x58673bf001b31af805fb7634fbf3257dde41fbb6ae05c71799b09632d126b5c7",
130 "parentHash":"0x2abcce1ac985ebea2a2d6878a78387158f46de8d6db2cefca00ea36df4030a40",
131 "receiptsRoot":"0x35fead0b79338d4acbbc361014521d227874a1e02d24342ed3e84460df91f271",
132 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
133 "stateRoot":"0x99f29ee8ed6622c6a1520dca86e361029605f76d2e09aa7d3b1f9fc8b0268b13",
134 "timestamp":"0x6801f4bb",
135 "transactionsRoot":"0x9484b18d38886f25a44b465ad0136c792ef67dd5863b102cab2ab7a76bfb707d",
136 "withdrawalsRoot":"0x152f0040f4328639397494ef0d9c02d36c38b73f09588f304084e9f29662e9cb"
137 }
138 }
139 }"#.to_string()
140 }
141
142 #[fixture]
143 fn polygon_rpc_block_response() -> String {
144 r#"{
146 "jsonrpc": "2.0",
147 "method": "eth_subscription",
148 "params": {
149 "subscription": "0x20f7c54c468149ed99648fd09268c903",
150 "result": {
151 "baseFeePerGas": "0x19e",
152 "difficulty": "0x18",
153 "gasLimit": "0x1c9c380",
154 "gasUsed": "0x1270f14",
155 "hash": "0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199",
156 "miner": "0x0000000000000000000000000000000000000000",
157 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
158 "nonce": "0x0000000000000000",
159 "number": "0x43309ed",
160 "parentHash": "0xf25e108267e3d6e1e4aaf4e329872273f2b1ad6186a4a22e370623aa8d021c50",
161 "receiptsRoot": "0xfffb93a991d15b9689536e59f20564cc49c254ec41a222d988abe58d2869968c",
162 "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
163 "stateRoot": "0xe66a9bc516bde8fc7b8c1ba0b95bfea0f4574fc6cfe95c68b7f8ab3d3158278d",
164 "timestamp": "0x680250d5",
165 "totalDifficulty": "0x505bd180",
166 "transactionsRoot": "0xd9ebc2fd5c7ce6f69ab2e427da495b0b0dff14386723b8c07b347449fd6293a6"
167 }
168 }
169 }"#.to_string()
170 }
171
172 #[fixture]
173 fn base_rpc_block_response() -> String {
174 r#"{
175 "jsonrpc":"2.0",
176 "method":"eth_subscription",
177 "params":{
178 "subscription":"0xeb7d715d93964e22b2d99192791ca984",
179 "result":{
180 "baseFeePerGas":"0xaae54",
181 "blobGasUsed":"0x0",
182 "difficulty":"0x0",
183 "excessBlobGas":"0x0",
184 "extraData":"0x00000000fa00000002",
185 "gasLimit":"0x7270e00",
186 "gasUsed":"0x56fce26",
187 "hash":"0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad",
188 "logsBloom":"0x02bcf67d7b87f2d884b8d56bbe3965f6becc9ed8f9637ffc67efdffcef446cf435ffec7e7ce8e4544fe782bb06ef37afc97687cbf3c7ee7e26dd12a8f1fd836bc17dd2fd64fce3ef03bc74d8faedb07dddafe6f2cedff3e6f5d8683cc2ef26f763dee76e7b6fdeeade8c8a7cec7a5fdca237be97be2efe67dc908df7ce3f94a3ce150b2a9f07776fa577d5c52dbffe5bfc38bbdfeefc305f0efaf37fba3a4cdabf366b17fcb3b881badbe571dfb2fd652e879fbf37e88dbedb6a6f9f4bb7aef528e81c1f3cda38f777cb0a2d6f0ddb8abcb3dda5d976541fa062dba6255a7b328b5fdf47e8d6fac2fc43d8bee5936e6e8f2bff33526fdf6637f3f2216d950fef",
189 "miner":"0x4200000000000000000000000000000000000011",
190 "mixHash":"0xeacd829463c5d21df523005d55f25a0ca20474f1310c5c7eb29ff2c479789e98",
191 "nonce":"0x0000000000000000",
192 "number":"0x1bca2ac",
193 "parentBeaconBlockRoot":"0xfe4c48425a274a6716c569dfa9c238551330fc39d295123b12bc2461e6f41834",
194 "parentHash":"0x9a6ad4ffb258faa47ecd5eea9e7a9d8fa1772aa6232bc7cb4bbad5bc30786258",
195 "receiptsRoot":"0x5fc932dd358c33f9327a704585c83aafbe0d25d12b62c1cd8282df8b328aac16",
196 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
197 "stateRoot":"0xd2d3a6a219fb155bfc5afbde11f3161f1051d931432ccf32c33affe54176bb18",
198 "timestamp":"0x6803a23b",
199 "transactionsRoot":"0x59726fb9afc101cd49199c70bbdbc28385f4defa02949cb6e20493e16035a59d",
200 "withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
201 }
202 }
203 }"#.to_string()
204 }
205
206 #[fixture]
207 fn arbitrum_rpc_block_response() -> String {
208 r#"{
210 "jsonrpc":"2.0",
211 "method":"eth_subscription",
212 "params":{
213 "subscription":"0x0c5a0b38096440ef9a30a84837cf2012",
214 "result":{
215 "baseFeePerGas":"0x989680",
216 "difficulty":"0x1",
217 "extraData":"0xc66cd959dcdc1baf028efb61140d4461629c53c9643296cbda1c40723e97283b",
218 "gasLimit":"0x4000000000000",
219 "gasUsed":"0x17af4",
220 "hash":"0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8",
221 "miner":"0xa4b000000000000000000073657175656e636572",
222 "mixHash":"0x0000000000023106000000000154528900000000000000200000000000000000",
223 "nonce":"0x00000000001daa7c",
224 "number":"0x138d1ab4",
225 "parentHash":"0xe7176e201c2db109be479770074ad11b979de90ac850432ed38ed335803861b6",
226 "receiptsRoot":"0xefb382e3a4e3169e57920fa2367fc81c98bbfbd13611f57767dee07d3b3f96d4",
227 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
228 "stateRoot":"0x57e5475675abf1ec4c763369342e327a04321d17eeaa730a4ca20a9cafeee380",
229 "timestamp":"0x6803a606",
230 "totalDifficulty":"0x123a3d6c",
231 "transactionsRoot":"0x710b520177ecb31fa9092d16ee593b692070912b99ddd9fcf73eb4e9dd15193d"
232 }
233 }
234 }"#.to_string()
235 }
236
237 #[rstest]
238 fn test_ethereum_block_parsing(eth_rpc_block_response: String) {
239 let block = match serde_json::from_str::<RpcNodeWssResponse<Block>>(ð_rpc_block_response)
240 {
241 Ok(rpc_response) => rpc_response.params.result,
242 Err(e) => panic!("Failed to deserialize block response with error {}", e),
243 };
244 assert_eq!(
245 block.to_string(),
246 "Block(number=22294175, timestamp=2025-04-18T06:44:11+00:00, hash=0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a)".to_string(),
247 );
248 assert_eq!(
249 block.hash,
250 Ustr::from("0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a")
251 );
252 assert_eq!(
253 block.parent_hash,
254 Ustr::from("0x2abcce1ac985ebea2a2d6878a78387158f46de8d6db2cefca00ea36df4030a40")
255 );
256 assert_eq!(block.number, 22294175);
257 assert_eq!(
258 block.miner,
259 Ustr::from("0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97")
260 );
261 assert_eq!(
263 block.timestamp,
264 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 18, 6, 44, 11).unwrap())
265 );
266 assert_eq!(block.gas_used, 14563593);
267 assert_eq!(block.gas_limit, 35894433);
268 }
269
270 #[rstest]
271 fn test_polygon_block_parsing(polygon_rpc_block_response: String) {
272 let block =
273 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&polygon_rpc_block_response) {
274 Ok(rpc_response) => rpc_response.params.result,
275 Err(e) => panic!("Failed to deserialize block response with error {}", e),
276 };
277 assert_eq!(
278 block.to_string(),
279 "Block(number=70453741, timestamp=2025-04-18T13:17:09+00:00, hash=0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199)".to_string(),
280 );
281 assert_eq!(
282 block.hash,
283 Ustr::from("0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199")
284 );
285 assert_eq!(
286 block.parent_hash,
287 Ustr::from("0xf25e108267e3d6e1e4aaf4e329872273f2b1ad6186a4a22e370623aa8d021c50")
288 );
289 assert_eq!(block.number, 70453741);
290 assert_eq!(
291 block.miner,
292 Ustr::from("0x0000000000000000000000000000000000000000")
293 );
294 assert_eq!(
296 block.timestamp,
297 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 18, 13, 17, 9).unwrap())
298 );
299 assert_eq!(block.gas_used, 19336980);
300 assert_eq!(block.gas_limit, 30000000);
301 }
302
303 #[rstest]
304 fn test_base_block_parsing(base_rpc_block_response: String) {
305 let block =
306 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&base_rpc_block_response) {
307 Ok(rpc_response) => rpc_response.params.result,
308 Err(e) => panic!("Failed to deserialize block response with error {}", e),
309 };
310 assert_eq!(
311 block.to_string(),
312 "Block(number=29139628, timestamp=2025-04-19T13:16:43+00:00, hash=0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad)".to_string(),
313 );
314 assert_eq!(
315 block.hash,
316 Ustr::from("0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad")
317 );
318 assert_eq!(
319 block.parent_hash,
320 Ustr::from("0x9a6ad4ffb258faa47ecd5eea9e7a9d8fa1772aa6232bc7cb4bbad5bc30786258")
321 );
322 assert_eq!(block.number, 29139628);
323 assert_eq!(
324 block.miner,
325 Ustr::from("0x4200000000000000000000000000000000000011")
326 );
327 assert_eq!(
329 block.timestamp,
330 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 19, 13, 16, 43).unwrap())
331 );
332 assert_eq!(block.gas_used, 91213350);
333 assert_eq!(block.gas_limit, 120000000);
334 }
335
336 #[rstest]
337 fn test_arbitrum_block_parsing(arbitrum_rpc_block_response: String) {
338 let block =
339 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&arbitrum_rpc_block_response) {
340 Ok(rpc_response) => rpc_response.params.result,
341 Err(e) => panic!("Failed to deserialize block response with error {}", e),
342 };
343 assert_eq!(
344 block.to_string(),
345 "Block(number=328014516, timestamp=2025-04-19T13:32:54+00:00, hash=0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8)".to_string(),
346 );
347 assert_eq!(
348 block.hash,
349 Ustr::from("0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8")
350 );
351 assert_eq!(
352 block.parent_hash,
353 Ustr::from("0xe7176e201c2db109be479770074ad11b979de90ac850432ed38ed335803861b6")
354 );
355 assert_eq!(block.number, 328014516);
356 assert_eq!(
357 block.miner,
358 Ustr::from("0xa4b000000000000000000073657175656e636572")
359 );
360 assert_eq!(
362 block.timestamp,
363 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 19, 13, 32, 54).unwrap())
364 );
365 assert_eq!(block.gas_used, 97012);
366 assert_eq!(block.gas_limit, 1125899906842624);
367 }
368}