1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use chrono::{DateTime, FixedOffset};
use std::collections::HashMap;
use web3::types::{Block, BlockId, H256};

/// Web3Dater is using `web3` client that allows to get block by date.
pub struct Web3Dater {
    web3client: web3::Web3<web3::transports::Http>,
    blocks_cache: HashMap<u64, Block<H256>>,
}

impl Web3Dater {
    /// Create a new Web3Dater instance.
    pub fn new(web3client: web3::Web3<web3::transports::Http>) -> Self {
        Self {
            web3client,
            blocks_cache: HashMap::new(),
        }
    }

    /// Clears the cache of blocks.
    pub fn clear_cache(&mut self) {
        self.blocks_cache.clear();
    }

    /// Get the closest block to the given date.
    ///
    /// `after` is a flag that indicates whether the block should be after or before the given date.
    pub async fn get_block_by_date(&mut self, date: DateTime<FixedOffset>, after: bool) -> Result<Block<H256>, web3::Error> {
        let target_timestamp = date.timestamp();

        let block_number = self.predict_block_by_date(date, None).await?;

        let block_time = self.get_avg_block_time(Some(1000), Some(block_number)).await?;

        let mut predicted_block = self.get_block(block_number).await?;
        let mut diff_time = target_timestamp - predicted_block.timestamp.as_u64() as i64;

        let mut search_block_number = block_number;

        loop {
            if diff_time.abs() < (block_time * 100.0) as i64 {
                break;
            }
            search_block_number = self.predict_block_by_date(date, Some(search_block_number)).await?;
            predicted_block = self.get_block(search_block_number).await?;
            diff_time = target_timestamp - predicted_block.timestamp.as_u64() as i64;
        }

        if diff_time.abs() > (block_time * 5.0) as i64 {
            search_block_number = (search_block_number as i64 + (diff_time as f64 / block_time).round() as i64) as u64;
        }

        loop {
            let block = self.get_block(search_block_number).await?;
            let prev_block = self.get_block(search_block_number - 1).await?;
            let next_block = self.get_block(search_block_number + 1).await?;

            if prev_block.timestamp.as_u64() <= target_timestamp as u64 && target_timestamp as u64 <= next_block.timestamp.as_u64() {
                return if after {
                    if block.timestamp.as_u64() >= target_timestamp as u64 {
                        Ok(block.clone())
                    } else {
                        Ok(next_block.clone())
                    }
                } else if block.timestamp.as_u64() <= target_timestamp as u64 {
                    Ok(block.clone())
                } else {
                    Ok(prev_block.clone())
                };
            }

            if block.timestamp.as_u64() > target_timestamp as u64 {
                search_block_number -= 1;
            } else {
                search_block_number += 1;
            }
        }
    }

    async fn get_block(&mut self, block_number: u64) -> Result<Block<H256>, web3::Error> {
        if self.blocks_cache.contains_key(&block_number) {
            return Ok(self.blocks_cache.get(&block_number).unwrap().clone());
        }
        let new_block = self.web3client.eth().block(BlockId::Number(block_number.into())).await?.unwrap();
        self.blocks_cache.insert(block_number, new_block.clone());
        Ok(new_block)
    }

    async fn predict_block_by_date(&mut self, date: DateTime<FixedOffset>, block_num: Option<u64>) -> Result<u64, web3::Error> {
        let block_time = self.get_avg_block_time(None, block_num).await?;

        let block_number = block_num.unwrap_or(self.web3client.eth().block_number().await?.as_u64());
        let block = self.get_block(block_number).await?;

        let diff_time: i64 = block.timestamp.as_u64() as i64 - date.timestamp();

        Ok((block_number as i64 - (diff_time as f64 / block_time).round() as i64) as u64)
    }

    async fn get_avg_block_time(&mut self, blocks_size: Option<u64>, block_number: Option<u64>) -> Result<f64, web3::Error> {
        let first_last_blocks_size = blocks_size.unwrap_or(100000);

        let latest_block_number = block_number.unwrap_or(self.web3client.eth().block_number().await?.as_u64());

        let first_block = self.get_block(latest_block_number - first_last_blocks_size).await?;
        let last_block = self.get_block(latest_block_number).await?;

        let block_time = ((last_block.timestamp - first_block.timestamp).as_u64() as f64 / first_last_blocks_size as f64) as f64;

        Ok(block_time)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn it_works() {
        let transport = web3::transports::Http::new("https://rpc.ankr.com/eth").unwrap();
        let web3client = web3::Web3::new(transport);

        let mut dater = Web3Dater::new(web3client.clone());

        let block = dater
            .get_block_by_date(DateTime::parse_from_rfc3339("2022-08-01T00:00:00+00:00").unwrap(), true)
            .await
            .unwrap();
        println!("{:?}", block);
    }
}