Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: dynamic chain support #73

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
12 changes: 12 additions & 0 deletions blockchain_api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
133 changes: 133 additions & 0 deletions blockchain_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[derive(Debug, Clone)]
pub struct BlockchainApiProvider {
project_id: ProjectId,
blockchain_api_rpc_endpoint: Url,
supported_chains: Arc<RwLock<HashSet<String>>>,
refresh_job: Arc<JoinHandle<Infallible>>,
}

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<HashSet<String>>,
) -> Result<(), Error> {
let response = reqwest::get(blockchain_api_supported_chains_endpoint)
.await?
.json::<SupportedChainsResponse>()
.await?;
*supported_chains.write().await = response.http;
Ok(())
}

impl BlockchainApiProvider {
pub async fn new(project_id: ProjectId, blockchain_api_endpoint: Url) -> Result<Self, Error> {
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}");
Copy link
Member Author

@chris13524 chris13524 Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we could get service-level metrics, alarms, etc. for this error, but would take more refactoring and not a big deal anyway because this type of temporary error is non-actionable and we test this code path on app startup.

}
}
}
});
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<Url> {
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"
);
}
}
5 changes: 4 additions & 1 deletion relay_rpc/src/auth/cacao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub enum CacaoError {
#[error("Invalid address")]
AddressInvalid,

#[error("EIP-1271 signatures not supported")]
Eip1271NotSupported,

#[error("Unsupported signature type")]
UnsupportedSignature,

Expand Down Expand Up @@ -94,7 +97,7 @@ pub struct Cacao {
impl Cacao {
const ETHEREUM: &'static str = "Ethereum";

pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result<bool, CacaoError> {
pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result<bool, CacaoError> {
self.p.validate()?;
self.h.validate()?;
self.s.verify(self, provider).await
Expand Down
59 changes: 0 additions & 59 deletions relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs

This file was deleted.

3 changes: 2 additions & 1 deletion relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use url::Url;

pub trait GetRpcUrl {
fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
#[allow(async_fn_in_trait)]
async fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
}
1 change: 0 additions & 1 deletion relay_rpc/src/auth/cacao/signature/eip1271/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use {
url::Url,
};

pub mod blockchain_api;
pub mod get_rpc_url;

pub const EIP1271: &str = "eip1271";
Expand Down
30 changes: 17 additions & 13 deletions relay_rpc/src/auth/cacao/signature/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl Signature {
pub async fn verify(
&self,
cacao: &Cacao,
get_provider: &impl GetRpcUrl,
provider: Option<&impl GetRpcUrl>,
) -> Result<bool, CacaoError> {
let address = cacao.p.address()?;

Expand All @@ -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"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it ok to panic here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the Keccak256 hash length is always 32 bytes

provider,
)
.await
} else {
Err(CacaoError::ProviderNotAvailable)
}
} else {
Err(CacaoError::ProviderNotAvailable)
Err(CacaoError::Eip1271NotSupported)
}
}
_ => Err(CacaoError::UnsupportedSignature),
Expand Down
8 changes: 4 additions & 4 deletions relay_rpc/src/auth/cacao/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Url> {
async fn get_rpc_url(&self, _: String) -> Option<Url> {
None
}
}
Expand Down Expand Up @@ -32,7 +32,7 @@ async fn cacao_verify_success() {
}
}"#;
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
let result = cacao.verify(&MockGetRpcUrl).await;
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
assert!(result.is_ok());
assert!(result.map_err(|_| false).unwrap());

Expand Down Expand Up @@ -69,7 +69,7 @@ async fn cacao_verify_success_identity_in_audience() {
}
}"#;
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
let result = cacao.verify(&MockGetRpcUrl).await;
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
assert!(result.is_ok());
assert!(result.map_err(|_| false).unwrap());

Expand Down Expand Up @@ -105,6 +105,6 @@ async fn cacao_verify_failure() {
}
}"#;
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap();
let result = cacao.verify(&MockGetRpcUrl).await;
let result = cacao.verify(Some(&MockGetRpcUrl)).await;
assert!(result.is_err());
}
Loading