1use crate::Cast;
2use alloy_provider::Provider;
3use clap::Parser;
4use eyre::Result;
5use foundry_cli::{
6 json::print_scalar,
7 opts::RpcOpts,
8 utils::{self, LoadConfig},
9};
10use futures::join;
11
12#[derive(Clone, Debug, Parser)]
14pub struct FindBlockArgs {
15 timestamp: u64,
17
18 #[command(flatten)]
19 rpc: RpcOpts,
20}
21
22fn interpolate_block(
23 low_block: u64,
24 low_timestamp: u64,
25 high_block: u64,
26 high_timestamp: u64,
27 target_timestamp: u64,
28) -> u64 {
29 let block_range = high_block - low_block;
30 let midpoint = high_block - block_range / 2;
31 if high_timestamp <= low_timestamp {
32 return midpoint;
33 }
34
35 let timestamp_offset = target_timestamp - low_timestamp;
36 let timestamp_range = high_timestamp - low_timestamp;
37 let block_offset =
38 u128::from(timestamp_offset) * u128::from(block_range) / u128::from(timestamp_range);
39 (low_block + block_offset as u64).clamp(low_block + 1, high_block - 1)
40}
41
42impl FindBlockArgs {
43 pub async fn run(self) -> Result<()> {
44 let Self { timestamp, rpc } = self;
45
46 let ts_target = timestamp;
47 let config = rpc.load_config()?;
48 let provider = utils::get_provider(&config)?;
49
50 let last_block_num = provider.get_block_number().await?;
51 let cast_provider = Cast::new(provider);
52
53 let res = join!(cast_provider.timestamp(last_block_num), cast_provider.timestamp(1));
54 let ts_block_latest: u64 = res.0?.to();
55 let ts_block_1: u64 = res.1?.to();
56
57 let block_num = if ts_block_latest < ts_target {
58 last_block_num
60 } else if ts_block_1 > ts_target {
61 1
63 } else {
64 let mut low_block = 1_u64; let mut low_timestamp = ts_block_1;
67 let mut high_block = last_block_num;
68 let mut high_timestamp = ts_block_latest;
69 let mut interpolation_budget =
72 u64::BITS - last_block_num.saturating_sub(1).leading_zeros();
73 loop {
74 let block_range = high_block - low_block;
75 if block_range == 0 {
76 break low_block;
77 }
78 if block_range == 1 {
79 let high_diff = high_timestamp - ts_target;
81 let low_diff = ts_target - low_timestamp;
82 break if low_diff < high_diff { low_block } else { high_block };
83 }
84
85 let midpoint = high_block - block_range / 2;
86 let next_block = if interpolation_budget == 0 {
87 midpoint
88 } else {
89 interpolation_budget -= 1;
90 interpolate_block(
91 low_block,
92 low_timestamp,
93 high_block,
94 high_timestamp,
95 ts_target,
96 )
97 };
98 let next_timestamp = cast_provider.timestamp(next_block).await?.to::<u64>();
99
100 if next_timestamp == ts_target {
101 break next_block;
102 }
103 if next_timestamp < ts_target {
104 low_block = next_block;
105 low_timestamp = next_timestamp;
106 } else {
107 high_block = next_block;
108 high_timestamp = next_timestamp;
109 }
110 }
111 };
112 print_scalar(block_num)?;
113
114 Ok(())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::interpolate_block;
121
122 #[test]
123 fn interpolates_block_from_timestamps() {
124 assert_eq!(interpolate_block(1, 100, 11, 200, 150), 6);
125 }
126
127 #[test]
128 fn keeps_interpolation_inside_search_bounds() {
129 assert_eq!(interpolate_block(1, 100, 11, 200, 100), 2);
130 assert_eq!(interpolate_block(1, 100, 11, 200, 200), 10);
131 }
132
133 #[test]
134 fn interpolates_without_overflow() {
135 assert_eq!(interpolate_block(0, 0, u64::MAX, u64::MAX, u64::MAX / 2), u64::MAX / 2);
136 }
137
138 #[test]
139 fn uses_midpoint_for_equal_timestamps() {
140 assert_eq!(interpolate_block(1, 100, 10, 100, 100), 6);
141 }
142}