From 2a4d2ab721941655ceafb0e9711c1c0a438e396d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 15 Jan 2024 11:20:28 -0500 Subject: [PATCH 01/10] feat: EIP-1271 support --- justfile | 15 +++ relay_rpc/Cargo.toml | 13 ++- relay_rpc/src/auth/cacao.rs | 12 ++- relay_rpc/src/auth/cacao/signature.rs | 131 ++++++++++++++++++++------ relay_rpc/src/auth/cacao/tests.rs | 18 ++-- 5 files changed, 148 insertions(+), 41 deletions(-) diff --git a/justfile b/justfile index 78c20d3..4cb6e46 100644 --- a/justfile +++ b/justfile @@ -72,6 +72,21 @@ fmt: echo ' ^^^^^^ To install `rustup component add rustfmt`, see https://github.com/rust-lang/rustfmt for details' fi +fmt-imports: + #!/bin/bash + set -euo pipefail + + if command -v cargo-fmt >/dev/null; then + echo '==> Running rustfmt' + cargo +nightly fmt -- --config group_imports=StdExternalCrate,imports_granularity=One + else + echo '==> rustfmt not found in PATH, skipping' + fi + +unit: lint test test-all + +devloop: unit fmt-imports + # Run commit checker commit-check: #!/bin/bash diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index cf7ba3d..7a3c757 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -22,4 +22,15 @@ once_cell = "1.16" jsonwebtoken = "8.1" k256 = { version = "0.13", optional = true } sha3 = { version = "0.10", optional = true } -sha2 = { version = "0.10.6" } \ No newline at end of file +sha2 = { version = "0.10.6" } +alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } +reqwest = { version = "0.11.18", features = ["default-tls"] } +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } +alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } +alloy-json-abi = "0.6" +alloy-sol-types = "0.6" +alloy-primitives = "0.6" +url = "2.5.0" +tokio = { version = "1.35.1", features = ["test-util", "macros"] } diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 246243b..e7c0378 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -2,7 +2,9 @@ use { self::{header::Header, payload::Payload, signature::Signature}, core::fmt::Debug, serde::{Deserialize, Serialize}, + serde_json::value::RawValue, std::fmt::{Display, Write}, + url::Url, }; pub mod header; @@ -21,11 +23,17 @@ pub enum CacaoError { #[error("Invalid payload resources")] PayloadResources, + #[error("Invalid address")] + AddressInvalid, + #[error("Unsupported signature type")] UnsupportedSignature, #[error("Unable to verify")] Verification, + + #[error("Internal EIP-1271 resolution error: {0}")] + Eip1271Internal(alloy_json_rpc::RpcError>), } impl From for CacaoError { @@ -77,10 +85,10 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub fn verify(&self) -> Result { + pub async fn verify(&self, provider: Option) -> Result { self.p.validate()?; self.h.validate()?; - self.s.verify(self) + self.s.verify(self, provider).await } pub fn siwe_message(&self) -> Result { diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index 90aad4f..101aa38 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -1,9 +1,17 @@ use { super::{Cacao, CacaoError}, + alloy_primitives::Address, + alloy_providers::provider::TempProvider, + alloy_rpc_types::{CallInput, CallRequest}, + alloy_sol_types::{sol, SolCall}, serde::{Deserialize, Serialize}, + sha3::{Digest, Keccak256}, + std::str::FromStr, + url::Url, }; pub const EIP191: &str = "eip191"; +pub const EIP1271: &str = "eip1271"; #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct Signature { @@ -12,48 +20,56 @@ pub struct Signature { } impl Signature { - pub fn verify(&self, cacao: &Cacao) -> Result { + pub async fn verify(&self, cacao: &Cacao, provider: Option) -> Result { + let address = cacao.p.address()?; + + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) + .map_err(|_| CacaoError::Verification)?; + + let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); + match self.t.as_str() { - EIP191 => Eip191.verify(&cacao.s.s, &cacao.p.address()?, &cacao.siwe_message()?), - // "eip1271" => Eip1271.verify(), TODO: How to accces our RPC? + EIP191 => Eip191.verify(&signature, &address, hash), + EIP1271 if provider.is_some() => { + Eip1271 + .verify( + signature, + Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, + &hash.finalize()[..] + .try_into() + .expect("hash length is 32 bytes"), + provider.expect("provider is some"), + ) + .await + } _ => Err(CacaoError::UnsupportedSignature), } } } +pub fn eip191_bytes(message: &str) -> Vec { + format!( + "\u{0019}Ethereum Signed Message:\n{}{}", + message.as_bytes().len(), + message + ) + .into() +} + pub struct Eip191; impl Eip191 { - pub fn eip191_bytes(&self, message: &str) -> Vec { - format!( - "\u{0019}Ethereum Signed Message:\n{}{}", - message.as_bytes().len(), - message - ) - .into() - } + fn verify(&self, signature: &[u8], address: &str, hash: Keccak256) -> Result { + use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; - fn verify(&self, signature: &str, address: &str, message: &str) -> Result { - use { - k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}, - sha3::{Digest, Keccak256}, - }; - - let signature_bytes = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification)?; + let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; + let recovery_id = + RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; - let sig = Sig::try_from(&signature_bytes[..64]).map_err(|_| CacaoError::Verification)?; - let recovery_id = RecoveryId::try_from(&signature_bytes[64] % 27) + let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) .map_err(|_| CacaoError::Verification)?; - let recovered_key = VerifyingKey::recover_from_digest( - Keccak256::new_with_prefix(self.eip191_bytes(message)), - &sig, - recovery_id, - ) - .map_err(|_| CacaoError::Verification)?; - let add = &Keccak256::default() .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) .finalize()[12..]; @@ -72,3 +88,60 @@ impl Eip191 { fn strip_hex_prefix(s: &str) -> &str { s.strip_prefix("0x").unwrap_or(s) } + +pub struct Eip1271; + +// https://eips.ethereum.org/EIPS/eip-1271 +const MAGIC_VALUE: u32 = 0x1626ba7e; +sol! { + function isValidSignature( + bytes32 _hash, + bytes memory _signature) + public + view + returns (bytes4 magicValue); +} + +impl Eip1271 { + async fn verify( + &self, + signature: Vec, + address: Address, + hash: &[u8; 32], + provider: Url, + ) -> Result { + let provider = + alloy_providers::provider::Provider::new(alloy_transport_http::Http::new(provider)); + + let call_request = CallRequest { + to: Some(address), + input: CallInput::new( + isValidSignatureCall { + _hash: alloy_primitives::FixedBytes::from(hash), + _signature: signature, + } + .abi_encode() + .into(), + ), + ..Default::default() + }; + + let result = provider.call(call_request, None).await.map_err(|e| { + if let Some(error_response) = e.as_error_resp() { + if error_response.message.starts_with("execution reverted:") { + CacaoError::Verification + } else { + CacaoError::Eip1271Internal(e) + } + } else { + CacaoError::Eip1271Internal(e) + } + })?; + + if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { + Ok(true) + } else { + Err(CacaoError::Verification) + } + } +} diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 2c8c8b8..8718483 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,8 +1,8 @@ use crate::auth::cacao::Cacao; /// Test that we can verify a deprecated Cacao. -#[test] -fn cacao_verify_success() { +#[tokio::test] +async fn cacao_verify_success() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -24,7 +24,7 @@ fn cacao_verify_success() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(None).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -37,8 +37,8 @@ fn cacao_verify_success() { } /// Test that we can verify a updated Cacao. -#[test] -fn cacao_verify_success_identity_in_audience() { +#[tokio::test] +async fn cacao_verify_success_identity_in_audience() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -61,7 +61,7 @@ fn cacao_verify_success_identity_in_audience() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(None).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -74,8 +74,8 @@ fn cacao_verify_success_identity_in_audience() { } /// Test that we can verify a Cacao -#[test] -fn cacao_verify_failure() { +#[tokio::test] +async fn cacao_verify_failure() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -97,6 +97,6 @@ fn cacao_verify_failure() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(None).await; assert!(result.is_err()); } From 1947232e3ae9460b34f70e49e13b41306d6a7eba Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 15 Jan 2024 11:25:20 -0500 Subject: [PATCH 02/10] chore: refactor --- relay_rpc/src/auth/cacao/signature.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index 101aa38..4429917 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -1,9 +1,10 @@ use { super::{Cacao, CacaoError}, - alloy_primitives::Address, - alloy_providers::provider::TempProvider, + alloy_primitives::{Address, FixedBytes}, + alloy_providers::provider::{Provider, TempProvider}, alloy_rpc_types::{CallInput, CallRequest}, alloy_sol_types::{sol, SolCall}, + alloy_transport_http::Http, serde::{Deserialize, Serialize}, sha3::{Digest, Keccak256}, std::str::FromStr, @@ -110,14 +111,13 @@ impl Eip1271 { hash: &[u8; 32], provider: Url, ) -> Result { - let provider = - alloy_providers::provider::Provider::new(alloy_transport_http::Http::new(provider)); + let provider = Provider::new(Http::new(provider)); let call_request = CallRequest { to: Some(address), input: CallInput::new( isValidSignatureCall { - _hash: alloy_primitives::FixedBytes::from(hash), + _hash: FixedBytes::from(hash), _signature: signature, } .abi_encode() From 67ce3c741f7f417f6d89af8deaa7d510e8c66708 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 15 Jan 2024 11:27:27 -0500 Subject: [PATCH 03/10] chore: unconstraint versions --- relay_rpc/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 7a3c757..dedfdb1 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -26,11 +26,13 @@ sha2 = { version = "0.10.6" } alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } -reqwest = { version = "0.11.18", features = ["default-tls"] } +reqwest = { version = "0.11", features = ["default-tls"] } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } alloy-json-abi = "0.6" alloy-sol-types = "0.6" alloy-primitives = "0.6" url = "2.5.0" + +[dev-dependencies] tokio = { version = "1.35.1", features = ["test-util", "macros"] } From f4b36d6d3b0d83af9dfef3f9f536b41c57e369b6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 15 Jan 2024 11:28:24 -0500 Subject: [PATCH 04/10] chore: unconstrain version --- relay_rpc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index dedfdb1..1cfe104 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -32,7 +32,7 @@ alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8 alloy-json-abi = "0.6" alloy-sol-types = "0.6" alloy-primitives = "0.6" -url = "2.5.0" +url = "2" [dev-dependencies] tokio = { version = "1.35.1", features = ["test-util", "macros"] } From 90ac16b2059453e329ab3de2a4e953556460a8ba Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 30 Jan 2024 17:50:00 -0500 Subject: [PATCH 05/10] fix: dynamic provider and test --- relay_rpc/Cargo.toml | 16 +++---- relay_rpc/src/auth/cacao.rs | 12 +++-- relay_rpc/src/auth/cacao/signature.rs | 64 ++++++++++++++++++++++----- relay_rpc/src/auth/cacao/tests.rs | 6 +-- 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 1cfe104..d0d3eb9 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -23,16 +23,16 @@ jsonwebtoken = "8.1" k256 = { version = "0.13", optional = true } sha3 = { version = "0.10", optional = true } sha2 = { version = "0.10.6" } -alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } -alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } reqwest = { version = "0.11", features = ["default-tls"] } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } -alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "5ed60f8" } -alloy-json-abi = "0.6" -alloy-sol-types = "0.6" -alloy-primitives = "0.6" url = "2" +alloy-providers = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-json-abi = "0.6.2" +alloy-sol-types = "0.6.2" +alloy-primitives = "0.6.2" [dev-dependencies] tokio = { version = "1.35.1", features = ["test-util", "macros"] } diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index e7c0378..1fa6887 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -1,10 +1,13 @@ use { - self::{header::Header, payload::Payload, signature::Signature}, + self::{ + header::Header, + payload::Payload, + signature::{GetProvider, Signature}, + }, core::fmt::Debug, serde::{Deserialize, Serialize}, serde_json::value::RawValue, std::fmt::{Display, Write}, - url::Url, }; pub mod header; @@ -29,6 +32,9 @@ pub enum CacaoError { #[error("Unsupported signature type")] UnsupportedSignature, + #[error("Provider not available for that chain")] + ProviderNotAvailable, + #[error("Unable to verify")] Verification, @@ -85,7 +91,7 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub async fn verify(&self, provider: Option) -> Result { + pub async fn verify(&self, provider: GetProvider) -> Result { self.p.validate()?; self.h.validate()?; self.s.verify(self, provider).await diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index 4429917..1474fd0 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -20,8 +20,14 @@ pub struct Signature { pub s: String, } +pub type GetProvider = fn(chain_id: String) -> Option; + impl Signature { - pub async fn verify(&self, cacao: &Cacao, provider: Option) -> Result { + pub async fn verify( + &self, + cacao: &Cacao, + get_provider: GetProvider, + ) -> Result { let address = cacao.p.address()?; let signature = data_encoding::HEXLOWER_PERMISSIVE @@ -32,17 +38,23 @@ impl Signature { match self.t.as_str() { EIP191 => Eip191.verify(&signature, &address, hash), - EIP1271 if provider.is_some() => { - Eip1271 - .verify( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider.expect("provider is some"), - ) - .await + EIP1271 => { + let chain_id = cacao.p.chain_id_reference()?; + let provider = get_provider(chain_id); + if let Some(provider) = provider { + Eip1271 + .verify( + 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) + } } _ => Err(CacaoError::UnsupportedSignature), } @@ -145,3 +157,31 @@ impl Eip1271 { } } } + +#[cfg(test)] +mod test { + use {super::*, alloy_primitives::address}; + + // Manual test. Paste address, signature, message, and project ID to verify function + #[tokio::test] + #[ignore] + async fn test_eip1271() { + let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let signature = "xxx"; + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(signature).as_bytes()) + .map_err(|_| CacaoError::Verification) + .unwrap(); + let message = "xxx"; + let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] + .try_into() + .unwrap(); + let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" + .parse() + .unwrap(); + assert!(Eip1271 + .verify(signature, address, hash, provider) + .await + .unwrap()); + } +} diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 8718483..29816fd 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -24,7 +24,7 @@ async fn cacao_verify_success() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(None).await; + let result = cacao.verify(|_| None).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -61,7 +61,7 @@ async fn cacao_verify_success_identity_in_audience() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(None).await; + let result = cacao.verify(|_| None).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -97,6 +97,6 @@ async fn cacao_verify_failure() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(None).await; + let result = cacao.verify(|_| None).await; assert!(result.is_err()); } From 62b9f9f6c867001d17dca1da67187b86d5a92750 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 30 Jan 2024 17:58:51 -0500 Subject: [PATCH 06/10] fix: lock to current version --- relay_rpc/Cargo.toml | 10 +++++----- relay_rpc/src/auth/cacao/signature.rs | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index d0d3eb9..ce393a4 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -25,11 +25,11 @@ sha3 = { version = "0.10", optional = true } sha2 = { version = "0.10.6" } reqwest = { version = "0.11", features = ["default-tls"] } url = "2" -alloy-providers = { git = "https://github.com/alloy-rs/alloy.git" } -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git" } -alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git" } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git" } -alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git" } +alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } alloy-json-abi = "0.6.2" alloy-sol-types = "0.6.2" alloy-primitives = "0.6.2" diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index 1474fd0..344d6c8 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -162,7 +162,8 @@ impl Eip1271 { mod test { use {super::*, alloy_primitives::address}; - // Manual test. Paste address, signature, message, and project ID to verify function + // Manual test. Paste address, signature, message, and project ID to verify + // function #[tokio::test] #[ignore] async fn test_eip1271() { From ed663fcaccba424bcd7224d9648e8931d5f4f861 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 30 Jan 2024 18:11:11 -0500 Subject: [PATCH 07/10] fix: allow closures --- relay_rpc/src/auth/cacao/signature.rs | 2 +- relay_rpc/src/auth/cacao/tests.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index 344d6c8..b029dc7 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -20,7 +20,7 @@ pub struct Signature { pub s: String, } -pub type GetProvider = fn(chain_id: String) -> Option; +pub type GetProvider = Box Option>; impl Signature { pub async fn verify( diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 29816fd..092202d 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -24,7 +24,7 @@ async fn cacao_verify_success() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(|_| None).await; + let result = cacao.verify(Box::new(|_| None)).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -61,7 +61,7 @@ async fn cacao_verify_success_identity_in_audience() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(|_| None).await; + let result = cacao.verify(Box::new(|_| None)).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -97,6 +97,6 @@ async fn cacao_verify_failure() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(|_| None).await; + let result = cacao.verify(Box::new(|_| None)).await; assert!(result.is_err()); } From 6be145b1844d87d0c7e0fcc73bcf1514f0c8f55c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 31 Jan 2024 16:21:10 -0500 Subject: [PATCH 08/10] fix: trait objects work better than boxed function traits --- relay_rpc/src/auth/cacao.rs | 4 ++-- relay_rpc/src/auth/cacao/signature.rs | 8 +++++--- relay_rpc/src/auth/cacao/tests.rs | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 1fa6887..a8bb5b7 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -2,7 +2,7 @@ use { self::{ header::Header, payload::Payload, - signature::{GetProvider, Signature}, + signature::{GetRpcUrl, Signature}, }, core::fmt::Debug, serde::{Deserialize, Serialize}, @@ -91,7 +91,7 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub async fn verify(&self, provider: GetProvider) -> Result { + pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result { self.p.validate()?; self.h.validate()?; self.s.verify(self, provider).await diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs index b029dc7..892aaca 100644 --- a/relay_rpc/src/auth/cacao/signature.rs +++ b/relay_rpc/src/auth/cacao/signature.rs @@ -20,13 +20,15 @@ pub struct Signature { pub s: String, } -pub type GetProvider = Box Option>; +pub trait GetRpcUrl { + fn get_rpc_url(&self, chain_id: String) -> Option; +} impl Signature { pub async fn verify( &self, cacao: &Cacao, - get_provider: GetProvider, + get_provider: &impl GetRpcUrl, ) -> Result { let address = cacao.p.address()?; @@ -40,7 +42,7 @@ impl Signature { EIP191 => Eip191.verify(&signature, &address, hash), EIP1271 => { let chain_id = cacao.p.chain_id_reference()?; - let provider = get_provider(chain_id); + let provider = get_provider.get_rpc_url(chain_id); if let Some(provider) = provider { Eip1271 .verify( diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 092202d..644deee 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,4 +1,12 @@ -use crate::auth::cacao::Cacao; +use {super::signature::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; + +struct MockGetRpcUrl; + +impl GetRpcUrl for MockGetRpcUrl { + fn get_rpc_url(&self, _: String) -> Option { + None + } +} /// Test that we can verify a deprecated Cacao. #[tokio::test] @@ -24,7 +32,7 @@ async fn cacao_verify_success() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(Box::new(|_| None)).await; + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -61,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(Box::new(|_| None)).await; + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -97,6 +105,6 @@ async fn cacao_verify_failure() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(Box::new(|_| None)).await; + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_err()); } From 685200faaf0e1cb90b457555bc4440898dbd895f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 31 Jan 2024 16:52:48 -0500 Subject: [PATCH 09/10] feat: add blockchain API provider --- relay_rpc/src/auth/cacao.rs | 2 +- relay_rpc/src/auth/cacao/signature.rs | 190 ------------------ .../cacao/signature/eip1271/blockchain_api.rs | 58 ++++++ .../cacao/signature/eip1271/get_rpc_url.rs | 5 + .../src/auth/cacao/signature/eip1271/mod.rs | 98 +++++++++ relay_rpc/src/auth/cacao/signature/eip191.rs | 39 ++++ relay_rpc/src/auth/cacao/signature/mod.rs | 63 ++++++ relay_rpc/src/auth/cacao/tests.rs | 2 +- 8 files changed, 265 insertions(+), 192 deletions(-) delete mode 100644 relay_rpc/src/auth/cacao/signature.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/mod.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip191.rs create mode 100644 relay_rpc/src/auth/cacao/signature/mod.rs diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index a8bb5b7..52bd759 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -2,7 +2,7 @@ use { self::{ header::Header, payload::Payload, - signature::{GetRpcUrl, Signature}, + signature::{eip1271::get_rpc_url::GetRpcUrl, Signature}, }, core::fmt::Debug, serde::{Deserialize, Serialize}, diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs deleted file mode 100644 index 892aaca..0000000 --- a/relay_rpc/src/auth/cacao/signature.rs +++ /dev/null @@ -1,190 +0,0 @@ -use { - super::{Cacao, CacaoError}, - alloy_primitives::{Address, FixedBytes}, - alloy_providers::provider::{Provider, TempProvider}, - alloy_rpc_types::{CallInput, CallRequest}, - alloy_sol_types::{sol, SolCall}, - alloy_transport_http::Http, - serde::{Deserialize, Serialize}, - sha3::{Digest, Keccak256}, - std::str::FromStr, - url::Url, -}; - -pub const EIP191: &str = "eip191"; -pub const EIP1271: &str = "eip1271"; - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] -pub struct Signature { - pub t: String, - pub s: String, -} - -pub trait GetRpcUrl { - fn get_rpc_url(&self, chain_id: String) -> Option; -} - -impl Signature { - pub async fn verify( - &self, - cacao: &Cacao, - get_provider: &impl GetRpcUrl, - ) -> Result { - let address = cacao.p.address()?; - - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) - .map_err(|_| CacaoError::Verification)?; - - let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); - - match self.t.as_str() { - EIP191 => Eip191.verify(&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 { - Eip1271 - .verify( - 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) - } - } - _ => Err(CacaoError::UnsupportedSignature), - } - } -} - -pub fn eip191_bytes(message: &str) -> Vec { - format!( - "\u{0019}Ethereum Signed Message:\n{}{}", - message.as_bytes().len(), - message - ) - .into() -} - -pub struct Eip191; - -impl Eip191 { - fn verify(&self, signature: &[u8], address: &str, hash: Keccak256) -> Result { - use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; - - let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; - let recovery_id = - RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; - - let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) - .map_err(|_| CacaoError::Verification)?; - - let add = &Keccak256::default() - .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) - .finalize()[12..]; - - let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); - - if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { - Err(CacaoError::Verification) - } else { - Ok(true) - } - } -} - -/// Remove the "0x" prefix from a hex string. -fn strip_hex_prefix(s: &str) -> &str { - s.strip_prefix("0x").unwrap_or(s) -} - -pub struct Eip1271; - -// https://eips.ethereum.org/EIPS/eip-1271 -const MAGIC_VALUE: u32 = 0x1626ba7e; -sol! { - function isValidSignature( - bytes32 _hash, - bytes memory _signature) - public - view - returns (bytes4 magicValue); -} - -impl Eip1271 { - async fn verify( - &self, - signature: Vec, - address: Address, - hash: &[u8; 32], - provider: Url, - ) -> Result { - let provider = Provider::new(Http::new(provider)); - - let call_request = CallRequest { - to: Some(address), - input: CallInput::new( - isValidSignatureCall { - _hash: FixedBytes::from(hash), - _signature: signature, - } - .abi_encode() - .into(), - ), - ..Default::default() - }; - - let result = provider.call(call_request, None).await.map_err(|e| { - if let Some(error_response) = e.as_error_resp() { - if error_response.message.starts_with("execution reverted:") { - CacaoError::Verification - } else { - CacaoError::Eip1271Internal(e) - } - } else { - CacaoError::Eip1271Internal(e) - } - })?; - - if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(true) - } else { - Err(CacaoError::Verification) - } - } -} - -#[cfg(test)] -mod test { - use {super::*, alloy_primitives::address}; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip1271() { - let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - let signature = "xxx"; - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification) - .unwrap(); - let message = "xxx"; - let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] - .try_into() - .unwrap(); - let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" - .parse() - .unwrap(); - assert!(Eip1271 - .verify(signature, address, hash, provider) - .await - .unwrap()); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs new file mode 100644 index 0000000..233759c --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs @@ -0,0 +1,58 @@ +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", +]; + +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 new file mode 100644 index 0000000..9639c17 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs @@ -0,0 +1,5 @@ +use url::Url; + +pub trait GetRpcUrl { + 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 new file mode 100644 index 0000000..5c6184b --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs @@ -0,0 +1,98 @@ +use { + super::CacaoError, + alloy_primitives::{Address, FixedBytes}, + alloy_providers::provider::{Provider, TempProvider}, + alloy_rpc_types::{CallInput, CallRequest}, + alloy_sol_types::{sol, SolCall}, + alloy_transport_http::Http, + url::Url, +}; + +pub mod blockchain_api; +pub mod get_rpc_url; + +pub const EIP1271: &str = "eip1271"; + +// https://eips.ethereum.org/EIPS/eip-1271 +const MAGIC_VALUE: u32 = 0x1626ba7e; +sol! { + function isValidSignature( + bytes32 _hash, + bytes memory _signature) + public + view + returns (bytes4 magicValue); +} + +pub async fn verify_eip1271( + signature: Vec, + address: Address, + hash: &[u8; 32], + provider: Url, +) -> Result { + let provider = Provider::new(Http::new(provider)); + + let call_request = CallRequest { + to: Some(address), + input: CallInput::new( + isValidSignatureCall { + _hash: FixedBytes::from(hash), + _signature: signature, + } + .abi_encode() + .into(), + ), + ..Default::default() + }; + + let result = provider.call(call_request, None).await.map_err(|e| { + if let Some(error_response) = e.as_error_resp() { + if error_response.message.starts_with("execution reverted:") { + CacaoError::Verification + } else { + CacaoError::Eip1271Internal(e) + } + } else { + CacaoError::Eip1271Internal(e) + } + })?; + + if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { + Ok(true) + } else { + Err(CacaoError::Verification) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix}, + alloy_primitives::address, + sha3::{Digest, Keccak256}, + }; + + // Manual test. Paste address, signature, message, and project ID to verify + // function + #[tokio::test] + #[ignore] + async fn test_eip1271() { + let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let signature = "xxx"; + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(signature).as_bytes()) + .map_err(|_| CacaoError::Verification) + .unwrap(); + let message = "xxx"; + let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] + .try_into() + .unwrap(); + let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" + .parse() + .unwrap(); + assert!(verify_eip1271(signature, address, hash, provider) + .await + .unwrap()); + } +} diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs new file mode 100644 index 0000000..4cb180d --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -0,0 +1,39 @@ +use { + super::CacaoError, + crate::auth::cacao::signature::strip_hex_prefix, + sha3::{Digest, Keccak256}, +}; + +pub const EIP191: &str = "eip191"; + +pub fn eip191_bytes(message: &str) -> Vec { + format!( + "\u{0019}Ethereum Signed Message:\n{}{}", + message.as_bytes().len(), + message + ) + .into() +} + +pub fn verify_eip191(signature: &[u8], address: &str, hash: Keccak256) -> Result { + use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; + + let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; + let recovery_id = + RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; + + let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) + .map_err(|_| CacaoError::Verification)?; + + let add = &Keccak256::default() + .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) + .finalize()[12..]; + + let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); + + if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { + Err(CacaoError::Verification) + } else { + Ok(true) + } +} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs new file mode 100644 index 0000000..bd2c4b2 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -0,0 +1,63 @@ +use { + self::{ + eip1271::{get_rpc_url::GetRpcUrl, verify_eip1271, EIP1271}, + eip191::{eip191_bytes, verify_eip191, EIP191}, + }, + super::{Cacao, CacaoError}, + alloy_primitives::Address, + serde::{Deserialize, Serialize}, + sha3::{Digest, Keccak256}, + std::str::FromStr, +}; + +pub mod eip1271; +pub mod eip191; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] +pub struct Signature { + pub t: String, + pub s: String, +} + +impl Signature { + pub async fn verify( + &self, + cacao: &Cacao, + get_provider: &impl GetRpcUrl, + ) -> Result { + let address = cacao.p.address()?; + + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) + .map_err(|_| CacaoError::Verification)?; + + let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); + + 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 + } else { + Err(CacaoError::ProviderNotAvailable) + } + } + _ => Err(CacaoError::UnsupportedSignature), + } + } +} + +/// Remove the "0x" prefix from a hex string. +fn strip_hex_prefix(s: &str) -> &str { + s.strip_prefix("0x").unwrap_or(s) +} diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 644deee..74c01d4 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,4 +1,4 @@ -use {super::signature::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; +use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; struct MockGetRpcUrl; From 2dcdec9e8b0d7fc947597755099ce266317be195 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 31 Jan 2024 17:48:46 -0500 Subject: [PATCH 10/10] fix: derive standard traits --- relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs index 233759c..ac8dcc7 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs @@ -30,6 +30,7 @@ const SUPPORTED_CHAINS: [&str; 26] = [ "eip155:999", ]; +#[derive(Debug, Clone)] pub struct BlockchainApiProvider { project_id: ProjectId, }