From 096dfbe3c17d91ff6fd56b8c79ee53a2dcb1c297 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Tue, 15 Oct 2024 14:09:35 +0200 Subject: [PATCH] feat(libs): add blockscout-chains crate (#1077) * feat: add blockscout-chains crate * feat: add derives * fix: change chain_id type * feat: add builder * chore: remove unused deps --- libs/Cargo.toml | 1 + libs/blockscout-chains/Cargo.toml | 15 ++++ libs/blockscout-chains/src/lib.rs | 112 ++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 libs/blockscout-chains/Cargo.toml create mode 100644 libs/blockscout-chains/src/lib.rs diff --git a/libs/Cargo.toml b/libs/Cargo.toml index 5bc341951..3c6c0ef53 100644 --- a/libs/Cargo.toml +++ b/libs/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "blockscout-auth", + "blockscout-chains", "blockscout-client/crate", "blockscout-db", "endpoints/swagger", diff --git a/libs/blockscout-chains/Cargo.toml b/libs/blockscout-chains/Cargo.toml new file mode 100644 index 000000000..7eb680c99 --- /dev/null +++ b/libs/blockscout-chains/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "blockscout-chains" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +reqwest-middleware = "0.3" +reqwest-retry = "0.6" + +[dev-dependencies] +tokio = { version = "1.0", features = ["macros"] } diff --git a/libs/blockscout-chains/src/lib.rs b/libs/blockscout-chains/src/lib.rs new file mode 100644 index 000000000..9aa9a06ae --- /dev/null +++ b/libs/blockscout-chains/src/lib.rs @@ -0,0 +1,112 @@ +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use serde::Deserialize; +use std::collections::HashMap; + +const CHAINS_URL: &str = "https://chains.blockscout.com/api/chains"; + +pub struct BlockscoutChainsClient { + client: ClientWithMiddleware, + url: String, +} + +impl BlockscoutChainsClient { + pub fn builder() -> BlockscoutChainsClientBuilder { + Default::default() + } + + pub async fn fetch_all(&self) -> Result { + let res = self.client.get(&self.url).send().await?; + let chains: BlockscoutChains = res.json().await?; + Ok(chains) + } +} + +impl Default for BlockscoutChainsClient { + fn default() -> Self { + Self::builder().build() + } +} + +pub struct BlockscoutChainsClientBuilder { + max_retries: u32, + url: String, +} + +impl BlockscoutChainsClientBuilder { + pub fn with_max_retries(mut self, max_retries: u32) -> Self { + self.max_retries = max_retries; + self + } + + pub fn with_url(mut self, url: String) -> Self { + self.url = url; + self + } + + pub fn build(self) -> BlockscoutChainsClient { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(self.max_retries); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + BlockscoutChainsClient { + client, + url: self.url, + } + } +} + +impl Default for BlockscoutChainsClientBuilder { + fn default() -> Self { + Self { + url: CHAINS_URL.to_string(), + max_retries: 3, + } + } +} + +pub type BlockscoutChains = HashMap; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlockscoutChainData { + pub name: String, + pub description: String, + pub ecosystem: Ecosystem, + pub is_testnet: Option, + pub layer: Option, + pub rollup_type: Option, + pub website: String, + pub explorers: Vec, + pub logo: String, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum Ecosystem { + Single(String), + Multiple(Vec), +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ExplorerConfig { + pub url: String, + pub hosted_by: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_get_blockscout_chains() { + let chains = BlockscoutChainsClient::builder() + .with_max_retries(0) + .build() + .fetch_all() + .await + .unwrap(); + assert!(!chains.is_empty()); + } +}