diff --git a/SUPPORTED_CHAINS.md b/SUPPORTED_CHAINS.md index 23939fdbe..74e2134cd 100644 --- a/SUPPORTED_CHAINS.md +++ b/SUPPORTED_CHAINS.md @@ -29,6 +29,9 @@ Chain name with associated `chainId` query param to use. - Base Goerli (`eip155:84531`) - Zora (`eip155:7777777`) - Zora Goerli (`eip155:999`) +- Boba Ethereum (`eip155:288`) +- Boba BNB (`eip155:56288`) +- Boba BNB Testnet (`eip155:9728`) ## WebSocket RPC @@ -41,7 +44,14 @@ WebSocket RPC is not recommended for production use, and may be removed in the f - Optimism Goerli (`eip155:420`) - Arbitrum (`eip155:42161`) - Arbitrum Goerli (`eip155:421613`) +- Polygon (`eip155:137`) +- Polygon Mumbai (`eip155:80001`) - Aurora (`eip155:1313161554`) - Aurora Testnet (`eip155:1313161555`) +- Base (`eip155:8453`) +- Base Goerli (`eip155:84531`) - Zora (`eip155:7777777`) - Zora Goerli (`eip155:999`) +- Boba Ethereum (`eip155:288`) +- Boba BNB (`eip155:56288`) +- Boba BNB Testnet (`eip155:9728`) diff --git a/src/env/mod.rs b/src/env/mod.rs index b795eef43..aaf352a24 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -17,6 +17,7 @@ mod omnia; mod pokt; mod publicnode; mod server; +mod tenderly; mod zksync; mod zora; @@ -28,6 +29,7 @@ pub use { pokt::*, publicnode::*, server::*, + tenderly::*, zksync::*, zora::*, }; diff --git a/src/env/tenderly.rs b/src/env/tenderly.rs new file mode 100644 index 000000000..9e211663b --- /dev/null +++ b/src/env/tenderly.rs @@ -0,0 +1,180 @@ +use { + super::ProviderConfig, + crate::providers::{Priority, Weight}, + std::collections::HashMap, +}; + +#[derive(Debug)] +pub struct TenderlyConfig { + pub supported_chains: HashMap, + pub supported_ws_chains: HashMap, +} + +impl Default for TenderlyConfig { + fn default() -> Self { + Self { + supported_chains: default_supported_chains(), + supported_ws_chains: default_ws_supported_chains(), + } + } +} + +impl ProviderConfig for TenderlyConfig { + fn supported_chains(self) -> HashMap { + self.supported_chains + } + + fn supported_ws_chains(self) -> HashMap { + self.supported_ws_chains + } + + fn provider_kind(&self) -> crate::providers::ProviderKind { + crate::providers::ProviderKind::Tenderly + } +} + +fn default_supported_chains() -> HashMap { + // Keep in-sync with SUPPORTED_CHAINS.md + + HashMap::from([ + // Ethereum Mainnet + ( + "eip155:1".into(), + ("mainnet".into(), Weight::new(Priority::Low).unwrap()), + ), + // Ethereum Görli + ( + "eip155:5".into(), + ("goerli".into(), Weight::new(Priority::Low).unwrap()), + ), + // Ethereum Sepolia + ( + "eip155:11155111".into(), + ("sepolia".into(), Weight::new(Priority::Low).unwrap()), + ), + // Optimism Mainnet + ( + "eip155:10".into(), + ("optimism".into(), Weight::new(Priority::Low).unwrap()), + ), + // Optimism Görli + ( + "eip155:420".into(), + ( + "optimism-goerli".into(), + Weight::new(Priority::Low).unwrap(), + ), + ), + // Polygon Mainnet + ( + "eip155:137".into(), + ("polygon".into(), Weight::new(Priority::Low).unwrap()), + ), + // Polygon Mumbai + ( + "eip155:80001".into(), + ("polygon-mumbai".into(), Weight::new(Priority::Low).unwrap()), + ), + // Base Mainnet + ( + "eip155:8453".into(), + ("base".into(), Weight::new(Priority::Low).unwrap()), + ), + // Base Görli + ( + "eip155:84531".into(), + ("base-goerli".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba Ethereum Mainnet + ( + "eip155:288".into(), + ("boba-ethereum".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba BNB Mainnet + ( + "eip155:56288".into(), + ("boba-bnb".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba BNB Testnet + ( + "eip155:9728".into(), + ( + "boba-bnb-testnet".into(), + Weight::new(Priority::Low).unwrap(), + ), + ), + ]) +} + +fn default_ws_supported_chains() -> HashMap { + // Keep in-sync with SUPPORTED_CHAINS.md + + HashMap::from([ + // Ethereum Mainnet + ( + "eip155:1".into(), + ("mainnet".into(), Weight::new(Priority::Low).unwrap()), + ), + // Ethereum Görli + ( + "eip155:5".into(), + ("goerli".into(), Weight::new(Priority::Low).unwrap()), + ), + // Ethereum Sepolia + ( + "eip155:11155111".into(), + ("sepolia".into(), Weight::new(Priority::Low).unwrap()), + ), + // Optimism Mainnet + ( + "eip155:10".into(), + ("optimism".into(), Weight::new(Priority::Low).unwrap()), + ), + // Optimism Görli + ( + "eip155:420".into(), + ( + "optimism-goerli".into(), + Weight::new(Priority::Low).unwrap(), + ), + ), + // Polygon Mainnet + ( + "eip155:137".into(), + ("polygon".into(), Weight::new(Priority::Low).unwrap()), + ), + // Polygon Mumbai + ( + "eip155:80001".into(), + ("polygon-mumbai".into(), Weight::new(Priority::Low).unwrap()), + ), + // Base Mainnet + ( + "eip155:8453".into(), + ("base".into(), Weight::new(Priority::Low).unwrap()), + ), + // Base Görli + ( + "eip155:84531".into(), + ("base-goerli".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba Ethereum Mainnet + ( + "eip155:288".into(), + ("boba-ethereum".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba BNB Mainnet + ( + "eip155:56288".into(), + ("boba-bnb".into(), Weight::new(Priority::Low).unwrap()), + ), + // Boba BNB Testnet + ( + "eip155:9728".into(), + ( + "boba-bnb-testnet".into(), + Weight::new(Priority::Low).unwrap(), + ), + ), + ]) +} diff --git a/src/lib.rs b/src/lib.rs index 615165f75..275035d2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use { OmniatechConfig, PoktConfig, PublicnodeConfig, + TenderlyConfig, ZKSyncConfig, ZoraConfig, }, @@ -34,6 +35,8 @@ use { PoktProvider, ProviderRepository, PublicnodeProvider, + TenderlyProvider, + TenderlyWsProvider, ZKSyncProvider, ZoraProvider, ZoraWsProvider, @@ -246,10 +249,13 @@ fn init_providers() -> ProviderRepository { providers .add_provider::(InfuraConfig::new(infura_project_id.clone())); providers.add_provider::(ZoraConfig::default()); + providers.add_provider::(TenderlyConfig::default()); + // WebSocket Providers providers .add_ws_provider::(InfuraConfig::new(infura_project_id)); providers.add_ws_provider::(ZoraConfig::default()); + providers.add_ws_provider::(TenderlyConfig::default()); providers } diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 7e0fd35ea..7af6941ad 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -20,6 +20,7 @@ mod infura; mod omnia; mod pokt; mod publicnode; +mod tenderly; mod weights; mod zerion; mod zksync; @@ -37,6 +38,7 @@ pub use { omnia::OmniatechProvider, pokt::PoktProvider, publicnode::PublicnodeProvider, + tenderly::{TenderlyProvider, TenderlyWsProvider}, zksync::ZKSyncProvider, zora::{ZoraProvider, ZoraWsProvider}, }; @@ -227,6 +229,7 @@ pub enum ProviderKind { Binance, ZKSync, Publicnode, + Tenderly, Omniatech, Base, Zora, @@ -240,6 +243,7 @@ impl Display for ProviderKind { ProviderKind::Binance => "Binance", ProviderKind::ZKSync => "zkSync", ProviderKind::Publicnode => "Publicnode", + ProviderKind::Tenderly => "Tenderly", ProviderKind::Omniatech => "Omniatech", ProviderKind::Base => "Base", ProviderKind::Zora => "Zora", @@ -255,6 +259,7 @@ impl ProviderKind { "Binance" => Some(Self::Binance), "zkSync" => Some(Self::ZKSync), "Publicnode" => Some(Self::Publicnode), + "Tenderly" => Some(Self::Tenderly), "Omniatech" => Some(Self::Omniatech), "Base" => Some(Self::Base), "Zora" => Some(Self::Zora), diff --git a/src/providers/tenderly.rs b/src/providers/tenderly.rs new file mode 100644 index 000000000..e44115415 --- /dev/null +++ b/src/providers/tenderly.rs @@ -0,0 +1,158 @@ +use { + super::{ + Provider, + ProviderKind, + RateLimited, + RpcProvider, + RpcProviderFactory, + RpcQueryParams, + RpcWsProvider, + WS_PROXY_TASK_METRICS, + }, + crate::{ + env::TenderlyConfig, + error::{RpcError, RpcResult}, + ws, + }, + async_trait::async_trait, + axum::response::{IntoResponse, Response}, + axum_tungstenite::WebSocketUpgrade, + hyper::{client::HttpConnector, http, Client, Method}, + hyper_tls::HttpsConnector, + std::collections::HashMap, + wc::future::FutureExt, +}; + +#[derive(Debug)] +pub struct TenderlyProvider { + pub client: Client>, + pub supported_chains: HashMap, +} + +#[derive(Debug)] +pub struct TenderlyWsProvider { + pub supported_chains: HashMap, +} + +impl Provider for TenderlyWsProvider { + fn supports_caip_chainid(&self, chain_id: &str) -> bool { + self.supported_chains.contains_key(chain_id) + } + + fn supported_caip_chains(&self) -> Vec { + self.supported_chains.keys().cloned().collect() + } + + fn provider_kind(&self) -> ProviderKind { + ProviderKind::Tenderly + } +} + +#[async_trait] +impl RateLimited for TenderlyWsProvider { + async fn is_rate_limited(&self, response: &mut Response) -> bool + where + Self: Sized, + { + response.status() == http::StatusCode::TOO_MANY_REQUESTS + } +} + +#[async_trait] +impl RpcWsProvider for TenderlyWsProvider { + async fn proxy( + &self, + ws: WebSocketUpgrade, + query_params: RpcQueryParams, + ) -> RpcResult { + let chain = &self + .supported_chains + .get(&query_params.chain_id.to_lowercase()) + .ok_or(RpcError::ChainNotFound)?; + + let project_id = query_params.project_id; + + let uri = format!("wss://{}.gateway.tenderly.co", chain); + + let (websocket_provider, _) = async_tungstenite::tokio::connect_async(uri).await?; + + Ok(ws.on_upgrade(move |socket| { + ws::proxy(project_id, socket, websocket_provider) + .with_metrics(WS_PROXY_TASK_METRICS.with_name("tenderly")) + })) + } +} + +impl Provider for TenderlyProvider { + fn supports_caip_chainid(&self, chain_id: &str) -> bool { + self.supported_chains.contains_key(chain_id) + } + + fn supported_caip_chains(&self) -> Vec { + self.supported_chains.keys().cloned().collect() + } + + fn provider_kind(&self) -> ProviderKind { + ProviderKind::Tenderly + } +} + +#[async_trait] +impl RateLimited for TenderlyProvider { + async fn is_rate_limited(&self, response: &mut Response) -> bool + where + Self: Sized, + { + response.status() == http::StatusCode::TOO_MANY_REQUESTS + } +} + +#[async_trait] +impl RpcProvider for TenderlyProvider { + async fn proxy(&self, chain_id: &str, body: hyper::body::Bytes) -> RpcResult { + let chain = &self + .supported_chains + .get(chain_id) + .ok_or(RpcError::ChainNotFound)?; + + let uri = format!("https://{}.gateway.tenderly.co", chain); + + let hyper_request = hyper::http::Request::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .body(hyper::body::Body::from(body))?; + + let response = self.client.request(hyper_request).await?.into_response(); + + Ok(response) + } +} + +impl RpcProviderFactory for TenderlyProvider { + fn new(provider_config: &TenderlyConfig) -> Self { + let forward_proxy_client = Client::builder().build::<_, hyper::Body>(HttpsConnector::new()); + let supported_chains: HashMap = provider_config + .supported_chains + .iter() + .map(|(k, v)| (k.clone(), v.0.clone())) + .collect(); + + TenderlyProvider { + client: forward_proxy_client, + supported_chains, + } + } +} + +impl RpcProviderFactory for TenderlyWsProvider { + fn new(provider_config: &TenderlyConfig) -> Self { + let supported_chains: HashMap = provider_config + .supported_ws_chains + .iter() + .map(|(k, v)| (k.clone(), v.0.clone())) + .collect(); + + TenderlyWsProvider { supported_chains } + } +} diff --git a/terraform/monitoring/dashboard.jsonnet b/terraform/monitoring/dashboard.jsonnet index 10f83a1f0..224c11025 100644 --- a/terraform/monitoring/dashboard.jsonnet +++ b/terraform/monitoring/dashboard.jsonnet @@ -76,6 +76,7 @@ dashboard.new( panels.weights.provider(ds, vars, 'Pokt') { gridPos: pos._4 }, panels.weights.provider(ds, vars, 'Base') { gridPos: pos._4 }, panels.weights.provider(ds, vars, 'Zora') { gridPos: pos._4 }, + panels.weights.provider(ds, vars, 'Tenderly') { gridPos: pos._4 }, row.new('Status Codes'), panels.status.provider(ds, vars, 'Infura') { gridPos: pos._4 }, @@ -86,6 +87,7 @@ dashboard.new( panels.status.provider(ds, vars, 'Pokt') { gridPos: pos._4 }, panels.status.provider(ds, vars, 'Base') { gridPos: pos._4 }, panels.status.provider(ds, vars, 'Zora') { gridPos: pos._4 }, + panels.status.provider(ds, vars, 'Tenderly') { gridPos: pos._4 }, row.new('Proxy Metrics'), panels.proxy.calls(ds, vars) { gridPos: pos._2 }, diff --git a/tests/functional/http/mod.rs b/tests/functional/http/mod.rs index 4042f85d6..0c8da7290 100644 --- a/tests/functional/http/mod.rs +++ b/tests/functional/http/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod base; pub(crate) mod binance; pub(crate) mod infura; pub(crate) mod pokt; +pub(crate) mod tenderly; pub(crate) mod zksync; pub(crate) mod zora; diff --git a/tests/functional/http/tenderly.rs b/tests/functional/http/tenderly.rs new file mode 100644 index 000000000..6e2b8afa7 --- /dev/null +++ b/tests/functional/http/tenderly.rs @@ -0,0 +1,46 @@ +use { + super::check_if_rpc_is_responding_correctly_for_supported_chain, + crate::context::ServerContext, + test_context::test_context, +}; + +#[test_context(ServerContext)] +#[tokio::test] +async fn tenderly_provider(ctx: &mut ServerContext) { + // Ethereum Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:1", "0x1").await; + + // Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:5", "0x5").await; + + // Sepolia + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:11155111", "0xaa36a7") + .await; + + // Optimism Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:10", "0xa").await; + + // Optimism Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:420", "0x1A4").await; + + // Polygon Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:137", "0x89").await; + + // Polygon Mumbai + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:80001", "0x13881").await; + + // Base Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:8453", "0x2105").await; + + // Base Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:84531", "0x14a33").await; + + // Boba Ethereum Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:288", "0x120").await; + + // Boba BNB Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:56288", "0xdbe0").await; + + // Boba BNB Testnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:9728", "0x2600").await; +} diff --git a/tests/functional/websocket/mod.rs b/tests/functional/websocket/mod.rs index 47f9f1325..ec5abd97e 100644 --- a/tests/functional/websocket/mod.rs +++ b/tests/functional/websocket/mod.rs @@ -4,6 +4,7 @@ use { }; pub(crate) mod infura; +pub(crate) mod tenderly; pub(crate) mod zora; async fn check_if_rpc_is_responding_correctly_for_supported_chain( diff --git a/tests/functional/websocket/tenderly.rs b/tests/functional/websocket/tenderly.rs new file mode 100644 index 000000000..8f6a0f041 --- /dev/null +++ b/tests/functional/websocket/tenderly.rs @@ -0,0 +1,46 @@ +use { + super::check_if_rpc_is_responding_correctly_for_supported_chain, + crate::context::ServerContext, + test_context::test_context, +}; + +#[test_context(ServerContext)] +#[tokio::test] +async fn tenderly_websocket_provider(ctx: &mut ServerContext) { + // Ethereum Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:1", "0x1").await; + + // Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:5", "0x5").await; + + // Sepolia + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:11155111", "0xaa36a7") + .await; + + // Optimism Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:10", "0xa").await; + + // Optimism Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:420", "0x1A4").await; + + // Polygon Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:137", "0x89").await; + + // Polygon Mumbai + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:80001", "0x13881").await; + + // Base Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:8453", "0x2105").await; + + // Base Görli + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:84531", "0x14a33").await; + + // Boba Ethereum Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:288", "0x120").await; + + // Boba BNB Mainnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:56288", "0xdbe0").await; + + // Boba BNB Testnet + check_if_rpc_is_responding_correctly_for_supported_chain(ctx, "eip155:9728", "0x2600").await; +}