From 7119c3044668f2d03df565934aa3f309bb65e09f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 8 Mar 2024 15:37:20 -0800 Subject: [PATCH] fix: dynamic chain support --- Cargo.toml | 11 +- blockchain_api/Cargo.toml | 12 ++ blockchain_api/src/lib.rs | 133 ++++++++++++++++++ relay_rpc/src/auth/cacao.rs | 5 +- .../cacao/signature/eip1271/blockchain_api.rs | 59 -------- .../cacao/signature/eip1271/get_rpc_url.rs | 3 +- .../src/auth/cacao/signature/eip1271/mod.rs | 1 - relay_rpc/src/auth/cacao/signature/mod.rs | 30 ++-- relay_rpc/src/auth/cacao/tests.rs | 2 +- 9 files changed, 173 insertions(+), 83 deletions(-) create mode 100644 blockchain_api/Cargo.toml create mode 100644 blockchain_api/src/lib.rs delete mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs diff --git a/Cargo.toml b/Cargo.toml index bcf95ef..64927a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,7 @@ authors = ["WalletConnect Team"] license = "Apache-2.0" [workspace] -members = [ - "relay_client", - "relay_rpc" -] +members = ["blockchain_api", "relay_client", "relay_rpc"] [features] default = ["full"] @@ -35,12 +32,12 @@ once_cell = "1.19" [[example]] name = "websocket_client" -required-features = ["client","rpc"] +required-features = ["client", "rpc"] [[example]] name = "http_client" -required-features = ["client","rpc"] +required-features = ["client", "rpc"] [[example]] name = "webhook" -required-features = ["client","rpc"] +required-features = ["client", "rpc"] diff --git a/blockchain_api/Cargo.toml b/blockchain_api/Cargo.toml new file mode 100644 index 0000000..2dafec8 --- /dev/null +++ b/blockchain_api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "blockchain_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +relay_rpc = { path = "../relay_rpc" } +reqwest = "0.11" +serde = "1.0" +tokio = { version = "1.0", features = ["test-util", "macros"] } +tracing = "0.1.40" +url = "2" diff --git a/blockchain_api/src/lib.rs b/blockchain_api/src/lib.rs new file mode 100644 index 0000000..96a40ed --- /dev/null +++ b/blockchain_api/src/lib.rs @@ -0,0 +1,133 @@ +pub use reqwest::Error; +use { + relay_rpc::{auth::cacao::signature::eip1271::get_rpc_url::GetRpcUrl, domain::ProjectId}, + serde::Deserialize, + std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration}, + tokio::{sync::RwLock, task::JoinHandle}, + tracing::error, + url::Url, +}; + +const BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR: &str = "/v1/supported-chains"; +const BLOCKCHAIN_API_RPC_ENDPOINT_STR: &str = "/v1"; +const BLOCKCHAIN_API_RPC_CHAIN_ID_PARAM: &str = "chainId"; +const BLOCKCHAIN_API_RPC_PROJECT_ID_PARAM: &str = "projectId"; + +const SUPPORTED_CHAINS_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4); + +#[derive(Debug, Deserialize)] +struct SupportedChainsResponse { + pub http: HashSet, +} + +#[derive(Debug, Clone)] +pub struct BlockchainApiProvider { + project_id: ProjectId, + blockchain_api_rpc_endpoint: Url, + supported_chains: Arc>>, + refresh_job: Arc>, +} + +impl Drop for BlockchainApiProvider { + fn drop(&mut self) { + self.refresh_job.abort(); + } +} + +async fn refresh_supported_chains( + blockchain_api_supported_chains_endpoint: Url, + supported_chains: &RwLock>, +) -> Result<(), Error> { + let response = reqwest::get(blockchain_api_supported_chains_endpoint) + .await? + .json::() + .await?; + *supported_chains.write().await = response.http; + Ok(()) +} + +impl BlockchainApiProvider { + pub async fn new(project_id: ProjectId, blockchain_api_endpoint: Url) -> Result { + let blockchain_api_rpc_endpoint = blockchain_api_endpoint + .join(BLOCKCHAIN_API_RPC_ENDPOINT_STR) + .expect("Safe unwrap: hardcoded URL: BLOCKCHAIN_API_RPC_ENDPOINT_STR"); + let blockchain_api_supported_chains_endpoint = blockchain_api_endpoint + .join(BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR) + .expect("Safe unwrap: hardcoded URL: BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR"); + + let supported_chains = Arc::new(RwLock::new(HashSet::new())); + refresh_supported_chains( + blockchain_api_supported_chains_endpoint.clone(), + &supported_chains, + ) + .await?; + let mut interval = tokio::time::interval(SUPPORTED_CHAINS_REFRESH_INTERVAL); + interval.tick().await; + let refresh_job = tokio::task::spawn({ + let supported_chains = supported_chains.clone(); + let blockchain_api_supported_chains_endpoint = + blockchain_api_supported_chains_endpoint.clone(); + async move { + loop { + interval.tick().await; + if let Err(e) = refresh_supported_chains( + blockchain_api_supported_chains_endpoint.clone(), + &supported_chains, + ) + .await + { + error!("Failed to refresh supported chains: {e}"); + } + } + } + }); + Ok(Self { + project_id, + blockchain_api_rpc_endpoint, + supported_chains, + refresh_job: Arc::new(refresh_job), + }) + } +} + +fn build_rpc_url(blockchain_api_rpc_endpoint: Url, chain_id: &str, project_id: &str) -> Url { + let mut url = blockchain_api_rpc_endpoint; + url.query_pairs_mut() + .append_pair(BLOCKCHAIN_API_RPC_CHAIN_ID_PARAM, chain_id) + .append_pair(BLOCKCHAIN_API_RPC_PROJECT_ID_PARAM, project_id); + url +} + +impl GetRpcUrl for BlockchainApiProvider { + async fn get_rpc_url(&self, chain_id: String) -> Option { + self.supported_chains + .read() + .await + .contains(&chain_id) + .then(|| { + build_rpc_url( + self.blockchain_api_rpc_endpoint.clone(), + &chain_id, + self.project_id.as_ref(), + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn rpc_endpoint() { + assert_eq!( + build_rpc_url( + "https://rpc.walletconnect.com/v1".parse().unwrap(), + "eip155:1", + "my-project-id" + ) + .as_str(), + "https://rpc.walletconnect.com/v1?chainId=eip155%3A1&projectId=my-project-id" + ); + } +} diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 18689e6..babe697 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -32,6 +32,9 @@ pub enum CacaoError { #[error("Invalid address")] AddressInvalid, + #[error("EIP-1271 signatures not supported")] + Eip1271NotSupported, + #[error("Unsupported signature type")] UnsupportedSignature, @@ -94,7 +97,7 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result { + pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result { self.p.validate()?; self.h.validate()?; self.s.verify(self, provider).await diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs deleted file mode 100644 index ac8dcc7..0000000 --- a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs +++ /dev/null @@ -1,59 +0,0 @@ -use {super::get_rpc_url::GetRpcUrl, crate::domain::ProjectId, url::Url}; - -// https://github.com/WalletConnect/blockchain-api/blob/master/SUPPORTED_CHAINS.md -const SUPPORTED_CHAINS: [&str; 26] = [ - "eip155:1", - "eip155:5", - "eip155:11155111", - "eip155:10", - "eip155:420", - "eip155:42161", - "eip155:421613", - "eip155:137", - "eip155:80001", - "eip155:1101", - "eip155:42220", - "eip155:1313161554", - "eip155:1313161555", - "eip155:56", - "eip155:56", - "eip155:43114", - "eip155:43113", - "eip155:324", - "eip155:280", - "near", - "eip155:100", - "solana:4sgjmw1sunhzsxgspuhpqldx6wiyjntz", - "eip155:8453", - "eip155:84531", - "eip155:7777777", - "eip155:999", -]; - -#[derive(Debug, Clone)] -pub struct BlockchainApiProvider { - project_id: ProjectId, -} - -impl BlockchainApiProvider { - pub fn new(project_id: ProjectId) -> Self { - Self { project_id } - } -} - -impl GetRpcUrl for BlockchainApiProvider { - fn get_rpc_url(&self, chain_id: String) -> Option { - if SUPPORTED_CHAINS.contains(&chain_id.as_str()) { - Some( - format!( - "https://rpc.walletconnect.com/v1?chainId={chain_id}&projectId={}", - self.project_id - ) - .parse() - .expect("Provider URL should be valid"), - ) - } else { - None - } - } -} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs index 9639c17..fb712b9 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs @@ -1,5 +1,6 @@ use url::Url; pub trait GetRpcUrl { - fn get_rpc_url(&self, chain_id: String) -> Option; + #[allow(async_fn_in_trait)] + async fn get_rpc_url(&self, chain_id: String) -> Option; } diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs index 5c6184b..50125ba 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs @@ -8,7 +8,6 @@ use { url::Url, }; -pub mod blockchain_api; pub mod get_rpc_url; pub const EIP1271: &str = "eip1271"; diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index bd2c4b2..89ff7a7 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -23,7 +23,7 @@ impl Signature { pub async fn verify( &self, cacao: &Cacao, - get_provider: &impl GetRpcUrl, + provider: Option<&impl GetRpcUrl>, ) -> Result { let address = cacao.p.address()?; @@ -36,20 +36,24 @@ impl Signature { match self.t.as_str() { EIP191 => verify_eip191(&signature, &address, hash), EIP1271 => { - let chain_id = cacao.p.chain_id_reference()?; - let provider = get_provider.get_rpc_url(chain_id); if let Some(provider) = provider { - verify_eip1271( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider, - ) - .await + let chain_id = cacao.p.chain_id_reference()?; + let provider = provider.get_rpc_url(chain_id).await; + if let Some(provider) = provider { + verify_eip1271( + signature, + Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, + &hash.finalize()[..] + .try_into() + .expect("hash length is 32 bytes"), + provider, + ) + .await + } else { + Err(CacaoError::ProviderNotAvailable) + } } else { - Err(CacaoError::ProviderNotAvailable) + Err(CacaoError::Eip1271NotSupported) } } _ => Err(CacaoError::UnsupportedSignature), diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 74c01d4..8280e7f 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -3,7 +3,7 @@ use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Caca struct MockGetRpcUrl; impl GetRpcUrl for MockGetRpcUrl { - fn get_rpc_url(&self, _: String) -> Option { + async fn get_rpc_url(&self, _: String) -> Option { None } }