Repository: gakonst/ethers-rs
Pull request #2349 (2023-04-14 04:09:20)

feat(etherscan): Add get_block_by_timestamp to ethers-etherscan

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

The Etherscan API has a blocks endpoint with a variety of useful methods. This PR incorporates the `get-block-number-by-timestamp` [endpoint](https://docs.etherscan.io/api-endpoints/blocks#get-block-number-by-timestamp). I need to be able to fetch blocks by timestamps to evaluate blockchain state by timestamp and this seems like the best home for that functionality given the `ethers-etherscan` module.

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

Add the API endpoint.

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

-   [x] Added Tests
-   [x] Added Documentation
-   ~[ ] Breaking changes~ (No breaking changes)


0xrainbowtrout: Just a ping here -- No rush but didn't want it to fall off if you hadn't seen it.
0xrainbowtrout: Fixed formatting from the failed clippy/fmt workflows.

Patch:
diff --git a/ethers-etherscan/src/blocks.rs b/ethers-etherscan/src/blocks.rs
new file mode 100644
index 00000000..f009e32b
--- /dev/null
+++ b/ethers-etherscan/src/blocks.rs
@@ -0,0 +1,47 @@
+use crate::{Client, EtherscanError, Response, Result};
+use ethers_core::types::BlockNumber;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct BlockNumberByTimestamp {
+    pub timestamp: u64,
+    pub block_number: BlockNumber,
+}
+
+impl Client {
+    /// Returns either (1) the oldest block since a particular timestamp occurred or (2) the newest
+    /// block that occurred prior to that timestamp
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// # async fn foo(client: ethers_etherscan::Client) -> Result<(), Box<dyn std::error::Error>> {
+    /// // The newest block that occurred prior to 1 January 2020
+    /// let block_number_before = client.get_block_by_timestamp(1577836800, "before");
+    /// // The oldest block that occurred after 1 January 2020
+    /// let block_number_after = client.get_block_by_timestamp(1577836800, "after");
+    /// # Ok(()) }
+    /// ```
+    pub async fn get_block_by_timestamp(
+        &self,
+        timestamp: u64,
+        closest: &str,
+    ) -> Result<BlockNumberByTimestamp> {
+        let query = self.create_query(
+            "block",
+            "getblocknobytime",
+            HashMap::from([("timestamp", timestamp.to_string()), ("closest", closest.to_string())]),
+        );
+        let response: Response<String> = self.get_json(&query).await?;
+
+        match response.status.as_str() {
+            "0" => Err(EtherscanError::BlockNumberByTimestampFailed),
+            "1" => Ok(BlockNumberByTimestamp {
+                timestamp,
+                block_number: response.result.parse::<BlockNumber>().unwrap(),
+            }),
+            err => Err(EtherscanError::BadStatusCode(err.to_string())),
+        }
+    }
+}
diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs
index 5f71f52d..d3208e7a 100644
--- a/ethers-etherscan/src/errors.rs
+++ b/ethers-etherscan/src/errors.rs
@@ -9,6 +9,8 @@ pub enum EtherscanError {
     ExecutionFailed(String),
     #[error("Balance failed")]
     BalanceFailed,
+    #[error("Block by timestamp failed")]
+    BlockNumberByTimestampFailed,
     #[error("Transaction receipt failed")]
     TransactionReceiptFailed,
     #[error("Gas estimation failed")]
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index c1ea855e..12827955 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -20,6 +20,7 @@ use std::{
 use tracing::{error, trace};
 
 pub mod account;
+pub mod blocks;
 pub mod contract;
 pub mod errors;
 pub mod gas;


Test patch:
diff --git a/ethers-etherscan/tests/it/blocks.rs b/ethers-etherscan/tests/it/blocks.rs
new file mode 100644
index 00000000..9c7ee6a2
--- /dev/null
+++ b/ethers-etherscan/tests/it/blocks.rs
@@ -0,0 +1,28 @@
+use crate::*;
+use ethers_core::types::BlockNumber;
+use serial_test::serial;
+
+#[tokio::test]
+#[serial]
+async fn check_get_block_by_timestamp_before() {
+    run_with_client(Chain::Mainnet, |client| async move {
+        let block_no = client.get_block_by_timestamp(1577836800, "before").await;
+        assert!(block_no.is_ok());
+
+        let block_no = block_no.unwrap().block_number;
+        assert_eq!(block_no, "9193265".parse::<BlockNumber>().unwrap());
+    })
+    .await
+}
+
+#[tokio::test]
+#[serial]
+async fn check_get_block_by_timestamp_after() {
+    run_with_client(Chain::Mainnet, |client| async move {
+        let block_no = client.get_block_by_timestamp(1577836800, "after").await;
+
+        let block_no = block_no.unwrap().block_number;
+        assert_eq!(block_no, "9193266".parse::<BlockNumber>().unwrap());
+    })
+    .await
+}
diff --git a/ethers-etherscan/tests/it/main.rs b/ethers-etherscan/tests/it/main.rs
index e0548705..0eac9aa1 100644
--- a/ethers-etherscan/tests/it/main.rs
+++ b/ethers-etherscan/tests/it/main.rs
@@ -10,6 +10,7 @@ use std::{
 };
 
 mod account;
+mod blocks;
 mod contract;
 mod gas;
 mod transaction;