Skip to content

Commit 593029f

Browse files
authored
fix: dynamic chain support (#73)
1 parent 243ca2d commit 593029f

File tree

9 files changed

+176
-86
lines changed

9 files changed

+176
-86
lines changed

Cargo.toml

+4-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ authors = ["WalletConnect Team"]
77
license = "Apache-2.0"
88

99
[workspace]
10-
members = [
11-
"relay_client",
12-
"relay_rpc"
13-
]
10+
members = ["blockchain_api", "relay_client", "relay_rpc"]
1411

1512
[features]
1613
default = ["full"]
@@ -35,12 +32,12 @@ once_cell = "1.19"
3532

3633
[[example]]
3734
name = "websocket_client"
38-
required-features = ["client","rpc"]
35+
required-features = ["client", "rpc"]
3936

4037
[[example]]
4138
name = "http_client"
42-
required-features = ["client","rpc"]
39+
required-features = ["client", "rpc"]
4340

4441
[[example]]
4542
name = "webhook"
46-
required-features = ["client","rpc"]
43+
required-features = ["client", "rpc"]

blockchain_api/Cargo.toml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "blockchain_api"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
relay_rpc = { path = "../relay_rpc" }
8+
reqwest = "0.11"
9+
serde = "1.0"
10+
tokio = { version = "1.0", features = ["test-util", "macros"] }
11+
tracing = "0.1.40"
12+
url = "2"

blockchain_api/src/lib.rs

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
pub use reqwest::Error;
2+
use {
3+
relay_rpc::{auth::cacao::signature::eip1271::get_rpc_url::GetRpcUrl, domain::ProjectId},
4+
serde::Deserialize,
5+
std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration},
6+
tokio::{sync::RwLock, task::JoinHandle},
7+
tracing::error,
8+
url::Url,
9+
};
10+
11+
const BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR: &str = "/v1/supported-chains";
12+
const BLOCKCHAIN_API_RPC_ENDPOINT_STR: &str = "/v1";
13+
const BLOCKCHAIN_API_RPC_CHAIN_ID_PARAM: &str = "chainId";
14+
const BLOCKCHAIN_API_RPC_PROJECT_ID_PARAM: &str = "projectId";
15+
16+
const SUPPORTED_CHAINS_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4);
17+
18+
#[derive(Debug, Deserialize)]
19+
struct SupportedChainsResponse {
20+
pub http: HashSet<String>,
21+
}
22+
23+
#[derive(Debug, Clone)]
24+
pub struct BlockchainApiProvider {
25+
project_id: ProjectId,
26+
blockchain_api_rpc_endpoint: Url,
27+
supported_chains: Arc<RwLock<HashSet<String>>>,
28+
refresh_job: Arc<JoinHandle<Infallible>>,
29+
}
30+
31+
impl Drop for BlockchainApiProvider {
32+
fn drop(&mut self) {
33+
self.refresh_job.abort();
34+
}
35+
}
36+
37+
async fn refresh_supported_chains(
38+
blockchain_api_supported_chains_endpoint: Url,
39+
supported_chains: &RwLock<HashSet<String>>,
40+
) -> Result<(), Error> {
41+
let response = reqwest::get(blockchain_api_supported_chains_endpoint)
42+
.await?
43+
.json::<SupportedChainsResponse>()
44+
.await?;
45+
*supported_chains.write().await = response.http;
46+
Ok(())
47+
}
48+
49+
impl BlockchainApiProvider {
50+
pub async fn new(project_id: ProjectId, blockchain_api_endpoint: Url) -> Result<Self, Error> {
51+
let blockchain_api_rpc_endpoint = blockchain_api_endpoint
52+
.join(BLOCKCHAIN_API_RPC_ENDPOINT_STR)
53+
.expect("Safe unwrap: hardcoded URL: BLOCKCHAIN_API_RPC_ENDPOINT_STR");
54+
let blockchain_api_supported_chains_endpoint = blockchain_api_endpoint
55+
.join(BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR)
56+
.expect("Safe unwrap: hardcoded URL: BLOCKCHAIN_API_SUPPORTED_CHAINS_ENDPOINT_STR");
57+
58+
let supported_chains = Arc::new(RwLock::new(HashSet::new()));
59+
refresh_supported_chains(
60+
blockchain_api_supported_chains_endpoint.clone(),
61+
&supported_chains,
62+
)
63+
.await?;
64+
let mut interval = tokio::time::interval(SUPPORTED_CHAINS_REFRESH_INTERVAL);
65+
interval.tick().await;
66+
let refresh_job = tokio::task::spawn({
67+
let supported_chains = supported_chains.clone();
68+
let blockchain_api_supported_chains_endpoint =
69+
blockchain_api_supported_chains_endpoint.clone();
70+
async move {
71+
loop {
72+
interval.tick().await;
73+
if let Err(e) = refresh_supported_chains(
74+
blockchain_api_supported_chains_endpoint.clone(),
75+
&supported_chains,
76+
)
77+
.await
78+
{
79+
error!("Failed to refresh supported chains: {e}");
80+
}
81+
}
82+
}
83+
});
84+
Ok(Self {
85+
project_id,
86+
blockchain_api_rpc_endpoint,
87+
supported_chains,
88+
refresh_job: Arc::new(refresh_job),
89+
})
90+
}
91+
}
92+
93+
fn build_rpc_url(blockchain_api_rpc_endpoint: Url, chain_id: &str, project_id: &str) -> Url {
94+
let mut url = blockchain_api_rpc_endpoint;
95+
url.query_pairs_mut()
96+
.append_pair(BLOCKCHAIN_API_RPC_CHAIN_ID_PARAM, chain_id)
97+
.append_pair(BLOCKCHAIN_API_RPC_PROJECT_ID_PARAM, project_id);
98+
url
99+
}
100+
101+
impl GetRpcUrl for BlockchainApiProvider {
102+
async fn get_rpc_url(&self, chain_id: String) -> Option<Url> {
103+
self.supported_chains
104+
.read()
105+
.await
106+
.contains(&chain_id)
107+
.then(|| {
108+
build_rpc_url(
109+
self.blockchain_api_rpc_endpoint.clone(),
110+
&chain_id,
111+
self.project_id.as_ref(),
112+
)
113+
})
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
121+
#[tokio::test]
122+
async fn rpc_endpoint() {
123+
assert_eq!(
124+
build_rpc_url(
125+
"https://rpc.walletconnect.com/v1".parse().unwrap(),
126+
"eip155:1",
127+
"my-project-id"
128+
)
129+
.as_str(),
130+
"https://rpc.walletconnect.com/v1?chainId=eip155%3A1&projectId=my-project-id"
131+
);
132+
}
133+
}

relay_rpc/src/auth/cacao.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub enum CacaoError {
3232
#[error("Invalid address")]
3333
AddressInvalid,
3434

35+
#[error("EIP-1271 signatures not supported")]
36+
Eip1271NotSupported,
37+
3538
#[error("Unsupported signature type")]
3639
UnsupportedSignature,
3740

@@ -94,7 +97,7 @@ pub struct Cacao {
9497
impl Cacao {
9598
const ETHEREUM: &'static str = "Ethereum";
9699

97-
pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result<bool, CacaoError> {
100+
pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result<bool, CacaoError> {
98101
self.p.validate()?;
99102
self.h.validate()?;
100103
self.s.verify(self, provider).await

relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs

-59
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use url::Url;
22

33
pub trait GetRpcUrl {
4-
fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
4+
#[allow(async_fn_in_trait)]
5+
async fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
56
}

relay_rpc/src/auth/cacao/signature/eip1271/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use {
88
url::Url,
99
};
1010

11-
pub mod blockchain_api;
1211
pub mod get_rpc_url;
1312

1413
pub const EIP1271: &str = "eip1271";

relay_rpc/src/auth/cacao/signature/mod.rs

+17-13
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl Signature {
2323
pub async fn verify(
2424
&self,
2525
cacao: &Cacao,
26-
get_provider: &impl GetRpcUrl,
26+
provider: Option<&impl GetRpcUrl>,
2727
) -> Result<bool, CacaoError> {
2828
let address = cacao.p.address()?;
2929

@@ -36,20 +36,24 @@ impl Signature {
3636
match self.t.as_str() {
3737
EIP191 => verify_eip191(&signature, &address, hash),
3838
EIP1271 => {
39-
let chain_id = cacao.p.chain_id_reference()?;
40-
let provider = get_provider.get_rpc_url(chain_id);
4139
if let Some(provider) = provider {
42-
verify_eip1271(
43-
signature,
44-
Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?,
45-
&hash.finalize()[..]
46-
.try_into()
47-
.expect("hash length is 32 bytes"),
48-
provider,
49-
)
50-
.await
40+
let chain_id = cacao.p.chain_id_reference()?;
41+
let provider = provider.get_rpc_url(chain_id).await;
42+
if let Some(provider) = provider {
43+
verify_eip1271(
44+
signature,
45+
Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?,
46+
&hash.finalize()[..]
47+
.try_into()
48+
.expect("hash length is 32 bytes"),
49+
provider,
50+
)
51+
.await
52+
} else {
53+
Err(CacaoError::ProviderNotAvailable)
54+
}
5155
} else {
52-
Err(CacaoError::ProviderNotAvailable)
56+
Err(CacaoError::Eip1271NotSupported)
5357
}
5458
}
5559
_ => Err(CacaoError::UnsupportedSignature),

relay_rpc/src/auth/cacao/tests.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Caca
33
struct MockGetRpcUrl;
44

55
impl GetRpcUrl for MockGetRpcUrl {
6-
fn get_rpc_url(&self, _: String) -> Option<Url> {
6+
async fn get_rpc_url(&self, _: String) -> Option<Url> {
77
None
88
}
99
}
@@ -32,7 +32,7 @@ async fn cacao_verify_success() {
3232
}
3333
}"#;
3434
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
35-
let result = cacao.verify(&MockGetRpcUrl).await;
35+
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
3636
assert!(result.is_ok());
3737
assert!(result.map_err(|_| false).unwrap());
3838

@@ -69,7 +69,7 @@ async fn cacao_verify_success_identity_in_audience() {
6969
}
7070
}"#;
7171
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
72-
let result = cacao.verify(&MockGetRpcUrl).await;
72+
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
7373
assert!(result.is_ok());
7474
assert!(result.map_err(|_| false).unwrap());
7575

@@ -105,6 +105,6 @@ async fn cacao_verify_failure() {
105105
}
106106
}"#;
107107
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
108-
let result = cacao.verify(&MockGetRpcUrl).await;
108+
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
109109
assert!(result.is_err());
110110
}

0 commit comments

Comments
 (0)