Last active
October 27, 2021 19:00
-
-
Save tilacog/b5ea4ac8a334f493be6fef0f38a80fcb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
edition = "2021" | |
name = "block-search-bin" | |
version = "0.1.0" | |
[dependencies] | |
anyhow = "*" | |
chrono = "*" | |
hex = "*" | |
serde_json = "*" | |
[dependencies.reqwest] | |
features = ["blocking", "json"] | |
version = "*" | |
[dependencies.serde] | |
features = ["derive"] | |
version = "1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use chrono::{naive::NaiveDateTime, Duration, NaiveDate}; | |
use serde::Deserialize; | |
use std::cmp::Ordering; | |
#[derive(Deserialize)] | |
struct ApiResponse { | |
result: RawBlock, | |
} | |
#[derive(Deserialize)] | |
struct RawBlock { | |
hash: String, | |
number: String, | |
timestamp: String, | |
} | |
#[derive(Debug)] | |
struct Block { | |
hash: String, | |
number: usize, | |
timestamp: NaiveDateTime, | |
} | |
impl TryFrom<RawBlock> for Block { | |
type Error = anyhow::Error; | |
fn try_from(raw: RawBlock) -> Result<Self, Self::Error> { | |
let timestamp = { | |
let unix_ts = i64::from_str_radix(raw.timestamp.trim_start_matches("0x"), 16)?; | |
NaiveDateTime::from_timestamp(unix_ts, 0) | |
}; | |
Ok(Block { | |
hash: raw.hash, | |
number: usize::from_str_radix(raw.number.trim_start_matches("0x"), 16)?, | |
timestamp, | |
}) | |
} | |
} | |
fn fetch_latest_block(api_url: &str) -> anyhow::Result<Block> { | |
fetch_block_inner("latest", api_url) | |
} | |
fn fetch_block(number: usize, api_url: &str) -> anyhow::Result<Block> { | |
let mut hex_number = String::from("0x"); | |
hex_number.push_str(&hex::encode(number.to_be_bytes())); | |
fetch_block_inner(&hex_number, api_url) | |
} | |
fn fetch_block_inner(number: &str, api_url: &str) -> anyhow::Result<Block> { | |
let client = reqwest::blocking::Client::new(); | |
let payload = serde_json::json!({ | |
"jsonrpc":"2.0", | |
"method":"eth_getBlockByNumber", | |
"params": [ | |
number, | |
false | |
], | |
"id":0 | |
}); | |
client | |
.post(api_url) | |
.json(&payload) | |
.send()? | |
.json::<ApiResponse>()? | |
.result | |
.try_into() | |
} | |
fn bisect( | |
latest_block: Block, | |
target_datetime: NaiveDateTime, | |
threshold: &Duration, | |
api_url: &str, | |
) -> anyhow::Result<Block> { | |
let mut upper_bound = latest_block.number; | |
let mut lower_bound = 0; | |
let mut limit = 100; | |
loop { | |
if limit <= 0 { | |
break Err(anyhow::anyhow!("Exhausted limit. Aborting.")); | |
} | |
if lower_bound == upper_bound { | |
// means this is the last attempt | |
limit = 1; | |
} | |
let midpoint = (upper_bound + lower_bound) / 2; | |
let block = fetch_block(midpoint, api_url)?; | |
if between_range(&block.timestamp, &target_datetime, &threshold) { | |
return Ok(block); | |
} else { | |
match block.timestamp.cmp(&target_datetime) { | |
Ordering::Less => { | |
lower_bound = block.number; | |
} | |
Ordering::Greater => { | |
upper_bound = block.number; | |
} | |
Ordering::Equal => unreachable!("we just checked that"), | |
} | |
} | |
limit -= 1; | |
} | |
} | |
fn between_range(check: &NaiveDateTime, target: &NaiveDateTime, threshold: &Duration) -> bool { | |
let low = *target - *threshold; | |
let high = *target + *threshold; | |
(check > &low) && (check < &high) | |
} | |
fn main() -> anyhow::Result<()> { | |
let api_url = std::env::var("ETH_API_URL")?; | |
let target_date = std::env::args() | |
.nth(1) | |
.ok_or(anyhow::anyhow!("Failed to parse block number from args")) | |
.and_then(|date_string| { | |
NaiveDate::parse_from_str(&date_string, "%Y-%m-%d") | |
.map_err(|e| anyhow::anyhow!(e.to_string())) | |
})? | |
.and_hms(0, 0, 0); | |
anyhow::ensure!(target_date <= chrono::Utc::now().naive_utc()); | |
let latest_block = fetch_latest_block(&api_url)?; | |
let threshold = Duration::minutes(10); | |
let target_block = bisect(latest_block, target_date, &threshold, &api_url)?; | |
println!( | |
"Block #{} created at {:?}", | |
target_block.number, target_block.timestamp | |
); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment