Skip to content

Instantly share code, notes, and snippets.

@tilacog
Last active October 27, 2021 19:00
Show Gist options
  • Save tilacog/b5ea4ac8a334f493be6fef0f38a80fcb to your computer and use it in GitHub Desktop.
Save tilacog/b5ea4ac8a334f493be6fef0f38a80fcb to your computer and use it in GitHub Desktop.
[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"
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