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

Update CCIP implementation and improve error handling #6

Merged
merged 7 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
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/
26 changes: 15 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,29 @@ 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"

[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
136 changes: 136 additions & 0 deletions src/ccip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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_insert_with(Vec::new)
.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