nautilus_model/defi/
block.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Represents an Ethereum-compatible blockchain block with essential metadata.
28#[derive(Debug, Clone, Deserialize)]
29pub struct Block {
30    /// The unique identifier hash of the block.
31    pub hash: String,
32    /// The block height/number in the blockchain.
33    #[serde(deserialize_with = "deserialize_hex_number")]
34    pub number: u64,
35    /// Hash of the parent block.
36    #[serde(rename = "parentHash")]
37    pub parent_hash: String,
38    /// Address of the miner or validator who produced this block.
39    pub miner: Ustr,
40    /// Maximum amount of gas allowed in this block.
41    #[serde(rename = "gasLimit", deserialize_with = "deserialize_hex_number")]
42    pub gas_limit: u64,
43    /// Total gas actually used by all transactions in this block.
44    #[serde(rename = "gasUsed", deserialize_with = "deserialize_hex_number")]
45    pub gas_used: u64,
46    /// Unix timestamp when the block was created.
47    #[serde(deserialize_with = "deserialize_hex_timestamp")]
48    pub timestamp: UnixNanos,
49    /// The blockchain that this block is part of.
50    #[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    /// Sets the blockchain network (chain) associated with this block.
77    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        // https://etherscan.io/block/22294175
111        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        // https://polygonscan.com/block/70453741
145        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        // https://arbiscan.io/block/328014516
209        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>>(&eth_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        // Timestamp of block is on Apr-18-2025 06:44:11 AM +UTC
262        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        // Timestamp of block is on Apr-18-2025 01:17:09 PM +UTC
295        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        // Timestamp of block is on Apr 19 2025 13:16:43 PM +UTC
328        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        // Timestamp of block is on Apr-19-2025 13:32:54 PM +UTC
361        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}