1use std::fmt::Display;
17
18use alloy_primitives::U256;
19use nautilus_core::UnixNanos;
20use serde::{Deserialize, Serialize};
21use ustr::Ustr;
22
23use crate::defi::{
24 Blockchain,
25 hex::{
26 deserialize_hex_number, deserialize_hex_timestamp, deserialize_opt_hex_u64,
27 deserialize_opt_hex_u256,
28 },
29};
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
37)]
38pub struct Block {
39 #[serde(skip)]
41 pub chain: Option<Blockchain>, pub hash: String,
44 #[serde(deserialize_with = "deserialize_hex_number")]
46 pub number: u64,
47 pub parent_hash: String,
49 pub miner: Ustr,
51 #[serde(deserialize_with = "deserialize_hex_number")]
53 pub gas_limit: u64,
54 #[serde(deserialize_with = "deserialize_hex_number")]
56 pub gas_used: u64,
57 #[serde(default, deserialize_with = "deserialize_opt_hex_u256")]
59 pub base_fee_per_gas: Option<U256>,
60 #[serde(default, deserialize_with = "deserialize_opt_hex_u256")]
62 pub blob_gas_used: Option<U256>,
63 #[serde(default, deserialize_with = "deserialize_opt_hex_u256")]
65 pub excess_blob_gas: Option<U256>,
66 #[serde(default, deserialize_with = "deserialize_opt_hex_u256")]
68 pub l1_gas_price: Option<U256>,
69 #[serde(default, deserialize_with = "deserialize_opt_hex_u64")]
71 pub l1_gas_used: Option<u64>,
72 #[serde(default, deserialize_with = "deserialize_opt_hex_u64")]
74 pub l1_fee_scalar: Option<u64>,
75 #[serde(deserialize_with = "deserialize_hex_timestamp")]
77 pub timestamp: UnixNanos,
78}
79
80impl Block {
81 #[allow(clippy::too_many_arguments)]
83 pub fn new(
84 hash: String,
85 parent_hash: String,
86 number: u64,
87 miner: Ustr,
88 gas_limit: u64,
89 gas_used: u64,
90 timestamp: UnixNanos,
91 chain: Option<Blockchain>,
92 ) -> Self {
93 Self {
94 chain,
95 hash,
96 parent_hash,
97 number,
98 miner,
99 gas_used,
100 gas_limit,
101 timestamp,
102 base_fee_per_gas: None,
103 blob_gas_used: None,
104 excess_blob_gas: None,
105 l1_gas_price: None,
106 l1_gas_used: None,
107 l1_fee_scalar: None,
108 }
109 }
110
111 pub fn chain(&self) -> Blockchain {
117 if let Some(chain) = self.chain {
118 chain
119 } else {
120 panic!("Must have the `chain` field set")
121 }
122 }
123
124 pub fn set_chain(&mut self, chain: Blockchain) {
125 self.chain = Some(chain)
126 }
127
128 #[must_use]
130 pub fn with_base_fee(mut self, fee: U256) -> Self {
131 self.base_fee_per_gas = Some(fee);
132 self
133 }
134
135 #[must_use]
137 pub fn with_blob_gas(mut self, used: U256, excess: U256) -> Self {
138 self.blob_gas_used = Some(used);
139 self.excess_blob_gas = Some(excess);
140 self
141 }
142
143 #[must_use]
145 pub fn with_l1_fee_components(mut self, price: U256, gas_used: u64, scalar: u64) -> Self {
146 self.l1_gas_price = Some(price);
147 self.l1_gas_used = Some(gas_used);
148 self.l1_fee_scalar = Some(scalar);
149 self
150 }
151}
152
153impl PartialEq for Block {
154 fn eq(&self, other: &Self) -> bool {
155 self.hash == other.hash
156 }
157}
158
159impl Eq for Block {}
160
161impl Display for Block {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 write!(
164 f,
165 "Block(chain={}, number={}, timestamp={}, hash={})",
166 self.chain(),
167 self.number,
168 self.timestamp.to_rfc3339(),
169 self.hash
170 )
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use alloy_primitives::U256;
177 use chrono::{TimeZone, Utc};
178 use nautilus_core::UnixNanos;
179 use rstest::{fixture, rstest};
180 use ustr::Ustr;
181
182 use super::Block;
183 use crate::defi::{Blockchain, chain::chains, rpc::RpcNodeWssResponse};
184
185 #[fixture]
186 fn eth_rpc_block_response() -> String {
187 r#"{
189 "jsonrpc":"2.0",
190 "method":"eth_subscription",
191 "params":{
192 "subscription":"0xe06a2375238a4daa8ec823f585a0ef1e",
193 "result":{
194 "baseFeePerGas":"0x1862a795",
195 "blobGasUsed":"0xc0000",
196 "difficulty":"0x0",
197 "excessBlobGas":"0x4840000",
198 "extraData":"0x546974616e2028746974616e6275696c6465722e78797a29",
199 "gasLimit":"0x223b4a1",
200 "gasUsed":"0xde3909",
201 "hash":"0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a",
202 "miner":"0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97",
203 "mixHash":"0x43adbd4692459c8820b0913b0bc70e8a87bed2d40c395cc41059aa108a7cbe84",
204 "nonce":"0x0000000000000000",
205 "number":"0x1542e9f",
206 "parentBeaconBlockRoot":"0x58673bf001b31af805fb7634fbf3257dde41fbb6ae05c71799b09632d126b5c7",
207 "parentHash":"0x2abcce1ac985ebea2a2d6878a78387158f46de8d6db2cefca00ea36df4030a40",
208 "receiptsRoot":"0x35fead0b79338d4acbbc361014521d227874a1e02d24342ed3e84460df91f271",
209 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
210 "stateRoot":"0x99f29ee8ed6622c6a1520dca86e361029605f76d2e09aa7d3b1f9fc8b0268b13",
211 "timestamp":"0x6801f4bb",
212 "transactionsRoot":"0x9484b18d38886f25a44b465ad0136c792ef67dd5863b102cab2ab7a76bfb707d",
213 "withdrawalsRoot":"0x152f0040f4328639397494ef0d9c02d36c38b73f09588f304084e9f29662e9cb"
214 }
215 }
216 }"#.to_string()
217 }
218
219 #[fixture]
220 fn polygon_rpc_block_response() -> String {
221 r#"{
223 "jsonrpc": "2.0",
224 "method": "eth_subscription",
225 "params": {
226 "subscription": "0x20f7c54c468149ed99648fd09268c903",
227 "result": {
228 "baseFeePerGas": "0x19e",
229 "difficulty": "0x18",
230 "gasLimit": "0x1c9c380",
231 "gasUsed": "0x1270f14",
232 "hash": "0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199",
233 "miner": "0x0000000000000000000000000000000000000000",
234 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
235 "nonce": "0x0000000000000000",
236 "number": "0x43309ed",
237 "parentHash": "0xf25e108267e3d6e1e4aaf4e329872273f2b1ad6186a4a22e370623aa8d021c50",
238 "receiptsRoot": "0xfffb93a991d15b9689536e59f20564cc49c254ec41a222d988abe58d2869968c",
239 "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
240 "stateRoot": "0xe66a9bc516bde8fc7b8c1ba0b95bfea0f4574fc6cfe95c68b7f8ab3d3158278d",
241 "timestamp": "0x680250d5",
242 "totalDifficulty": "0x505bd180",
243 "transactionsRoot": "0xd9ebc2fd5c7ce6f69ab2e427da495b0b0dff14386723b8c07b347449fd6293a6"
244 }
245 }
246 }"#.to_string()
247 }
248
249 #[fixture]
250 fn base_rpc_block_response() -> String {
251 r#"{
252 "jsonrpc":"2.0",
253 "method":"eth_subscription",
254 "params":{
255 "subscription":"0xeb7d715d93964e22b2d99192791ca984",
256 "result":{
257 "baseFeePerGas":"0xaae54",
258 "blobGasUsed":"0x0",
259 "difficulty":"0x0",
260 "excessBlobGas":"0x0",
261 "extraData":"0x00000000fa00000002",
262 "gasLimit":"0x7270e00",
263 "gasUsed":"0x56fce26",
264 "hash":"0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad",
265 "logsBloom":"0x02bcf67d7b87f2d884b8d56bbe3965f6becc9ed8f9637ffc67efdffcef446cf435ffec7e7ce8e4544fe782bb06ef37afc97687cbf3c7ee7e26dd12a8f1fd836bc17dd2fd64fce3ef03bc74d8faedb07dddafe6f2cedff3e6f5d8683cc2ef26f763dee76e7b6fdeeade8c8a7cec7a5fdca237be97be2efe67dc908df7ce3f94a3ce150b2a9f07776fa577d5c52dbffe5bfc38bbdfeefc305f0efaf37fba3a4cdabf366b17fcb3b881badbe571dfb2fd652e879fbf37e88dbedb6a6f9f4bb7aef528e81c1f3cda38f777cb0a2d6f0ddb8abcb3dda5d976541fa062dba6255a7b328b5fdf47e8d6fac2fc43d8bee5936e6e8f2bff33526fdf6637f3f2216d950fef",
266 "miner":"0x4200000000000000000000000000000000000011",
267 "mixHash":"0xeacd829463c5d21df523005d55f25a0ca20474f1310c5c7eb29ff2c479789e98",
268 "nonce":"0x0000000000000000",
269 "number":"0x1bca2ac",
270 "parentBeaconBlockRoot":"0xfe4c48425a274a6716c569dfa9c238551330fc39d295123b12bc2461e6f41834",
271 "parentHash":"0x9a6ad4ffb258faa47ecd5eea9e7a9d8fa1772aa6232bc7cb4bbad5bc30786258",
272 "receiptsRoot":"0x5fc932dd358c33f9327a704585c83aafbe0d25d12b62c1cd8282df8b328aac16",
273 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
274 "stateRoot":"0xd2d3a6a219fb155bfc5afbde11f3161f1051d931432ccf32c33affe54176bb18",
275 "timestamp":"0x6803a23b",
276 "transactionsRoot":"0x59726fb9afc101cd49199c70bbdbc28385f4defa02949cb6e20493e16035a59d",
277 "withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
278 }
279 }
280 }"#.to_string()
281 }
282
283 #[fixture]
284 fn arbitrum_rpc_block_response() -> String {
285 r#"{
287 "jsonrpc":"2.0",
288 "method":"eth_subscription",
289 "params":{
290 "subscription":"0x0c5a0b38096440ef9a30a84837cf2012",
291 "result":{
292 "baseFeePerGas":"0x989680",
293 "difficulty":"0x1",
294 "extraData":"0xc66cd959dcdc1baf028efb61140d4461629c53c9643296cbda1c40723e97283b",
295 "gasLimit":"0x4000000000000",
296 "gasUsed":"0x17af4",
297 "hash":"0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8",
298 "miner":"0xa4b000000000000000000073657175656e636572",
299 "mixHash":"0x0000000000023106000000000154528900000000000000200000000000000000",
300 "nonce":"0x00000000001daa7c",
301 "number":"0x138d1ab4",
302 "parentHash":"0xe7176e201c2db109be479770074ad11b979de90ac850432ed38ed335803861b6",
303 "receiptsRoot":"0xefb382e3a4e3169e57920fa2367fc81c98bbfbd13611f57767dee07d3b3f96d4",
304 "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
305 "stateRoot":"0x57e5475675abf1ec4c763369342e327a04321d17eeaa730a4ca20a9cafeee380",
306 "timestamp":"0x6803a606",
307 "totalDifficulty":"0x123a3d6c",
308 "transactionsRoot":"0x710b520177ecb31fa9092d16ee593b692070912b99ddd9fcf73eb4e9dd15193d"
309 }
310 }
311 }"#.to_string()
312 }
313
314 #[rstest]
315 fn test_block_set_chain() {
316 let mut block = Block::new(
317 "0x1234567890abcdef".to_string(),
318 "0xabcdef1234567890".to_string(),
319 12345,
320 Ustr::from("0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"),
321 21000,
322 20000,
323 UnixNanos::from(1_640_995_200_000_000_000u64),
324 None,
325 );
326
327 assert!(block.chain.is_none());
328
329 let chain = Blockchain::Ethereum;
330 block.set_chain(chain);
331
332 assert_eq!(block.chain, Some(chain));
333 }
334
335 #[rstest]
336 fn test_ethereum_block_parsing(eth_rpc_block_response: String) {
337 let mut block =
338 match serde_json::from_str::<RpcNodeWssResponse<Block>>(ð_rpc_block_response) {
339 Ok(rpc_response) => rpc_response.params.result,
340 Err(e) => panic!("Failed to deserialize block response with error {e}"),
341 };
342 block.set_chain(Blockchain::Ethereum);
343
344 assert_eq!(
345 block.to_string(),
346 "Block(chain=Ethereum, number=22294175, timestamp=2025-04-18T06:44:11+00:00, hash=0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a)".to_string(),
347 );
348 assert_eq!(
349 block.hash,
350 "0x71ece187051700b814592f62774e6ebd8ebdf5efbb54c90859a7d1522ce38e0a"
351 );
352 assert_eq!(
353 block.parent_hash,
354 "0x2abcce1ac985ebea2a2d6878a78387158f46de8d6db2cefca00ea36df4030a40"
355 );
356 assert_eq!(block.number, 22294175);
357 assert_eq!(block.miner, "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97");
358 assert_eq!(
360 block.timestamp,
361 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 18, 6, 44, 11).unwrap())
362 );
363 assert_eq!(block.gas_used, 14563593);
364 assert_eq!(block.gas_limit, 35894433);
365
366 assert_eq!(block.base_fee_per_gas, Some(U256::from(0x1862a795u64)));
367 assert_eq!(block.blob_gas_used, Some(U256::from(0xc0000u64)));
368 assert_eq!(block.excess_blob_gas, Some(U256::from(0x4840000u64)));
369 }
370
371 #[rstest]
372 fn test_polygon_block_parsing(polygon_rpc_block_response: String) {
373 let mut block =
374 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&polygon_rpc_block_response) {
375 Ok(rpc_response) => rpc_response.params.result,
376 Err(e) => panic!("Failed to deserialize block response with error {e}"),
377 };
378 block.set_chain(Blockchain::Polygon);
379
380 assert_eq!(
381 block.to_string(),
382 "Block(chain=Polygon, number=70453741, timestamp=2025-04-18T13:17:09+00:00, hash=0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199)".to_string(),
383 );
384 assert_eq!(
385 block.hash,
386 "0x38ca655a2009e1748097f5559a0c20de7966243b804efeb53183614e4bebe199"
387 );
388 assert_eq!(
389 block.parent_hash,
390 "0xf25e108267e3d6e1e4aaf4e329872273f2b1ad6186a4a22e370623aa8d021c50"
391 );
392 assert_eq!(block.number, 70453741);
393 assert_eq!(block.miner, "0x0000000000000000000000000000000000000000");
394 assert_eq!(
396 block.timestamp,
397 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 18, 13, 17, 9).unwrap())
398 );
399 assert_eq!(block.gas_used, 19336980);
400 assert_eq!(block.gas_limit, 30000000);
401 assert_eq!(block.base_fee_per_gas, Some(U256::from(0x19eu64)));
402 assert!(block.blob_gas_used.is_none()); assert!(block.excess_blob_gas.is_none()); }
405
406 #[rstest]
407 fn test_base_block_parsing(base_rpc_block_response: String) {
408 let mut block =
409 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&base_rpc_block_response) {
410 Ok(rpc_response) => rpc_response.params.result,
411 Err(e) => panic!("Failed to deserialize block response with error {e}"),
412 };
413 block.set_chain(Blockchain::Base);
414
415 assert_eq!(
416 block.to_string(),
417 "Block(chain=Base, number=29139628, timestamp=2025-04-19T13:16:43+00:00, hash=0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad)".to_string(),
418 );
419 assert_eq!(
420 block.hash,
421 "0x14575c65070d455e6d20d5ee17be124917a33ce4437dd8615a56d29e8279b7ad"
422 );
423 assert_eq!(
424 block.parent_hash,
425 "0x9a6ad4ffb258faa47ecd5eea9e7a9d8fa1772aa6232bc7cb4bbad5bc30786258"
426 );
427 assert_eq!(block.number, 29139628);
428 assert_eq!(block.miner, "0x4200000000000000000000000000000000000011");
429 assert_eq!(
431 block.timestamp,
432 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 19, 13, 16, 43).unwrap())
433 );
434 assert_eq!(block.gas_used, 91213350);
435 assert_eq!(block.gas_limit, 120000000);
436
437 assert_eq!(block.base_fee_per_gas, Some(U256::from(0xaae54u64)));
438 assert_eq!(block.blob_gas_used, Some(U256::ZERO));
439 assert_eq!(block.excess_blob_gas, Some(U256::ZERO));
440 }
441
442 #[rstest]
443 fn test_arbitrum_block_parsing(arbitrum_rpc_block_response: String) {
444 let mut block =
445 match serde_json::from_str::<RpcNodeWssResponse<Block>>(&arbitrum_rpc_block_response) {
446 Ok(rpc_response) => rpc_response.params.result,
447 Err(e) => panic!("Failed to deserialize block response with error {e}"),
448 };
449 block.set_chain(Blockchain::Arbitrum);
450
451 assert_eq!(
452 block.to_string(),
453 "Block(chain=Arbitrum, number=328014516, timestamp=2025-04-19T13:32:54+00:00, hash=0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8)".to_string(),
454 );
455 assert_eq!(
456 block.hash,
457 "0x724a0af4720fd7624976f71b16163de25f8532e87d0e7058eb0c1d3f6da3c1f8"
458 );
459 assert_eq!(
460 block.parent_hash,
461 "0xe7176e201c2db109be479770074ad11b979de90ac850432ed38ed335803861b6"
462 );
463 assert_eq!(block.number, 328014516);
464 assert_eq!(block.miner, "0xa4b000000000000000000073657175656e636572");
465 assert_eq!(
467 block.timestamp,
468 UnixNanos::from(Utc.with_ymd_and_hms(2025, 4, 19, 13, 32, 54).unwrap())
469 );
470 assert_eq!(block.gas_used, 97012);
471 assert_eq!(block.gas_limit, 1125899906842624);
472
473 assert_eq!(block.base_fee_per_gas, Some(U256::from(0x989680u64)));
474 assert!(block.blob_gas_used.is_none());
475 assert!(block.excess_blob_gas.is_none());
476 }
477
478 #[rstest]
479 fn test_block_builder_helpers() {
480 let block = Block::new(
481 "0xabc".into(),
482 "0xdef".into(),
483 1,
484 Ustr::from("0x0000000000000000000000000000000000000000"),
485 100_000,
486 50_000,
487 UnixNanos::from(1_700_000_000u64),
488 Some(Blockchain::Arbitrum),
489 );
490
491 let block = block
492 .with_base_fee(U256::from(1_000u64))
493 .with_blob_gas(U256::from(0x10u8), U256::from(0x20u8))
494 .with_l1_fee_components(U256::from(30_000u64), 1_234, 1_000_000);
495
496 assert_eq!(block.chain, Some(chains::ARBITRUM.name));
497 assert_eq!(block.base_fee_per_gas, Some(U256::from(1_000u64)));
498 assert_eq!(block.blob_gas_used, Some(U256::from(0x10u8)));
499 assert_eq!(block.excess_blob_gas, Some(U256::from(0x20u8)));
500 assert_eq!(block.l1_gas_price, Some(U256::from(30_000u64)));
501 assert_eq!(block.l1_gas_used, Some(1_234));
502 assert_eq!(block.l1_fee_scalar, Some(1_000_000));
503 }
504}