Skip to content

Commit

Permalink
Update CCIP implementation and improve error handling (#6)
Browse files Browse the repository at this point in the history
Co-authored-by: Luc <[email protected]>
  • Loading branch information
Antony1060 and lucemans authored Nov 22, 2023
1 parent 5669e42 commit 62688ad
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 280 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
Cargo.lock
.idea/
29 changes: 18 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 3 additions & 1 deletion examples/offchain.rs
Original file line number Diff line number Diff line change
@@ -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<()> {
Expand Down
133 changes: 133 additions & 0 deletions src/ccip.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub message: Option<String>,
}

pub async fn handle_ccip_raw(
client: &reqwest::Client,
url: &str,
sender: &Address,
calldata: &[u8],
) -> Result<Bytes, CCIPRequestError> {
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<M: Middleware>(
client: &reqwest::Client,
sender: &Address,
tx: &TypedTransaction,
calldata: &[u8],
urls: Vec<String>,
) -> Result<Bytes, CCIPReadMiddlewareError<M>> {
// 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<String, Vec<String>> = 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<T: Clone + Hash + Eq>(src: &[T]) -> Vec<T> {
let mut set = HashSet::new();

let mut copy = src.to_vec();
copy.retain(|item| set.insert(item.clone()));

copy
}
45 changes: 41 additions & 4 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -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<String, Vec<String>>);

/// Handle CCIP-Read middlware specific errors.
#[derive(Error, Debug)]
pub enum CCIPReadMiddlewareError<M: Middleware> {
Expand All @@ -9,14 +31,11 @@ pub enum CCIPReadMiddlewareError<M: Middleware> {
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,

Expand All @@ -33,6 +52,12 @@ pub enum CCIPReadMiddlewareError<M: Middleware> {
#[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,
}
Expand All @@ -51,3 +76,15 @@ impl<M: Middleware> MiddlewareError for CCIPReadMiddlewareError<M> {
}
}
}

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()
}
}
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 62688ad

Please sign in to comment.