diff --git a/.gitignore b/.gitignore index 96ef6c0..77147e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.idea/ diff --git a/Cargo.toml b/Cargo.toml index 29abad6..394fbee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,25 +18,32 @@ exclude = [ ] [dependencies] +# Tracing +tracing = "0.1.40" + # Error handling -thiserror = { version = "1.0.26", default-features = false } +thiserror = { version = "1.0.50", default-features = false } # Serialization/deserialization -serde_json = "1" +serde_json = "1.0.108" +serde = { version = "1.0.192", features = ["derive"] } # HTTP -reqwest = "0.11" +reqwest = "0.11.22" # Async -async-recursion = "1.0.4" -async-trait = { version = "0.1.50", default-features = false } +async-recursion = "1.0.5" +async-trait = { version = "0.1.74", default-features = false } # Ethers -ethers-core = "2.0.4" -ethers-providers = "2.0.4" -futures-util = "0.3.28" +ethers-core = "2.0.11" +ethers-providers = "2.0.11" +futures-util = "0.3.29" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] -tokio = { version = "1.7.1", features = ["macros", "rt-multi-thread"] } -ethers = "2.0.4" -anyhow = "1.0" +tokio = { version = "1.34.0", features = ["macros", "rt-multi-thread"] } +ethers = "2.0.11" +anyhow = "1.0.75" diff --git a/examples/offchain.rs b/examples/offchain.rs index 02492f0..ca63225 100644 --- a/examples/offchain.rs +++ b/examples/offchain.rs @@ -1,7 +1,9 @@ +use std::convert::TryFrom; + use anyhow::Result; use ethers::prelude::*; + use ethers_ccip_read::CCIPReadMiddleware; -use std::convert::TryFrom; #[tokio::main] async fn main() -> Result<()> { diff --git a/src/ccip.rs b/src/ccip.rs new file mode 100644 index 0000000..fb5dfe8 --- /dev/null +++ b/src/ccip.rs @@ -0,0 +1,133 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; + +use ethers_core::types::transaction::eip2718::TypedTransaction; +use ethers_core::types::{Address, Bytes}; +use ethers_core::utils::hex; +use ethers_providers::Middleware; +use reqwest::Response; +use serde::Deserialize; + +use crate::errors::{CCIPFetchError, CCIPRequestError}; +use crate::utils::truncate_str; +use crate::CCIPReadMiddlewareError; + +#[derive(Debug, Clone, Deserialize)] +pub struct CCIPResponse { + pub data: Option, + pub message: Option, +} + +pub async fn handle_ccip_raw( + client: &reqwest::Client, + url: &str, + sender: &Address, + calldata: &[u8], +) -> Result { + tracing::debug!("making CCIP request to {url}"); + + let sender_hex = hex::encode_prefixed(sender.0); + let data_hex: String = hex::encode_prefixed(calldata); + + tracing::debug!("sender: {}", sender_hex); + tracing::debug!("data: {}", truncate_str(&data_hex, 20)); + + let request = if url.contains("{data}") { + let href = url + .replace("{sender}", &sender_hex) + .replace("{data}", &data_hex); + + client.get(href) + } else { + let body = serde_json::json!({ + "data": data_hex, + "sender": sender_hex + }); + + client.post(url).json(&body) + }; + + let resp: Response = request.send().await?; + + let resp_text = resp.text().await?; + + // TODO: handle non-json responses + // in case of erroneous responses, server can return Content-Type that is not application/json + // in this case, we should read the response as text and perhaps treat that as the error + let result: CCIPResponse = serde_json::from_str(&resp_text).map_err(|err| { + CCIPRequestError::GatewayFormatError(format!( + "response format error: {err}, gateway returned: {resp_text}" + )) + })?; + + if let Some(response_data) = result.data { + return hex::decode(response_data) + .map_err(|_| { + CCIPRequestError::GatewayFormatError( + "response data is not a valid hex sequence".to_string(), + ) + }) + .map(Bytes::from); + }; + + if let Some(message) = result.message { + return Err(CCIPRequestError::GatewayError(message)); + } + + Err(CCIPRequestError::GatewayFormatError( + "response format error: invalid response".to_string(), + )) +} + +/// This function makes a Cross-Chain Interoperability Protocol (CCIP-Read) request +/// and returns the result as `Bytes` or an error message. +/// +/// # Arguments +/// +/// * `sender`: The sender's address. +/// * `tx`: The typed transaction. +/// * `calldata`: The function call data as bytes. +/// * `urls`: A vector of Offchain Gateway URLs to send the request to. +/// +/// # Returns +/// +/// an opaque byte string to send to callbackFunction on Offchain Resolver contract. +pub async fn handle_ccip( + client: &reqwest::Client, + sender: &Address, + tx: &TypedTransaction, + calldata: &[u8], + urls: Vec, +) -> Result> { + // If there are no URLs or the transaction's destination is empty, return an empty result + if urls.is_empty() || tx.to().is_none() { + return Ok(Bytes::new()); + } + + let urls = dedup_ord(&urls); + + // url —> [error_message] + let mut errors: HashMap> = HashMap::new(); + + for url in urls { + let result = handle_ccip_raw(client, &url, sender, calldata).await; + + match result { + Ok(result) => return Ok(result), + Err(err) => { + errors.entry(url).or_default().push(err.to_string()); + } + } + } + + Err(CCIPReadMiddlewareError::FetchError(CCIPFetchError(errors))) +} + +fn dedup_ord(src: &[T]) -> Vec { + let mut set = HashSet::new(); + + let mut copy = src.to_vec(); + copy.retain(|item| set.insert(item.clone())); + + copy +} diff --git a/src/errors.rs b/src/errors.rs index 4fbf79d..dfaad01 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,28 @@ +use ethers_core::utils::hex::FromHexError; +use std::collections::HashMap; +use std::fmt::Display; + use ethers_providers::{Middleware, MiddlewareError}; use thiserror::Error; +#[allow(clippy::enum_variant_names)] +#[derive(Error, Debug)] +pub enum CCIPRequestError { + // gateway supplied error + #[error("Gateway error: {0}")] + GatewayError(String), + + // when gateway either fails to respond with an expected format + #[error("Gateway format error: {0}")] + GatewayFormatError(String), + + #[error("HTTP error: {0}")] + HTTPError(#[from] reqwest::Error), +} + +#[derive(Debug)] +pub struct CCIPFetchError(pub(crate) HashMap>); + /// Handle CCIP-Read middlware specific errors. #[derive(Error, Debug)] pub enum CCIPReadMiddlewareError { @@ -9,14 +31,11 @@ pub enum CCIPReadMiddlewareError { MiddlewareError(M::Error), #[error("Error(s) during CCIP fetch: {0}")] - FetchError(String), + FetchError(CCIPFetchError), #[error("CCIP Read sender did not match {}", sender)] SenderError { sender: String }, - #[error("Bad result from backend: {0}")] - GatewayError(String), - #[error("CCIP Read no provided URLs")] GatewayNotFoundError, @@ -33,6 +52,12 @@ pub enum CCIPReadMiddlewareError { #[error("Error(s) during NFT ownership verification: {0}")] NFTOwnerError(String), + #[error("Error(s) decoding revert bytes: {0}")] + HexDecodeError(#[from] FromHexError), + + #[error("Error(s) decoding abi: {0}")] + AbiDecodeError(#[from] ethers_core::abi::Error), + #[error("Unsupported URL scheme")] UnsupportedURLSchemeError, } @@ -51,3 +76,15 @@ impl MiddlewareError for CCIPReadMiddlewareError { } } } + +impl Display for CCIPFetchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut errors = f.debug_struct("CCIPFetchError"); + + for (url, messages) in self.0.iter() { + errors.field(url, messages); + } + + errors.finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index c75b99f..d8056df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ //! # Ethers CCIP-Read //! //! Provides an [ethers](https://docs.rs/ethers) compatible middleware for submitting -mod middleware; +pub use errors::CCIPReadMiddlewareError; pub use middleware::CCIPReadMiddleware; +mod ccip; mod errors; -pub use errors::CCIPReadMiddlewareError; - +mod middleware; pub mod utils; diff --git a/src/middleware.rs b/src/middleware.rs index 9f92c8b..15bc10f 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,50 +1,89 @@ +use std::iter::successors; use std::str::FromStr; +use std::time::Duration; use async_recursion::async_recursion; use async_trait::async_trait; - +use ethers_core::abi::Detokenize; +use ethers_core::types::{Address, BlockNumber, Selector, TransactionRequest, H160, U256}; use ethers_core::{ - abi::{self, Detokenize, ParamType, Token}, - types::{ - transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes, - NameOrAddress, Selector, TransactionRequest, H160, U256, - }, - utils::{self, hex}, + abi::{self, ParamType, Token}, + types::{transaction::eip2718::TypedTransaction, BlockId, Bytes, NameOrAddress}, + utils::hex, }; use ethers_providers::{ens, erc, Middleware, MiddlewareError}; use futures_util::try_join; use hex::FromHex; -use reqwest::{Response, Url}; +use reqwest::Url; use serde_json::Value; -use crate::{ - utils::{decode_bytes, dns_encode}, - CCIPReadMiddlewareError, -}; +use crate::ccip::handle_ccip; +use crate::utils::{build_reqwest, decode_bytes, dns_encode}; +use crate::CCIPReadMiddlewareError; #[derive(Debug, Clone)] pub struct CCIPReadMiddleware { - inner: M, + provider: M, + ens: Address, + reqwest_client: reqwest::Client, + max_redirect_attempt: u8, +} + +pub struct CCIPReadMiddlewareBuilder { + provider: Option, ens: Option
, + timeout: Option, + max_redirect_attempt: Option, } -static MAX_CCIP_REDIRECT_ATTEMPT: u8 = 10; +impl Default for CCIPReadMiddlewareBuilder { + fn default() -> Self { + CCIPReadMiddlewareBuilder { + provider: None, + ens: None, + timeout: None, + max_redirect_attempt: None, + } + } +} -impl CCIPReadMiddleware -where - M: Middleware, -{ +impl CCIPReadMiddlewareBuilder { + pub fn with_provider(mut self, provider: M) -> Self { + self.provider = Some(provider); + self + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + pub fn with_max_redirect_attempt(mut self, max_redirect_attempt: u8) -> Self { + self.max_redirect_attempt = Some(max_redirect_attempt); + self + } + + pub fn build(self) -> Result, String> { + Ok(CCIPReadMiddleware { + provider: self.provider.ok_or("provider is required".to_string())?, + ens: self.ens.unwrap_or(ens::ENS_ADDRESS), + reqwest_client: build_reqwest(self.timeout.unwrap_or(Duration::from_secs(10))), + max_redirect_attempt: self.max_redirect_attempt.unwrap_or(10), + }) + } +} + +static OFFCHAIN_LOOKUP_SELECTOR: &[u8] = &[0x55, 0x6f, 0x18, 0x30]; + +impl CCIPReadMiddleware { /// Creates an instance of CCIPReadMiddleware /// `ìnner` the inner Middleware pub fn new(inner: M) -> Self { - Self { inner, ens: None } + Self::builder().with_provider(inner).build().unwrap() } - /// Sets the ENS Address (default: mainnet) - #[must_use] - pub fn ens>(mut self, ens: T) -> Self { - self.ens = Some(ens.into()); - self + pub fn builder() -> CCIPReadMiddlewareBuilder { + CCIPReadMiddlewareBuilder::default() } /// The supports_wildcard checks if a given resolver supports the wildcard resolution by calling @@ -90,10 +129,11 @@ where return Ok(false); } - let _result: U256 = decode_bytes(ParamType::Uint(256), _tx); + // Decode the result + let data: U256 = decode_bytes(ParamType::Uint(256), &_tx.0)?; // If the result is one, the resolver supports wildcard resolution; otherwise, it does not - Ok(_result.eq(&U256::one())) + Ok(data == U256::one()) } async fn query_resolver( @@ -140,130 +180,57 @@ where // resolve let mut data = self.call(&tx, None).await?; if parse_bytes { - data = decode_bytes(ParamType::Bytes, data); + data = decode_bytes(ParamType::Bytes, &data)?; } - Ok(decode_bytes(param, data)) + Ok(decode_bytes(param, &data)?) } pub async fn get_resolver(&self, ens_name: &str) -> Result> { - let mut current_name: String = ens_name.to_string(); + let ens_addr = self.ens; - let ens_addr = self.ens.unwrap_or(ens::ENS_ADDRESS); + let names: Vec<&str> = + successors(Some(ens_name), |&last| last.split_once('.').map(|it| it.1)).collect(); - loop { - if current_name.eq("") || current_name.eq(".") { + for name in names { + if name.is_empty() || name.eq(".") { return Ok(H160::zero()); } - if !ens_name.eq("eth") && current_name.eq("eth") { + if !ens_name.eq("eth") && name.eq("eth") { return Ok(H160::zero()); } let data = self - .call( - &ens::get_resolver(ens_addr, ¤t_name.to_string()).into(), - None, - ) + .call(&ens::get_resolver(ens_addr, name).into(), None) .await?; if data.0.is_empty() { return Ok(H160::zero()); } - let resolver_address: Address = decode_bytes(ParamType::Address, data); + let resolver_address: Address = decode_bytes(ParamType::Address, &data)?; if resolver_address != Address::zero() { - if current_name != ens_name && !self.supports_wildcard(resolver_address).await? { + if name != ens_name && !self.supports_wildcard(resolver_address).await? { return Ok(H160::zero()); } return Ok(resolver_address); } - - let mut splitted_name: Vec<&str> = current_name.split('.').collect(); - current_name = splitted_name.split_off(1).join(".").to_string(); - } - } - - /// This function makes a Cross-Chain Interoperability Protocol (CCIP-Read) request - /// and returns the result as `Bytes` or an error message. - /// - /// # Arguments - /// - /// * `sender`: The sender's address. - /// * `tx`: The typed transaction. - /// * `calldata`: The function call data as bytes. - /// * `urls`: A vector of Offchain Gateway URLs to send the request to. - /// - /// # Returns - /// - /// an opaque byte string to send to callbackFunction on Offchain Resolver contract. - async fn ccip_request( - &self, - sender: Address, - tx: &TypedTransaction, - calldata: &[u8], - urls: Vec<&str>, - ) -> Result> { - // If there are no URLs or the transaction's destination is empty, return an empty result - if urls.is_empty() || tx.to().is_none() { - return Ok(Bytes::from([])); - } - - // Convert calldata to a hex string - let data: String = calldata - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect(); - - let mut error_messages: Vec = vec![]; - - for (_i, url) in urls.iter().enumerate() { - // Replace the placeholders in the URL with the sender address and data - let href = url - .replace("{sender}", &format!("0x{:x}", sender)) - .replace("{data}", &format!("0x{}", &data.to_lowercase()).to_string()); - - let mut request: reqwest::RequestBuilder = reqwest::Client::new().get(&href); - - // If the URL does not contain the "{data}" placeholder, create a POST request instead - if !url.contains("{data}") { - let body = serde_json::json!({ "data": data, "sender": sender }); - request = reqwest::Client::new().post(&href).json(&body); - } - - let resp: Response = request.send().await.unwrap(); - - let result: Value = resp.json().await.unwrap(); - let mut error_message = "unknown error"; - - // If the result contains the "data" field, decode the data and return it as Bytes - if let Some(returned_data) = result.get("data") { - let decoded: Vec = - hex::decode(&returned_data.as_str().unwrap().to_string()[2..]).unwrap(); - return Ok(Bytes::from(decoded)); - }; - - if let Some(_message) = result.get("message") { - error_message = result["message"].as_str().unwrap(); - } - - error_messages.push(error_message.to_string()); } - Err(CCIPReadMiddlewareError::FetchError( - error_messages.join(", "), - )) + Ok(H160::zero()) } - #[async_recursion] + #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] + #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] async fn _call( &self, transaction: &TypedTransaction, block_id: Option, attempt: u8, ) -> Result> { - if attempt >= MAX_CCIP_REDIRECT_ATTEMPT { + if attempt >= self.max_redirect_attempt { // may need more info return Err(CCIPReadMiddlewareError::MaxRedirectionError); } @@ -273,101 +240,94 @@ where NameOrAddress::Address(addr) => *addr, }; - // let tx_value: Value = utils::serialize(transaction); - let block_value = utils::serialize(&block_id.unwrap_or_else(|| BlockNumber::Latest.into())); - let result = match self.inner().call(transaction, block_id).await { - Ok(response) => response.to_string(), - Err(provider_error) => { - let content = provider_error.as_error_response().unwrap(); - let data = content.data.as_ref().unwrap_or(&serde_json::Value::Null); - if data.is_null() { - return Err(CCIPReadMiddlewareError::GatewayError(content.to_string())); - } - data.to_string() - .trim_matches('"') - .trim_start_matches("0x") - .to_string() - } - }; + let result = self + .inner() + .call(transaction, block_id) + .await + .or_else(|err| { + let Some(rpc_err) = err.as_error_response() else { + return Err(CCIPReadMiddlewareError::MiddlewareError(err)); + }; + + let Some(Value::String(data)) = rpc_err.clone().data else { + return Err(CCIPReadMiddlewareError::MiddlewareError(err)); + }; - if block_value.eq("latest") - && !tx_sender.is_zero() - && result.starts_with("556f1830") - && hex::decode(result.clone()).unwrap().len() % 32 == 4 + Ok(Bytes::from_hex(data)?) + })?; + + if !matches!(block_id.unwrap_or(BlockId::Number(BlockNumber::Latest)), BlockId::Number(block) if block.is_latest()) { - let output_types = vec![ - ParamType::Address, // 'address' - ParamType::Array(Box::new(ParamType::String)), // 'string[]' - ParamType::Bytes, // 'bytes' - ParamType::FixedBytes(4), // 'bytes4' - ParamType::Bytes, // 'bytes' - ]; - - let decoded_data: Vec = - abi::decode(&output_types, &Vec::from_hex(&result.clone()[8..]).unwrap()).unwrap(); - - if let ( - Token::Address(addr), - Token::Array(strings), - Token::Bytes(bytes), - Token::FixedBytes(bytes4), - Token::Bytes(bytes2), - ) = ( - decoded_data.get(0).unwrap(), - decoded_data.get(1).unwrap(), - decoded_data.get(2).unwrap(), - decoded_data.get(3).unwrap(), - decoded_data.get(4).unwrap(), - ) { - let sender: Address = *addr; - let urls: Vec<&str> = strings - .iter() - .map(|t| match t { - Token::String(s) => s.as_str(), - _ => panic!("CCIP Read contained corrupt URL string"), - }) - .collect(); - - let call_data: &[u8] = bytes; - let callback_selector: Vec = bytes4.clone(); - let extra_data: &[u8] = bytes2; - - if !sender.eq(&tx_sender) { - return Err(CCIPReadMiddlewareError::SenderError { - sender: format!("0x{:x}", sender), - }); - } + return Ok(result); + } - let ccip_result = self - .ccip_request(sender, transaction, call_data, urls) - .await?; - if ccip_result.is_empty() { - return Err(CCIPReadMiddlewareError::GatewayNotFoundError); - } + if tx_sender.is_zero() + || !result.starts_with(OFFCHAIN_LOOKUP_SELECTOR) + || result.len() % 32 != 4 + { + return Ok(result); + } - let ccip_result_token = Token::Bytes(ccip_result.as_ref().to_vec()); - let extra_data_token = Token::Bytes(extra_data.into()); + let output_types = vec![ + ParamType::Address, // 'address' + ParamType::Array(Box::new(ParamType::String)), // 'string[]' + ParamType::Bytes, // 'bytes' + ParamType::FixedBytes(4), // 'bytes4' + ParamType::Bytes, // 'bytes' + ]; + + let decoded_data: Vec = abi::decode(&output_types, &result[4..])?; + + let ( + Some(Token::Address(sender)), + Some(Token::Array(urls)), + Some(Token::Bytes(calldata)), + Some(Token::FixedBytes(callback_selector)), + Some(Token::Bytes(extra_data)), + ) = ( + decoded_data.get(0), + decoded_data.get(1), + decoded_data.get(2), + decoded_data.get(3), + decoded_data.get(4), + ) + else { + return Ok(result); + }; - let tokens = vec![ccip_result_token, extra_data_token]; + let urls: Vec = urls + .iter() + .cloned() + // NOTE: not sure about how good filter_map is here + // i.e. should we return an error or handle it more gracefully? + // for now, ignoring non-string values is definitely better than panicking + .filter_map(|t| t.into_string()) + .collect(); - let encoded_data = abi::encode(&tokens); - let mut new_transaction = transaction.clone(); - new_transaction.set_data(Bytes::from( - [callback_selector.clone(), encoded_data.clone()].concat(), - )); + if !sender.eq(&tx_sender) { + return Err(CCIPReadMiddlewareError::SenderError { + sender: format!("0x{:x}", sender), + }); + } - return self._call(&new_transaction, block_id, attempt + 1).await; - } + let ccip_result = + handle_ccip(&self.reqwest_client, sender, transaction, calldata, urls).await?; + + if ccip_result.is_empty() { + return Err(CCIPReadMiddlewareError::GatewayNotFoundError); } - let result = match Bytes::from_str(&result) { - Ok(bytes) => bytes, - Err(error) => { - println!("error: {:?}", error); - return Err(CCIPReadMiddlewareError::GatewayError(error.to_string())); - } - }; - Ok(result) + let ccip_result_token = Token::Bytes(ethers_core::abi::Bytes::from(ccip_result.as_ref())); + let extra_data_token = Token::Bytes(extra_data.clone()); + + let encoded_data = abi::encode(&[ccip_result_token, extra_data_token]); + + let mut callback_tx = transaction.clone(); + callback_tx.set_data(Bytes::from( + [callback_selector.clone(), encoded_data.clone()].concat(), + )); + + self._call(&callback_tx, block_id, attempt + 1).await } } @@ -384,7 +344,7 @@ where /// Get a reference to the inner middleware fn inner(&self) -> &M { - &self.inner + &self.provider } /// Call the underlying middleware with the provided transaction and block @@ -396,6 +356,10 @@ where return self._call(tx, block, 0).await; } + /** + The following couple of methods were copied from ethers-rs, and modified to work with ENSIP-10 + **/ + /// Resolve a field of an ENS name async fn resolve_field(&self, ens_name: &str, field: &str) -> Result { let field: String = self @@ -433,7 +397,7 @@ where ..Default::default() }; let data = self.call(&tx.into(), None).await?; - if decode_bytes::
(ParamType::Address, data) != owner { + if decode_bytes::
(ParamType::Address, &data)? != owner { return Err(CCIPReadMiddlewareError::NFTOwnerError( "Incorrect owner.".to_string(), )); @@ -455,7 +419,7 @@ where ..Default::default() }; let data = self.call(&tx.into(), None).await?; - if decode_bytes::(ParamType::Uint(64), data) == 0 { + if decode_bytes::(ParamType::Uint(64), &data)? == 0 { return Err(CCIPReadMiddlewareError::NFTOwnerError( "Incorrect balance.".to_string(), )); @@ -498,9 +462,10 @@ where #[cfg(test)] mod tests { - use super::*; use ethers_core::types::TransactionRequest; - use ethers_providers::MAINNET; + use ethers_providers::{JsonRpcError, MockResponse, Provider, MAINNET}; + + use super::*; #[tokio::test] async fn test_eip_2544_ens_wildcards() { @@ -516,8 +481,8 @@ mod tests { ); let supports_wildcard = provider.supports_wildcard(resolver_address).await.unwrap(); - assert_eq!( - supports_wildcard, true, + assert!( + supports_wildcard, "Wildcard is not supported, expected to be true" ); @@ -548,49 +513,47 @@ mod tests { let result = provider.call(&tx, None).await.unwrap(); - let data: Bytes = decode_bytes(ParamType::Bytes, result); - let record: String = decode_bytes(ParamType::String, data); + let data: Bytes = decode_bytes(ParamType::Bytes, &result).unwrap(); + let record: String = decode_bytes(ParamType::String, &data).unwrap(); assert_eq!(record, email); } - // todo: requires modification in ethers-rs, uncomment when merged - // - // #[tokio::test] - // async fn test_mismatched_sender() { - // let resolver_address = "0xC1735677a60884ABbCF72295E88d47764BeDa282"; - - // let (provider, mock) = Provider::mocked(); - // let provider = CCIPReadMiddleware::new(provider); - - // let tx: TypedTransaction = TransactionRequest { - // // parameters = text(bytes32 node, string calldata key) node: namehash('1.offchainexample.eth'), key: 'email' - // // tx_data = selector(resolve(bytes,bytes)), namehash(name), parameters - // // ensip10 interface + encode(dnsencode(name), tx_data) - // data: Some(Bytes::from(hex::decode("9061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap())), - // to: Some(resolver_address.into()), - // ..Default::default() - // }.into(); - - // let error_code = 3; - // // sender information altered to c1735677a60884abbcf72295e88d47764beda283 - // let error_data = r#""0x556f1830000000000000000000000000c1735677a60884abbcf72295e88d47764beda28300000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000160f4d4d2f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004768747470733a2f2f6f6666636861696e2d7265736f6c7665722d6578616d706c652e75632e722e61707073706f742e636f6d2f7b73656e6465727d2f7b646174617d2e6a736f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001449061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001449061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000""#; - // let error_message = "execution reverted"; - // let error = JsonRpcError { - // code: error_code, - // data: Some(serde_json::from_str(error_data).unwrap()), - // message: error_message.to_string(), - // }; - // mock.push_response(MockResponse::Error(error.clone())); - - // let result = provider.call(&tx, None).await; - // assert!(result.is_err()); - // assert_eq!( - // result.unwrap_err().to_string(), - // format!( - // "CCIP Read sender did not match {}", - // "0xc1735677a60884abbcf72295e88d47764beda283" - // ) - // ); - // } + #[tokio::test] + async fn test_mismatched_sender() { + let resolver_address = "0xC1735677a60884ABbCF72295E88d47764BeDa282"; + + let (provider, mock) = Provider::mocked(); + let provider = CCIPReadMiddleware::new(provider); + + let tx: TypedTransaction = TransactionRequest { + // parameters = text(bytes32 node, string calldata key) node: namehash('1.offchainexample.eth'), key: 'email' + // tx_data = selector(resolve(bytes,bytes)), namehash(name), parameters + // ensip10 interface + encode(dnsencode(name), tx_data) + data: Some(Bytes::from(hex::decode("9061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap())), + to: Some(resolver_address.into()), + ..Default::default() + }.into(); + + let error_code = 3; + // sender information altered to c1735677a60884abbcf72295e88d47764beda283 + let error_data = r#""0x556f1830000000000000000000000000c1735677a60884abbcf72295e88d47764beda28300000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000160f4d4d2f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004768747470733a2f2f6f6666636861696e2d7265736f6c7665722d6578616d706c652e75632e722e61707073706f742e636f6d2f7b73656e6465727d2f7b646174617d2e6a736f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001449061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001449061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001701310f6f6666636861696e6578616d706c650365746800000000000000000000000000000000000000000000000000000000000000000000000000000000008459d1d43c1c9fb8c1fe76f464ccec6d2c003169598fdfcbcb6bbddf6af9c097a39fa0048c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000005656d61696c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000""#; + let error_message = "execution reverted"; + let error = JsonRpcError { + code: error_code, + data: Some(serde_json::from_str(error_data).unwrap()), + message: error_message.to_string(), + }; + mock.push_response(MockResponse::Error(error.clone())); + + let result = provider.call(&tx, None).await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + format!( + "CCIP Read sender did not match {}", + "0xc1735677a60884abbcf72295e88d47764beda283" + ) + ); + } } diff --git a/src/utils.rs b/src/utils.rs index 91f778e..36f99fe 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,17 +1,32 @@ -use ethers_core::{ - abi::{self, Detokenize, ParamType}, - types::Bytes, -}; +use ethers_core::abi; +use ethers_core::abi::{Detokenize, ParamType}; +use std::time::Duration; -/// infallible conversion of Bytes to Address/String -/// -/// # Panics -/// -/// If the provided bytes were not an interpretation of an address -pub fn decode_bytes(param: ParamType, bytes: Bytes) -> T { - let tokens = abi::decode(&[param], bytes.as_ref()) - .expect("could not abi-decode bytes to address tokens"); - T::from_tokens(tokens).expect("could not parse tokens as address") +pub(crate) fn truncate_str(src: &str, side: usize) -> String { + if src.len() < side * 2 + 3 { + return src.to_string(); + } + + format!("{}..{}", &src[..side], &src[src.len() - side..]) +} + +pub(crate) fn decode_bytes(param: ParamType, bytes: &[u8]) -> Result { + let tokens = abi::decode(&[param], bytes)?; + T::from_tokens(tokens).map_err(|err| abi::Error::Other(err.to_string().into())) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn build_reqwest(timeout: Duration) -> reqwest::Client { + reqwest::Client::builder() + .timeout(timeout) + .build() + .expect("should be a valid reqwest client") +} + +#[cfg(target_arch = "wasm32")] +pub(crate) fn build_reqwest(_timeout: Duration) -> reqwest::Client { + // reqwest doesn't support timeouts on wasm + reqwest::Client::new() } /// Encodes a domain name into its binary representation according to the DNS