Skip to main content

evm_sdk/
block.rs

1use ethers::types::U256;
2use ethers::types::{Block as EthersBlock, H64, H256, Transaction};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6use crate::{Evm, types::EvmError};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BlockInfo {
10    /// Block number
11    pub number: Option<u64>,
12    /// Block hash
13    pub hash: Option<H256>,
14    /// Parent hash
15    pub parent_hash: H256,
16    /// Block timestamp in seconds since epoch
17    pub timestamp: U256,
18    /// Block gas limit
19    pub gas_limit: U256,
20    /// Block gas used
21    pub gas_used: U256,
22    /// Miner address (author)
23    pub miner: ethers::types::Address,
24    /// Block difficulty
25    pub difficulty: U256,
26    /// Total difficulty
27    pub total_difficulty: Option<U256>,
28    /// Block size in bytes
29    pub size: Option<U256>,
30    /// Transaction count
31    pub transaction_count: usize,
32    /// Transaction hashes (only available in basic block)
33    pub transaction_hashes: Option<Vec<H256>>,
34    /// Full transactions (only available when requested with get_block_with_txs)
35    pub transactions: Option<Vec<Transaction>>,
36    /// Base fee per gas (EIP-1559)
37    pub base_fee_per_gas: Option<U256>,
38    /// Extra data
39    pub extra_data: ethers::types::Bytes,
40    /// Sha3 uncles
41    pub sha3_uncles: H256,
42    /// Logs bloom
43    pub logs_bloom: Option<ethers::types::Bloom>,
44    /// Receipts root
45    pub receipts_root: H256,
46    /// State root
47    pub state_root: H256,
48    /// Transactions root
49    pub transactions_root: H256,
50    /// Nonce
51    pub nonce: Option<H64>,
52    /// Mix hash
53    pub mix_hash: Option<H256>,
54    /// Uncles
55    pub uncles: Vec<H256>,
56}
57
58impl BlockInfo {
59    /// Convert from Ethers block with transaction hashes
60    pub fn from_ethers_block(block: &EthersBlock<H256>) -> Self {
61        Self {
62            number: block.number.map(|n| n.as_u64()),
63            hash: block.hash,
64            parent_hash: block.parent_hash,
65            timestamp: block.timestamp,
66            gas_limit: block.gas_limit,
67            gas_used: block.gas_used,
68            miner: block.author.unwrap_or(ethers::types::Address::zero()),
69            difficulty: block.difficulty,
70            total_difficulty: block.total_difficulty,
71            size: block.size,
72            transaction_count: block.transactions.len(),
73            transaction_hashes: Some(block.transactions.clone()),
74            transactions: None,
75            base_fee_per_gas: block.base_fee_per_gas,
76            extra_data: block.extra_data.clone(),
77            sha3_uncles: block.uncles_hash,
78            logs_bloom: block.logs_bloom,
79            receipts_root: block.receipts_root,
80            state_root: block.state_root,
81            transactions_root: block.transactions_root,
82            nonce: block.nonce,
83            mix_hash: block.mix_hash,
84            uncles: block.uncles.clone(),
85        }
86    }
87
88    /// Convert from Ethers block with full transactions
89    pub fn from_ethers_block_with_txs(block: &EthersBlock<Transaction>) -> Self {
90        let transaction_hashes: Vec<H256> = block.transactions.iter().map(|tx| tx.hash).collect();
91
92        Self {
93            number: block.number.map(|n| n.as_u64()),
94            hash: block.hash,
95            parent_hash: block.parent_hash,
96            timestamp: block.timestamp,
97            gas_limit: block.gas_limit,
98            gas_used: block.gas_used,
99            miner: block.author.unwrap_or(ethers::types::Address::zero()),
100            difficulty: block.difficulty,
101            total_difficulty: block.total_difficulty,
102            size: block.size,
103            transaction_count: block.transactions.len(),
104            transaction_hashes: Some(transaction_hashes),
105            transactions: Some(block.transactions.clone()),
106            base_fee_per_gas: block.base_fee_per_gas,
107            extra_data: block.extra_data.clone(),
108            sha3_uncles: block.uncles_hash,
109            logs_bloom: block.logs_bloom,
110            receipts_root: block.receipts_root,
111            state_root: block.state_root,
112            transactions_root: block.transactions_root,
113            nonce: block.nonce,
114            mix_hash: block.mix_hash,
115            uncles: block.uncles.clone(),
116        }
117    }
118
119    /// Get block timestamp as u64 (if it fits)
120    pub fn timestamp_u64(&self) -> Option<u64> {
121        self.timestamp.try_into().ok()
122    }
123
124    /// Get block number as u64
125    pub fn number_u64(&self) -> Option<u64> {
126        self.number
127    }
128
129    /// Get gas limit as u64
130    pub fn gas_limit_u64(&self) -> Option<u64> {
131        self.gas_limit.try_into().ok()
132    }
133
134    /// Get gas used as u64
135    pub fn gas_used_u64(&self) -> Option<u64> {
136        self.gas_used.try_into().ok()
137    }
138
139    /// Calculate gas used percentage
140    pub fn gas_used_percentage(&self) -> Option<f64> {
141        let gas_used: u128 = self.gas_used.try_into().ok()?;
142        let gas_limit: u128 = self.gas_limit.try_into().ok()?;
143        if gas_limit == 0 {
144            return None;
145        }
146        Some((gas_used as f64 / gas_limit as f64) * 100.0)
147    }
148}
149
150pub struct BlockService {
151    evm: Arc<Evm>,
152}
153
154impl BlockService {
155    pub fn new(evm: Arc<Evm>) -> Self {
156        Self { evm }
157    }
158
159    /// Get block information by block number (with transaction hashes only)
160    pub async fn get_block_by_number(
161        &self,
162        block_number: u64,
163    ) -> Result<Option<BlockInfo>, EvmError> {
164        let block = self
165            .evm
166            .get_block_by_number(ethers::types::BlockNumber::Number(block_number.into()))
167            .await?;
168        Ok(block.map(|b| BlockInfo::from_ethers_block(&b)))
169    }
170
171    /// Get block information by block hash (with transaction hashes only)
172    pub async fn get_block_by_hash(&self, block_hash: H256) -> Result<Option<BlockInfo>, EvmError> {
173        let block = self.evm.get_block_by_hash(block_hash).await?;
174        Ok(block.map(|b| BlockInfo::from_ethers_block(&b)))
175    }
176
177    /// Get block with full transaction details by block number
178    pub async fn get_block_with_txs(
179        &self,
180        block_number: u64,
181    ) -> Result<Option<BlockInfo>, EvmError> {
182        let block = self
183            .evm
184            .get_block_with_txs(ethers::types::BlockNumber::Number(block_number.into()))
185            .await?;
186        Ok(block.map(|b| BlockInfo::from_ethers_block_with_txs(&b)))
187    }
188
189    /// Get block with full transaction details by block hash
190    pub async fn get_block_with_txs_by_hash(
191        &self,
192        block_hash: H256,
193    ) -> Result<Option<BlockInfo>, EvmError> {
194        let block = self.get_block_by_hash(block_hash).await?;
195        if let Some(block_info) = block {
196            if let Some(block_number) = block_info.number {
197                return self.get_block_with_txs(block_number).await;
198            }
199        }
200        Ok(None)
201    }
202
203    /// Get latest block information (with transaction hashes only)
204    pub async fn get_latest_block(&self) -> Result<Option<BlockInfo>, EvmError> {
205        let block = self
206            .evm
207            .get_block_by_number(ethers::types::BlockNumber::Latest)
208            .await?;
209        Ok(block.map(|b| BlockInfo::from_ethers_block(&b)))
210    }
211
212    /// Get latest block with full transaction details
213    pub async fn get_latest_block_with_txs(&self) -> Result<Option<BlockInfo>, EvmError> {
214        let block = self
215            .evm
216            .get_block_with_txs(ethers::types::BlockNumber::Latest)
217            .await?;
218        Ok(block.map(|b| BlockInfo::from_ethers_block_with_txs(&b)))
219    }
220
221    /// Get multiple blocks in a range
222    pub async fn get_blocks_in_range(
223        &self,
224        start: u64,
225        end: u64,
226    ) -> Result<Vec<Option<BlockInfo>>, EvmError> {
227        let mut blocks = Vec::new();
228        let mut futures = Vec::new();
229        for block_number in start..=end {
230            let service = self.evm.clone();
231            futures.push(async move {
232                service
233                    .get_block_by_number(ethers::types::BlockNumber::Number(block_number.into()))
234                    .await
235                    .ok()
236                    .flatten()
237                    .map(|b| BlockInfo::from_ethers_block(&b))
238            });
239        }
240        for future in futures {
241            blocks.push(tokio::spawn(future).await.ok().flatten());
242        }
243        Ok(blocks)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use crate::trade::{self, Trade};
250
251    use super::*;
252    use ethers::types::H256;
253    use evm_client::EvmType;
254    use std::sync::Arc;
255
256    #[tokio::test]
257    async fn lisent_liquidity_last_transaction() {
258        let evm = Arc::new(Evm::new(EvmType::ETHEREUM_MAINNET).await.unwrap());
259        let trade = Trade::new(evm.clone());
260        let block_service = evm.clone().get_block_service();
261        loop {
262            match block_service.get_latest_block().await {
263                Ok(Some(block)) => {
264                    for hash in block.transaction_hashes.unwrap() {
265                        let trade = trade
266                            .get_transactions_by_tx(&format!("{:?}", hash))
267                            .await
268                            .unwrap();
269                        println!("transaction hash: {:?}", trade.hash);
270                        println!("dex: {:?}", trade.get_dex_names());
271                        println!(
272                            "liquidity pool address: {:?}",
273                            trade.get_liquidity_pool_addresses()
274                        );
275                        println!("received: {:?}", trade.get_received_token_eth());
276                        println!("spent: {:?}", trade.get_spent_token_eth());
277                        println!("direction: {:?}", trade.getDirection());
278                    }
279                }
280                Ok(None) => println!("⚠️ Nont Block"),
281                Err(e) => println!("❌ Error: {}", e),
282            }
283            tokio::time::sleep(tokio::time::Duration::from_secs(
284                evm.clone().client.get_block_interval_time().unwrap(),
285            ))
286            .await;
287        }
288    }
289
290    #[tokio::test]
291    async fn test_poll_latest_block_per_second() {
292        let evm = Arc::new(Evm::new(EvmType::ETHEREUM_MAINNET).await.unwrap());
293        let trade = Trade::new(evm.clone());
294        let block_service = evm.clone().get_block_service();
295        for i in 0..5 {
296            match block_service.get_latest_block().await {
297                Ok(Some(block)) => {
298                    println!("✅ {} seconds: block #{:?}", i, block.number);
299                    println!("Block hash: {:?}", block.transaction_hashes);
300                    for hash in block.transaction_hashes.unwrap() {
301                        let trade = trade
302                            .get_transactions_by_tx(&format!("{:?}", hash))
303                            .await
304                            .unwrap();
305                        println!("All transactions in the block: {:?}", trade);
306                    }
307                }
308                Ok(None) => println!("⚠️  {} s: Nont Block", i),
309                Err(e) => println!("❌ {} s: Error: {}", i, e),
310            }
311            tokio::time::sleep(tokio::time::Duration::from_secs(
312                evm.clone().client.get_block_interval_time().unwrap(),
313            ))
314            .await;
315        }
316    }
317
318    #[tokio::test]
319    async fn test_get_block_by_number_success() {
320        // Test: Get an existing block
321        let evm = Evm::new(EvmType::ETHEREUM_MAINNET).await.unwrap();
322        let evm_arc = Arc::new(evm);
323        let block_service = evm_arc.get_block_service();
324        // Get the latest block number
325        let latest_block = block_service.get_latest_block().await;
326        match latest_block {
327            Ok(Some(block_info)) => {
328                println!("✅ Successfully got latest block");
329                println!("   Block number: {:?}", block_info.number);
330                println!("   Block hash: {:?}", block_info.hash);
331                println!("   Timestamp: {}", block_info.timestamp);
332                println!("   Transaction count: {}", block_info.transaction_count);
333                println!("   Gas used: {}", block_info.gas_used);
334                // Verify basic fields are not empty
335                assert!(
336                    block_info.parent_hash != H256::zero(),
337                    "Parent hash should not be zero"
338                );
339                assert!(
340                    block_info.timestamp > U256::zero(),
341                    "Timestamp should be greater than zero"
342                );
343                assert!(
344                    block_info.miner != ethers::types::Address::zero(),
345                    "Miner address should not be zero"
346                );
347                // Test conversion functions
348                if let Some(timestamp_u64) = block_info.timestamp_u64() {
349                    println!("   Timestamp(u64): {}", timestamp_u64);
350                }
351
352                if let Some(gas_percentage) = block_info.gas_used_percentage() {
353                    println!("   Gas usage percentage: {:.2}%", gas_percentage);
354                    assert!(
355                        gas_percentage >= 0.0 && gas_percentage <= 100.0,
356                        "Gas usage should be between 0-100%"
357                    );
358                }
359            }
360            Ok(None) => {
361                println!("⚠️  Latest block is None (possible node issue)");
362            }
363            Err(e) => {
364                println!("❌ Failed to get latest block: {}", e);
365                // Skip test if network issue
366                if e.to_string().contains("Rpc Error") || e.to_string().contains("timeout") {
367                    println!("   Skipping test (network issue)");
368                    return;
369                }
370                panic!("Failed to get latest block: {}", e);
371            }
372        }
373    }
374
375    #[tokio::test]
376    async fn test_get_block_by_hash_and_compare() {
377        // Test: Get block by number, then by hash, and compare results
378        let evm = Evm::new(EvmType::ETHEREUM_MAINNET).await.unwrap();
379        let evm_arc = Arc::new(evm);
380        let block_service = evm_arc.get_block_service();
381        // 1. First get the latest block
382        let latest_block_result = block_service.get_latest_block().await;
383        if let Ok(Some(latest_block)) = latest_block_result {
384            if let (Some(block_number), Some(block_hash)) = (latest_block.number, latest_block.hash)
385            {
386                println!("✅ Got latest block #{}", block_number);
387                println!("   Hash: {:?}", block_hash);
388                // 2. Get by block number
389                let by_number_result = block_service.get_block_by_number(block_number).await;
390                assert!(
391                    by_number_result.is_ok(),
392                    "Should succeed when getting by block number"
393                );
394                if let Ok(Some(by_number)) = by_number_result {
395                    // 3. Get by block hash
396                    let by_hash_result = block_service.get_block_by_hash(block_hash).await;
397                    assert!(
398                        by_hash_result.is_ok(),
399                        "Should succeed when getting by block hash"
400                    );
401                    if let Ok(Some(by_hash)) = by_hash_result {
402                        // Compare results
403                        println!("   Comparing results from block number and block hash...");
404                        // Verify basic fields are consistent
405                        assert_eq!(
406                            by_number.number, by_hash.number,
407                            "Block numbers should match"
408                        );
409                        assert_eq!(by_number.hash, by_hash.hash, "Block hashes should match");
410                        assert_eq!(
411                            by_number.parent_hash, by_hash.parent_hash,
412                            "Parent hashes should match"
413                        );
414                        assert_eq!(
415                            by_number.timestamp, by_hash.timestamp,
416                            "Timestamps should match"
417                        );
418                        assert_eq!(
419                            by_number.transaction_count, by_hash.transaction_count,
420                            "Transaction counts should match"
421                        );
422                        // Verify transaction hash lists are consistent
423                        if let (Some(number_hashes), Some(hash_hashes)) =
424                            (&by_number.transaction_hashes, &by_hash.transaction_hashes)
425                        {
426                            assert_eq!(
427                                number_hashes.len(),
428                                hash_hashes.len(),
429                                "Transaction hash list lengths should match"
430                            );
431
432                            // If there are transactions, compare the first one
433                            if !number_hashes.is_empty() {
434                                assert_eq!(
435                                    number_hashes[0], hash_hashes[0],
436                                    "First transaction hash should match"
437                                );
438                            }
439                        }
440                        println!("   ✅ Results from block number and block hash match");
441                        // Test getting block with transactions
442                        let with_txs_result = block_service.get_block_with_txs(block_number).await;
443                        match with_txs_result {
444                            Ok(Some(block_with_txs)) => {
445                                println!("   ✅ Successfully got block with transactions");
446                                println!(
447                                    "      Transaction count: {}",
448                                    block_with_txs.transaction_count
449                                );
450
451                                // Verify transaction hash list exists
452                                assert!(
453                                    block_with_txs.transaction_hashes.is_some(),
454                                    "Transaction hashes should be Some"
455                                );
456
457                                // If block contains full transactions
458                                if let Some(transactions) = &block_with_txs.transactions {
459                                    println!(
460                                        "      Full transaction count: {}",
461                                        transactions.len()
462                                    );
463                                    assert_eq!(
464                                        transactions.len(),
465                                        block_with_txs.transaction_count,
466                                        "Transaction counts should match"
467                                    );
468
469                                    // Verify each transaction has a hash
470                                    for tx in transactions {
471                                        assert_ne!(
472                                            tx.hash,
473                                            H256::zero(),
474                                            "Transaction hash should not be zero"
475                                        );
476                                    }
477                                }
478                            }
479                            Ok(None) => {
480                                println!(
481                                    "   ⚠️  Block with transactions is None (node might not support)"
482                                );
483                            }
484                            Err(e) => {
485                                println!("   ⚠️  Failed to get block with transactions: {}", e);
486                                // Skip if node doesn't support this
487                            }
488                        }
489                    }
490                }
491            } else {
492                println!("⚠️  Latest block missing number or hash");
493            }
494        } else {
495            println!("⚠️  Cannot get latest block, skipping comparison test");
496        }
497    }
498
499    #[tokio::test]
500    async fn test_get_blocks_in_range() {
501        // Test: Get blocks in a range (only when network is available)
502        let evm = Evm::new(EvmType::ETHEREUM_MAINNET).await.unwrap();
503        let evm_arc = Arc::new(evm);
504        let block_service = evm_arc.get_block_service();
505        // Get latest block number
506        let latest_block = block_service.get_latest_block().await;
507        if let Ok(Some(latest_block_info)) = latest_block {
508            if let Some(latest_number) = latest_block_info.number {
509                if latest_number >= 2 {
510                    // Get last 2 blocks
511                    let start = latest_number - 1;
512                    let end = latest_number;
513                    println!("✅ Testing get blocks in range {}-{}", start, end);
514                    let blocks_result = block_service.get_blocks_in_range(start, end).await;
515                    assert!(
516                        blocks_result.is_ok(),
517                        "Should succeed when getting blocks in range"
518                    );
519                    let blocks = blocks_result.unwrap();
520                    assert_eq!(blocks.len(), 2, "Should return 2 blocks");
521                    let mut found_blocks = 0;
522                    for (i, block_opt) in blocks.iter().enumerate() {
523                        let expected_number = start + i as u64;
524                        match block_opt {
525                            Some(block) => {
526                                found_blocks += 1;
527                                if let Some(block_number) = block.number {
528                                    println!("   Block #{}: found", block_number);
529                                    assert_eq!(
530                                        block_number, expected_number,
531                                        "Block number mismatch"
532                                    );
533                                }
534                            }
535                            None => {
536                                println!(
537                                    "   Block #{}: not found (possible node issue)",
538                                    expected_number
539                                );
540                            }
541                        }
542                    }
543                    println!("   Found {}/2 blocks", found_blocks);
544                } else {
545                    println!("⚠️  Chain too short for range test");
546                }
547            }
548        } else {
549            println!("⚠️  Cannot get latest block, skipping range test");
550        }
551    }
552}