diff --git a/core/src/execution_options/eip7702.rs b/core/src/execution_options/eip7702.rs index 225b2c1..ab4a1cd 100644 --- a/core/src/execution_options/eip7702.rs +++ b/core/src/execution_options/eip7702.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::defs::AddressDef; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] +#[schema(title = "EIP-7702 Execution Options")] #[serde(rename_all = "camelCase")] pub struct Eip7702ExecutionOptions { /// The EOA address that will sign the EIP-7702 transaction diff --git a/core/src/rpc_clients/bundler.rs b/core/src/rpc_clients/bundler.rs index e99f386..7a1a3a2 100644 --- a/core/src/rpc_clients/bundler.rs +++ b/core/src/rpc_clients/bundler.rs @@ -75,7 +75,7 @@ pub struct TwExecuteResponse { #[serde(rename_all = "camelCase")] pub struct TwGetTransactionHashResponse { /// The transaction hash - pub transaction_hash: String, + pub transaction_hash: Option, } impl BundlerClient { @@ -149,7 +149,10 @@ impl BundlerClient { } /// Get transaction hash from bundler using transaction ID - pub async fn tw_get_transaction_hash(&self, transaction_id: &str) -> TransportResult { + pub async fn tw_get_transaction_hash( + &self, + transaction_id: &str, + ) -> TransportResult> { let params = serde_json::json!([transaction_id]); let response: TwGetTransactionHashResponse = diff --git a/core/src/signer.rs b/core/src/signer.rs index 92799f8..cb245ab 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -348,7 +348,7 @@ impl AccountSigner for EoaSigner { .await .map_err(|e| { tracing::error!("Error signing authorization with EOA (IAW): {:?}", e); - EngineError::from(e) + e })?; // Return the signed authorization as Authorization diff --git a/executors/src/eip7702_executor/confirm.rs b/executors/src/eip7702_executor/confirm.rs index aee2f2c..4db56c3 100644 --- a/executors/src/eip7702_executor/confirm.rs +++ b/executors/src/eip7702_executor/confirm.rs @@ -1,5 +1,6 @@ use alloy::primitives::{Address, TxHash}; use alloy::providers::Provider; +use engine_core::error::{AlloyRpcErrorToEngineError, EngineError}; use engine_core::{ chain::{Chain, ChainService, RpcCredentials}, execution_options::WebhookOptions, @@ -62,13 +63,25 @@ pub struct Eip7702ConfirmationResult { #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "errorCode")] pub enum Eip7702ConfirmationError { #[error("Chain service error for chainId {chain_id}: {message}")] + #[serde(rename_all = "camelCase")] ChainServiceError { chain_id: u64, message: String }, #[error("Failed to get transaction hash from bundler: {message}")] TransactionHashError { message: String }, #[error("Failed to confirm transaction: {message}")] - ConfirmationError { message: String }, + #[serde(rename_all = "camelCase")] + ConfirmationError { + message: String, + inner_error: Option, + }, + + #[error("Receipt not yet available for transaction: {message}")] + #[serde(rename_all = "camelCase")] + ReceiptNotAvailable { + message: String, + transaction_hash: TxHash, + }, #[error("Transaction failed: {message}")] TransactionFailed { message: String }, @@ -169,26 +182,25 @@ where .bundler_client() .tw_get_transaction_hash(&job_data.bundler_transaction_id) .await - .map_err(|e| { - // Check if it's a "not found" or "pending" error - let error_msg = e.to_string(); - if error_msg.contains("not found") || error_msg.contains("pending") { - // Transaction not ready yet, nack and retry - Eip7702ConfirmationError::TransactionHashError { - message: format!("Transaction not ready: {}", error_msg), - } - .nack(Some(Duration::from_secs(5)), RequeuePosition::Last) - } else { - Eip7702ConfirmationError::TransactionHashError { message: error_msg }.fail() - } - })?; + .map_err(|e| Eip7702ConfirmationError::TransactionHashError { + message: e.to_string(), + }) + .map_err_fail()?; - let transaction_hash = transaction_hash_str.parse::().map_err(|e| { - Eip7702ConfirmationError::TransactionHashError { - message: format!("Invalid transaction hash format: {}", e), + let transaction_hash = match transaction_hash_str { + Some(hash) => hash.parse::().map_err(|e| { + Eip7702ConfirmationError::TransactionHashError { + message: format!("Invalid transaction hash format: {}", e), + } + .fail() + })?, + None => { + return Err(Eip7702ConfirmationError::TransactionHashError { + message: "Transaction not found".to_string(), + }) + .map_err_nack(Some(Duration::from_secs(2)), RequeuePosition::Last); } - .fail() - })?; + }; tracing::debug!( transaction_hash = ?transaction_hash, @@ -205,18 +217,20 @@ where // If transaction not found, nack and retry Eip7702ConfirmationError::ConfirmationError { message: format!("Failed to get transaction receipt: {}", e), + inner_error: Some(e.to_engine_error(&chain)), } - .nack(Some(Duration::from_secs(10)), RequeuePosition::Last) + .nack(Some(Duration::from_secs(5)), RequeuePosition::Last) })?; let receipt = match receipt { Some(receipt) => receipt, None => { // Transaction not mined yet, nack and retry - return Err(Eip7702ConfirmationError::ConfirmationError { + return Err(Eip7702ConfirmationError::ReceiptNotAvailable { message: "Transaction not mined yet".to_string(), + transaction_hash, }) - .map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last); + .map_err_nack(Some(Duration::from_secs(2)), RequeuePosition::Last); } }; diff --git a/executors/src/eip7702_executor/send.rs b/executors/src/eip7702_executor/send.rs index 5248552..bebf6d2 100644 --- a/executors/src/eip7702_executor/send.rs +++ b/executors/src/eip7702_executor/send.rs @@ -1,7 +1,7 @@ use alloy::{ dyn_abi::TypedData, eips::eip7702::Authorization, - primitives::{Address, Bytes, ChainId, FixedBytes, U256}, + primitives::{Address, Bytes, ChainId, FixedBytes, U256, address}, providers::Provider, sol_types::eip712_domain, }; @@ -34,7 +34,8 @@ use crate::{ use super::confirm::{Eip7702ConfirmationHandler, Eip7702ConfirmationJobData}; -const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: &str = "0xD6999651Fc0964B9c6B444307a0ab20534a66560"; +const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address = + address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560"); // --- Job Payload --- #[derive(Serialize, Deserialize, Debug, Clone)] @@ -81,13 +82,18 @@ pub enum Eip7702SendError { ChainServiceError { chain_id: u64, message: String }, #[error("Failed to sign typed data: {message}")] - SigningError { message: String }, - - #[error("Failed to sign authorization: {message}")] - AuthorizationError { message: String }, + #[serde(rename_all = "camelCase")] + SigningError { + message: String, + inner_error: Option, + }, #[error("Failed to check 7702 delegation: {message}")] - DelegationCheckError { message: String }, + #[serde(rename_all = "camelCase")] + DelegationCheckError { + message: String, + inner_error: Option, + }, #[error("Failed to call bundler: {message}")] BundlerCallError { message: String }, @@ -241,7 +247,8 @@ where ) .await .map_err(|e| Eip7702SendError::SigningError { - message: e.to_string(), + message: format!("Failed to sign typed data: {e}"), + inner_error: Some(e), }) .map_err_fail()?; @@ -249,32 +256,28 @@ where let is_minimal_account = check_is_7702_minimal_account(&chain, job_data.eoa_address) .await .map_err(|e| Eip7702SendError::DelegationCheckError { - message: e.to_string(), + message: format!("Failed to check if wallet has 7702 delegation: {e}"), + inner_error: Some(e), }) .map_err_fail()?; // 5. Sign authorization if needed let authorization = if !is_minimal_account { let nonce = job_data.nonce.unwrap_or_default(); - let minimal_account_address: Address = MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS - .parse() - .map_err(|e| Eip7702SendError::AuthorizationError { - message: format!("Invalid minimal account implementation address: {}", e), - }) - .map_err_fail()?; let auth = self .eoa_signer .sign_authorization( signing_options.clone(), job_data.chain_id, - minimal_account_address, + MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, nonce, job_data.signing_credential.clone(), ) .await - .map_err(|e| Eip7702SendError::AuthorizationError { - message: e.to_string(), + .map_err(|e| Eip7702SendError::SigningError { + message: format!("Failed to sign authorization: {e}"), + inner_error: Some(e), }) .map_err_fail()?; @@ -493,12 +496,7 @@ async fn check_is_7702_minimal_account( let target_address = Address::from_slice(target_bytes); // Compare with the minimal account implementation address - let minimal_account_address: Address = - MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS - .parse() - .map_err(|e| EngineError::ValidationError { - message: format!("Invalid minimal account implementation address: {}", e), - })?; + let minimal_account_address: Address = MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS; let is_delegated = target_address == minimal_account_address; diff --git a/executors/src/external_bundler/confirm.rs b/executors/src/external_bundler/confirm.rs index a3dc929..1f6e3b6 100644 --- a/executors/src/external_bundler/confirm.rs +++ b/executors/src/external_bundler/confirm.rs @@ -52,15 +52,18 @@ pub struct UserOpConfirmationResult { #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "errorCode")] pub enum UserOpConfirmationError { #[error("Chain service error for chainId {chain_id}: {message}")] + #[serde(rename_all = "camelCase")] ChainServiceError { chain_id: u64, message: String }, #[error("Receipt not yet available for user operation {user_op_hash}")] + #[serde(rename_all = "camelCase")] ReceiptNotAvailable { user_op_hash: Bytes, attempt_number: u32, }, #[error("Failed to query user operation receipt: {message}")] + #[serde(rename_all = "camelCase")] ReceiptQueryFailed { user_op_hash: Bytes, message: String, @@ -68,6 +71,7 @@ pub enum UserOpConfirmationError { }, #[error("Internal error: {message}")] + #[serde(rename_all = "camelCase")] InternalError { message: String }, #[error("Transaction cancelled by user")]