diff --git a/client/Cargo.toml b/client/Cargo.toml index 0362bc06..cd146888 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -22,4 +22,4 @@ tokio = "1.32.0" [dev-dependencies] hex = "0.4" -pretty_assertions = "1" +pretty_assertions = "1.4.0" diff --git a/client/src/error.rs b/client/src/error.rs index 52e7d93d..4662bec3 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -1,6 +1,9 @@ use ethers::{ - abi::EncodePackedError, contract::ContractError, prelude::Middleware, providers::ProviderError, - types::H256, + abi::EncodePackedError, + contract::ContractError, + prelude::Middleware, + providers::ProviderError, + types::{TimeError, H256}, }; #[derive(Debug, thiserror::Error)] @@ -18,6 +21,9 @@ pub enum Error { #[error(transparent)] EncodePackedError(#[from] EncodePackedError), + #[error(transparent)] + TimeError(#[from] TimeError), + #[error("Contract error {0}")] ContractError(String), @@ -44,6 +50,9 @@ pub enum Error { #[error("Message not RLP bytes encoded: {0}")] MessageNotRlpBytes(String), + + #[error("Block has no number, parent block hash is {0:?}")] + BlockHasNoNumber(H256), } impl From> for Error { diff --git a/client/src/lib.rs b/client/src/lib.rs index 359c7dd2..eeab4da9 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -14,6 +14,7 @@ pub use error::{Error, Result}; use async_trait::async_trait; use auto_impl::auto_impl; +use ethers::types::BlockNumber; use ethers::{ abi::{AbiDecode, AbiEncode, ParamType, RawLog, Token}, contract::{EthCall, EthEvent, EthLogDecode}, @@ -553,6 +554,70 @@ where } } +/// Get the block number by timestamp +/// +/// # Arguments +/// * `at_date_time`: Look for the block at this datetime +/// * `start_from_block`: Will perform search starting from this block, +/// if `None` is specified then searches from block 1. +/// * `middleware`: The client to perform requests to RPC with. +pub async fn get_block_number_by_timestamp( + at_date: chrono::DateTime, + start_from_block: Option, + middleware: M, +) -> Result> { + let start_from_block = start_from_block.unwrap_or(1_u64.into()); + + let right_block = match middleware + .get_block(BlockNumber::Latest) + .await + .map_err(|e| Error::Middleware(format!("{e}")))? + { + Some(r) => r, + None => return Ok(None), + }; + + let mut right = right_block + .number + .ok_or(Error::BlockHasNoNumber(right_block.parent_hash))?; + + let mut left = start_from_block; + + if at_date > right_block.time()? { + return Ok(None); + } + + let mut middle = left + (right - left) / 2; + + while left < right { + middle = left + (right - left) / 2; + + let middle_block = middleware + .get_block(BlockNumber::Number(middle)) + .await + .map_err(|e| Error::Middleware(format!("{e}")))? + .ok_or(Error::BlockHasNoNumber(right_block.parent_hash))?; + + let middle_block_timestamp = middle_block.time()?; + + let signed_duration_since_requested_timestamp = + middle_block_timestamp.signed_duration_since(at_date); + + let num_milliseconds = signed_duration_since_requested_timestamp.num_milliseconds(); + + // look within the 500ms margin to the right of the given date. + if (0..500).contains(&num_milliseconds) { + return Ok(Some(middle)); + } else if num_milliseconds.is_positive() { + right = middle; + } else { + left = middle; + } + } + + Ok(Some(middle)) +} + fn get_l1_bridge_burn_message_keccak( burn: &BridgeBurnFilter, l1_receiver: Address, diff --git a/client/tests/request_blocks_by_timestamp.rs b/client/tests/request_blocks_by_timestamp.rs new file mode 100644 index 00000000..338d772b --- /dev/null +++ b/client/tests/request_blocks_by_timestamp.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use chrono::{prelude::*, Datelike, TimeZone, Utc}; +use ethers::providers::{Http, Provider}; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn request_first_block_unlimited() { + let provider_l2 = Provider::::try_from("https://mainnet.era.zksync.io").unwrap(); + let client_l2 = Arc::new(provider_l2); + + let date = "2023-10-5T12:00:00Z".parse::>().unwrap(); + + let previous_midnight = Utc + .with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0) + .unwrap(); + + let res = client::get_block_number_by_timestamp(previous_midnight, None, client_l2) + .await + .unwrap() + .unwrap(); + + // First block on 2023-10-5 https://explorer.zksync.io/block/15560287 + assert_eq!(res, 15560287.into()); +} + +#[tokio::test] +async fn request_first_block_limited() { + let provider_l2 = Provider::::try_from("https://mainnet.era.zksync.io").unwrap(); + let client_l2 = Arc::new(provider_l2); + + let date = "2023-10-5T12:00:00Z".parse::>().unwrap(); + + let previous_midnight = Utc + .with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0) + .unwrap(); + + let res = + client::get_block_number_by_timestamp(previous_midnight, Some(15000000.into()), client_l2) + .await + .unwrap() + .unwrap(); + + // First block on 2023-10-5 https://explorer.zksync.io/block/15560287 + assert_eq!(res, 15560287.into()); +} + +#[tokio::test] +async fn request_from_the_future() { + let provider_l2 = Provider::::try_from("https://mainnet.era.zksync.io").unwrap(); + let client_l2 = Arc::new(provider_l2); + + let date = Utc::now(); + + let previous_midnight = Utc + .with_ymd_and_hms(date.year(), date.month(), date.day() + 1, 0, 0, 0) + .unwrap(); + + let res = + client::get_block_number_by_timestamp(previous_midnight, Some(15000000.into()), client_l2) + .await + .unwrap(); + + // No first block in the next day + assert_eq!(res, None); +}