Skip to content

Commit

Permalink
Use official chain list (#14)
Browse files Browse the repository at this point in the history
Using the official chain list we can easily create RPC clients for any of the chains. Can update in future to allow overwriting URLs for specific chains (e.g. using our internal proxy in our deployment instead of the public url)
  • Loading branch information
ligustah authored Apr 6, 2024
1 parent 093fb30 commit cd5423f
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 14 deletions.
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ libp2p = { version = "0.53.2", features = [
] }
tokio = { version = "1.36.0", features = ["full"] }
eyre = "0.6.12"
regex = "1.10.4"
async-trait = "0.1.77"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
Expand All @@ -45,7 +46,7 @@ alloy = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b
"pubsub",
] }


alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639" }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "7e39c85f9f51e6449a8b661f54df0ac213f18639", features = [
"pubsub",
"ws",
Expand Down
1 change: 1 addition & 0 deletions data/chains.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ seed:
touch dev.db
sqlx migrate run

update-chain-list:
curl https://chainid.network/chains.json -s -odata/chains.json

ci: init seed
19 changes: 8 additions & 11 deletions src/chain.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use crate::chain_list::{Chains, CHAINS};
use crate::controller::ControllerCommands;
use crate::types::Premint;
use alloy::network::Ethereum;
use alloy::pubsub::PubSubFrontend;
use alloy_provider::{Provider, RootProvider};
use alloy_rpc_client::{RpcClient, WsConnect};
use alloy_provider::Provider;
use futures_util::StreamExt;
use tokio::sync::mpsc::Sender;

Expand Down Expand Up @@ -56,14 +55,12 @@ impl MintChecker {
}
}

async fn make_provider(&self) -> eyre::Result<RootProvider<Ethereum, PubSubFrontend>> {
let ws_transport = WsConnect::new(self.rpc_url.clone());
async fn make_provider(&self) -> eyre::Result<Box<dyn Provider<Ethereum>>> {
let chain = CHAINS.get_chain_by_id(self.chain_id as i64);

// Connect to the WS client.
let rpc_client = RpcClient::connect_pubsub(ws_transport).await?;

// Create the provider.
let provider = RootProvider::<Ethereum, _>::new(rpc_client);
Ok(provider)
match chain {
Some(c) => c.get_rpc(true).await,
None => Err(eyre::eyre!("Chain not found for id {}", self.chain_id)),
}
}
}
166 changes: 166 additions & 0 deletions src/chain_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use alloy::network::Ethereum;
use alloy_provider::{Provider, ProviderBuilder};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};

const CHAINS_JSON: &str = include_str!("../data/chains.json");

pub struct Chains(Vec<Chain>);

pub static CHAINS: Lazy<Chains> = Lazy::new(|| Chains::new());
static VARIABLE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{(.+?)}").unwrap());

impl Chains {
fn new() -> Self {
Chains(serde_json::from_str::<Vec<Chain>>(CHAINS_JSON).unwrap())
}

pub fn get_chain_by_id(&self, chain_id: i64) -> Option<Chain> {
self.0
.iter()
.find(|chain| chain.chain_id == chain_id)
.and_then(|chain| Some(chain.clone()))
}
}

// types created by https://transform.tools/json-to-rust-serde
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Chain {
pub name: String,
pub chain: String,
pub icon: Option<String>,
pub rpc: Vec<String>,
#[serde(default)]
pub features: Vec<Feature>,
pub faucets: Vec<String>,
pub native_currency: NativeCurrency,
#[serde(rename = "infoURL")]
pub info_url: String,
pub short_name: String,
pub chain_id: i64,
pub network_id: i64,
pub slip44: Option<i64>,
pub ens: Option<Ens>,
#[serde(default)]
pub explorers: Vec<Explorer>,
pub title: Option<String>,
pub status: Option<String>,
#[serde(default)]
pub red_flags: Vec<String>,
pub parent: Option<Parent>,
}

async fn connect(url: &String) -> eyre::Result<Box<dyn Provider<Ethereum>>> {
if VARIABLE_REGEX.is_match(url) {
return Err(eyre::eyre!("URL contains variables"));
}

let builder = ProviderBuilder::<_, Ethereum>::default().with_recommended_layers();
let provider = builder.on_builtin(url).await?;

Ok(Box::new(provider))
}

impl Chain {
pub async fn get_rpc(&self, need_pub_sub: bool) -> eyre::Result<Box<dyn Provider<Ethereum>>> {
for rpc in self.rpc.iter() {
if need_pub_sub && !rpc.starts_with("ws") {
continue;
}

tracing::info!("Trying to connect to {}", rpc);
let provider = connect(rpc).await;
if provider.is_ok() {
return provider;
}
}

Err(eyre::eyre!("No suitable RPC URL found for chain"))
}
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Feature {
pub name: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NativeCurrency {
pub name: String,
pub symbol: String,
pub decimals: i64,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ens {
pub registry: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Explorer {
pub name: String,
pub url: String,
pub standard: String,
pub icon: Option<String>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Parent {
#[serde(rename = "type")]
pub type_field: String,
pub chain: String,
#[serde(default)]
pub bridges: Vec<Bridge>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bridge {
pub url: String,
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_chains_new() {
let _chains = Chains::new();
}

#[test]
fn test_get_chain_by_id() {
let chain = CHAINS.get_chain_by_id(7777777);
assert!(chain.is_some());
assert_eq!(chain.unwrap().name, "Zora".to_string());
}

#[tokio::test]
async fn test_chain_connect() {
let chain = CHAINS.get_chain_by_id(7777777).unwrap();
let provider = connect(&chain.rpc[0]).await.unwrap();

// quick integration test here
let number = provider.get_block_number().await.unwrap();
assert!(number > 0);
}

#[tokio::test]
async fn test_chain_connect_variable() {
let url = "https://mainnet.infura.io/v3/${INFURA_API_KEY}".to_string();
let provider = connect(&url).await;

assert!(provider.is_err());
match provider {
Ok(_) => panic!("Expected error"),
Err(e) => assert!(e.to_string().contains("URL contains variables")),
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod api;
pub mod chain;
pub mod chain_list;
pub mod config;
pub mod controller;
pub mod p2p;
Expand Down

0 comments on commit cd5423f

Please sign in to comment.