Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 core/src/execution_options/eip7702.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions core/src/rpc_clients/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

impl BundlerClient {
Expand Down Expand Up @@ -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<String> {
pub async fn tw_get_transaction_hash(
&self,
transaction_id: &str,
) -> TransportResult<Option<String>> {
let params = serde_json::json!([transaction_id]);

let response: TwGetTransactionHashResponse =
Expand Down
2 changes: 1 addition & 1 deletion core/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 36 additions & 22 deletions executors/src/eip7702_executor/confirm.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<EngineError>,
},

#[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 },
Expand Down Expand Up @@ -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::<TxHash>().map_err(|e| {
Eip7702ConfirmationError::TransactionHashError {
message: format!("Invalid transaction hash format: {}", e),
let transaction_hash = match transaction_hash_str {
Some(hash) => hash.parse::<TxHash>().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,
Expand All @@ -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);
}
};

Expand Down
46 changes: 22 additions & 24 deletions executors/src/eip7702_executor/send.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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<EngineError>,
},

#[error("Failed to check 7702 delegation: {message}")]
DelegationCheckError { message: String },
#[serde(rename_all = "camelCase")]
DelegationCheckError {
message: String,
inner_error: Option<EngineError>,
},

#[error("Failed to call bundler: {message}")]
BundlerCallError { message: String },
Expand Down Expand Up @@ -241,40 +247,37 @@ 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()?;

// 4. Check if wallet has 7702 delegation set
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()?;

Expand Down Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions executors/src/external_bundler/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,26 @@ 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,
inner_error: Option<EngineError>,
},

#[error("Internal error: {message}")]
#[serde(rename_all = "camelCase")]
InternalError { message: String },

#[error("Transaction cancelled by user")]
Expand Down