From ceaa854a3e3d68f1030ec6fea843f704569a9b3a Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Tue, 22 Jul 2025 20:05:46 +0530 Subject: [PATCH 1/6] Add engine-eip7702-core package and update dependencies - Introduced the `engine-eip7702-core` package with its dependencies in `Cargo.lock` and `Cargo.toml`. - Updated the `executors` module to include `engine-eip7702-core` as a dependency. - Refactored execution options in `eip7702.rs` to support new execution models for EIP-7702. - Enhanced transaction handling in the `eip7702_executor` to accommodate new sender details and improve compatibility with existing job structures. - Updated webhook and job data structures to reflect changes in sender details and execution options. - Improved error handling and retry logic in job processing to enhance robustness. --- Cargo.lock | 15 + Cargo.toml | 1 + core/src/execution_options/eip7702.rs | 31 +- core/src/execution_options/mod.rs | 2 +- core/src/signer.rs | 2 +- eip7702-core/Cargo.toml | 14 + eip7702-core/src/constants.rs | 11 + eip7702-core/src/delegated_account.rs | 132 +++++++ eip7702-core/src/lib.rs | 3 + eip7702-core/src/transaction.rs | 232 +++++++++++ executors/Cargo.toml | 1 + executors/src/eip7702_executor/confirm.rs | 32 +- executors/src/eip7702_executor/send.rs | 459 ++++++++++------------ server/src/execution_router/mod.rs | 3 +- twmq/src/job.rs | 54 +++ 15 files changed, 726 insertions(+), 266 deletions(-) create mode 100644 eip7702-core/Cargo.toml create mode 100644 eip7702-core/src/constants.rs create mode 100644 eip7702-core/src/delegated_account.rs create mode 100644 eip7702-core/src/lib.rs create mode 100644 eip7702-core/src/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 4a739a5..8a3f692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2175,6 +2175,20 @@ dependencies = [ "vault-types", ] +[[package]] +name = "engine-eip7702-core" +version = "0.1.0" +dependencies = [ + "alloy", + "engine-core", + "rand 0.9.1", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", +] + [[package]] name = "engine-executors" version = "0.1.0" @@ -2184,6 +2198,7 @@ dependencies = [ "engine-aa-core", "engine-aa-types", "engine-core", + "engine-eip7702-core", "futures", "hex", "hmac", diff --git a/Cargo.toml b/Cargo.toml index ef7b824..72ea61b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "aa-types", "aa-core", "core", + "eip7702-core", "executors", "server", "thirdweb-core", diff --git a/core/src/execution_options/eip7702.rs b/core/src/execution_options/eip7702.rs index ab4a1cd..0746f53 100644 --- a/core/src/execution_options/eip7702.rs +++ b/core/src/execution_options/eip7702.rs @@ -1,15 +1,36 @@ use alloy::primitives::Address; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::defs::AddressDef; -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[schema(title = "EIP-7702 Execution Options")] +#[serde(rename_all = "camelCase", untagged)] +pub enum Eip7702ExecutionOptions { + /// Execute the transaction as the owner of the account + Owner(Eip7702OwnerExecution), + /// Execute a transaction on a different delegated account (`account_address`), which has granted a session key to the `session_key_address` + /// `session_key_address` is the signer for this transaction + SessionKey(Eip7702SessionKeyExecution), +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[schema(title = "EIP-7702 Owner Execution")] #[serde(rename_all = "camelCase")] -pub struct Eip7702ExecutionOptions { - /// The EOA address that will sign the EIP-7702 transaction - #[schemars(with = "AddressDef")] +pub struct Eip7702OwnerExecution { #[schema(value_type = AddressDef)] + /// The delegated EOA address pub from: Address, } + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[schema(title = "EIP-7702 Session Key Execution")] +#[serde(rename_all = "camelCase")] +pub struct Eip7702SessionKeyExecution { + #[schema(value_type = AddressDef)] + /// The session key address is your server wallet, which has been granted a session key to the `account_address` + pub session_key_address: Address, + #[schema(value_type = AddressDef)] + /// The account address is the address of a delegated account you want to execute the transaction on. This account has granted a session key to the `session_key_address` + pub account_address: Address, +} diff --git a/core/src/execution_options/mod.rs b/core/src/execution_options/mod.rs index 808a78f..7aaeaac 100644 --- a/core/src/execution_options/mod.rs +++ b/core/src/execution_options/mod.rs @@ -27,7 +27,7 @@ fn default_idempotency_key() -> String { } /// All supported specific execution options are contained here -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type")] #[schema(title = "Execution Option Variants")] pub enum SpecificExecutionOptions { diff --git a/core/src/signer.rs b/core/src/signer.rs index a15ba44..4698a19 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -338,7 +338,7 @@ impl AccountSigner for EoaSigner { } => { let iaw_result = self .iaw_client - .sign_transaction(auth_token, thirdweb_auth, &transaction) + .sign_transaction(auth_token, thirdweb_auth, transaction) .await .map_err(|e| { tracing::error!("Error signing transaction with EOA (IAW): {:?}", e); diff --git a/eip7702-core/Cargo.toml b/eip7702-core/Cargo.toml new file mode 100644 index 0000000..c838160 --- /dev/null +++ b/eip7702-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "engine-eip7702-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +alloy = { workspace = true, features = ["serde"] } +tokio = "1.44.2" +engine-core = { path = "../core" } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1.41" +rand = "0.9" +thiserror = "2.0" \ No newline at end of file diff --git a/eip7702-core/src/constants.rs b/eip7702-core/src/constants.rs new file mode 100644 index 0000000..24d1574 --- /dev/null +++ b/eip7702-core/src/constants.rs @@ -0,0 +1,11 @@ +use alloy::primitives::{Address, address}; + +/// The minimal account implementation address used for EIP-7702 delegation +pub const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address = + address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560"); + +/// EIP-7702 delegation prefix bytes +pub const EIP_7702_DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00]; + +/// EIP-7702 delegation code length (prefix + address) +pub const EIP_7702_DELEGATION_CODE_LENGTH: usize = 23; \ No newline at end of file diff --git a/eip7702-core/src/delegated_account.rs b/eip7702-core/src/delegated_account.rs new file mode 100644 index 0000000..fb57dfe --- /dev/null +++ b/eip7702-core/src/delegated_account.rs @@ -0,0 +1,132 @@ +use alloy::{ + primitives::{Address, FixedBytes}, + providers::Provider, +}; +use engine_core::{ + chain::Chain, + credentials::SigningCredential, + error::{AlloyRpcErrorToEngineError, EngineError, RpcErrorKind}, + signer::{AccountSigner, EoaSigner, EoaSigningOptions}, +}; +use rand::Rng; + +use crate::constants::{ + EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX, + MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, +}; + +/// Represents an EOA address that can have EIP-7702 delegation, associated with a specific chain +#[derive(Clone, Debug)] +pub struct DelegatedAccount { + /// The EOA address that may have delegation + pub eoa_address: Address, + /// The chain this account operates on + pub chain: C, +} + +impl DelegatedAccount { + /// Create a new delegated account from an EOA address and chain + pub fn new(eoa_address: Address, chain: C) -> Self { + Self { eoa_address, chain } + } + + /// Check if the EOA has EIP-7702 delegation to the minimal account implementation + pub async fn is_minimal_account(&self) -> Result { + // Get the bytecode at the EOA address using eth_getCode + let code = self + .chain + .provider() + .get_code_at(self.eoa_address) + .await + .map_err(|e| e.to_engine_error(self.chain()))?; + + tracing::debug!( + eoa_address = ?self.eoa_address, + code_length = code.len(), + code_hex = ?alloy::hex::encode(&code), + "Checking EIP-7702 delegation" + ); + + // Check if code exists and starts with EIP-7702 delegation prefix "0xef0100" + if code.len() < EIP_7702_DELEGATION_CODE_LENGTH + || !code.starts_with(&EIP_7702_DELEGATION_PREFIX) + { + tracing::debug!( + eoa_address = ?self.eoa_address, + has_delegation = false, + reason = "Code too short or doesn't start with EIP-7702 prefix", + "EIP-7702 delegation check result" + ); + return Ok(false); + } + + // Extract the target address from bytes 3-23 (20 bytes for address) + // EIP-7702 format: 0xef0100 + 20 bytes address + let target_bytes = &code[3..23]; + let target_address = Address::from_slice(target_bytes); + + // Compare with the minimal account implementation address + let is_delegated = target_address == MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS; + + tracing::debug!( + eoa_address = ?self.eoa_address, + target_address = ?target_address, + minimal_account_address = ?MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, + has_delegation = is_delegated, + "EIP-7702 delegation check result" + ); + + Ok(is_delegated) + } + + /// Get the EOA address + pub fn address(&self) -> Address { + self.eoa_address + } + + /// Get the current nonce for the EOA + pub async fn get_nonce(&self) -> Result { + self.chain + .provider() + .get_transaction_count(self.eoa_address) + .await + .map_err(|e| e.to_engine_error(self.chain())) + } + + /// Get a reference to the chain + pub fn chain(&self) -> &C { + &self.chain + } + + /// Sign authorization for EIP-7702 delegation (automatically fetches nonce) + pub async fn sign_authorization( + &self, + eoa_signer: &EoaSigner, + credentials: &SigningCredential, + ) -> Result { + let nonce = self.get_nonce().await?; + + let signing_options = EoaSigningOptions { + from: self.eoa_address, + chain_id: Some(self.chain.chain_id()), + }; + + eoa_signer + .sign_authorization( + signing_options, + self.chain.chain_id(), + MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, + nonce, + credentials, + ) + .await + } + + /// Generate a random UID for wrapped calls + pub fn generate_random_uid() -> FixedBytes<32> { + let mut rng = rand::rng(); + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes); + FixedBytes::from(bytes) + } +} diff --git a/eip7702-core/src/lib.rs b/eip7702-core/src/lib.rs new file mode 100644 index 0000000..dff5410 --- /dev/null +++ b/eip7702-core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod constants; +pub mod delegated_account; +pub mod transaction; \ No newline at end of file diff --git a/eip7702-core/src/transaction.rs b/eip7702-core/src/transaction.rs new file mode 100644 index 0000000..812fdfe --- /dev/null +++ b/eip7702-core/src/transaction.rs @@ -0,0 +1,232 @@ +use alloy::{ + dyn_abi::TypedData, + primitives::{Address, U256}, + sol, + sol_types::{SolCall, eip712_domain}, +}; +use engine_core::{ + chain::Chain, + credentials::SigningCredential, + error::{AlloyRpcErrorToEngineError, EngineError}, + signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + transaction::InnerTransaction, +}; +use serde_json::Value; + +use crate::delegated_account::DelegatedAccount; + +sol!( + #[derive(serde::Serialize)] + struct Call { + address target; + uint256 value; + bytes data; + } + + #[derive(serde::Serialize)] + struct WrappedCalls { + Call[] calls; + bytes32 uid; + } + + function execute(Call[] calldata calls) external payable; +); + +/// A transaction for a minimal account that supports signing and execution via bundler +pub struct MinimalAccountTransaction { + /// The delegated account this transaction belongs to + account: DelegatedAccount, + /// The raw transactions to be wrapped + wrapped_calls: WrappedCalls, + /// Authorization if needed for delegation setup + authorization: Option, +} + +impl DelegatedAccount { + /// Create a transaction for a session key address to execute on a target account + /// The session key address is the signer for this transaction, ie, they will sign the wrapped calls + /// The thirdweb executor is only responsible for calling executeWithSig + /// The flow is: + /// thirdweb executor -> executeWithSig(wrapped_calls) on the session key address + /// session key address -> execute(wrapped_calls.calls) on the target account + pub fn session_key_transaction( + self, + target_account: Address, + transactions: &[InnerTransaction], + ) -> MinimalAccountTransaction { + // First take all the inner transactions, and convert them to calls + // These are all the calls that the session key address wants to make on the target account + let inner_calls = transactions + .iter() + .map(|tx| Call { + target: tx.to.unwrap_or_default(), + value: tx.value, + data: tx.data.clone(), + }) + .collect(); + + // then get the calldata for calling the execute function (on the target account) with these calls + let outer_call = executeCall { calls: inner_calls }; + + // the session key address wants to call the execute function on the target account with these calls + let session_key_call = Call { + target: target_account, + value: U256::ZERO, + data: outer_call.abi_encode().into(), + }; + + // but the session key address still wants the "executor" (thirdweb bundler) to sponsor the transaction + // so the session key call is wrapped in a WrappedCalls struct + // the session key address is the signer for this transaction, ie, they will sign the wrapped calls + // the thirdweb executor is only responsible for calling executeWithSig + // the flow is: + // thirdweb executor -> executeWithSig(wrapped_calls) on the session key address + // session key address -> execute(wrapped_calls.calls) on the target account + let wrapped_calls = WrappedCalls { + calls: vec![session_key_call], + uid: Self::generate_random_uid(), + }; + + MinimalAccountTransaction { + account: self, + wrapped_calls, + authorization: None, + } + } + + pub fn owner_transaction( + self, + transactions: &[InnerTransaction], + ) -> MinimalAccountTransaction { + let inner_calls = transactions + .iter() + .map(|tx| Call { + target: tx.to.unwrap_or_default(), + value: tx.value, + data: tx.data.clone(), + }) + .collect(); + + let wrapped_calls = WrappedCalls { + calls: inner_calls, + uid: Self::generate_random_uid(), + }; + + MinimalAccountTransaction { + account: self, + wrapped_calls, + authorization: None, + } + } +} + +impl MinimalAccountTransaction { + /// Set the authorization for delegation setup + pub fn set_authorization(&mut self, authorization: alloy::eips::eip7702::SignedAuthorization) { + self.authorization = Some(authorization); + } + + pub async fn add_authorization_if_needed( + mut self, + signer: &EoaSigner, + credentials: &SigningCredential, + ) -> Result { + if self.account.is_minimal_account().await? { + return Ok(self); + } + + let authorization = self.account.sign_authorization(signer, credentials).await?; + self.authorization = Some(authorization); + Ok(self) + } + + /// Build the transaction data as JSON for bundler execution with automatic signing + pub async fn build( + &self, + eoa_signer: &EoaSigner, + credentials: &SigningCredential, + ) -> Result<(Value, String), EngineError> { + let signature = self.sign_wrapped_calls(eoa_signer, credentials).await?; + + // Serialize wrapped calls to JSON + let wrapped_calls_json = serde_json::to_value(&self.wrapped_calls).map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to serialize wrapped calls: {}", e), + } + })?; + + Ok((wrapped_calls_json, signature)) + } + + /// Execute the transaction directly via bundler client + /// This builds the transaction and calls tw_execute on the bundler + pub async fn execute( + &self, + eoa_signer: &EoaSigner, + credentials: &SigningCredential, + ) -> Result { + let (wrapped_calls_json, signature) = self.build(eoa_signer, credentials).await?; + + self.account + .chain() + .bundler_client() + .tw_execute( + self.account.address(), + &wrapped_calls_json, + &signature, + self.authorization.as_ref(), + ) + .await + .map_err(|e| e.to_engine_bundler_error(self.account.chain())) + } + + /// Get the account this transaction belongs to + pub fn account(&self) -> &DelegatedAccount { + &self.account + } + + /// Get the authorization if set + pub fn authorization(&self) -> Option<&alloy::eips::eip7702::SignedAuthorization> { + self.authorization.as_ref() + } + + pub async fn sign_wrapped_calls( + &self, + eoa_signer: &EoaSigner, + credentials: &SigningCredential, + ) -> Result { + let typed_data = self.create_wrapped_calls_typed_data(); + self.sign_typed_data(eoa_signer, credentials, &typed_data) + .await + } + + /// Sign typed data with EOA signer + async fn sign_typed_data( + &self, + eoa_signer: &EoaSigner, + credentials: &SigningCredential, + typed_data: &TypedData, + ) -> Result { + let signing_options = EoaSigningOptions { + from: self.account.address(), + chain_id: Some(self.account.chain().chain_id()), + }; + + eoa_signer + .sign_typed_data(signing_options, typed_data, credentials) + .await + } + + /// Create typed data for signing wrapped calls using Alloy's native types + fn create_wrapped_calls_typed_data(&self) -> TypedData { + let domain = eip712_domain! { + name: "MinimalAccount", + version: "1", + chain_id: self.account.chain().chain_id(), + verifying_contract: self.account.address(), + }; + + // Use Alloy's native TypedData creation from struct + TypedData::from_struct(&self.wrapped_calls, Some(domain)) + } +} diff --git a/executors/Cargo.toml b/executors/Cargo.toml index 283ef56..fc95d89 100644 --- a/executors/Cargo.toml +++ b/executors/Cargo.toml @@ -16,6 +16,7 @@ thiserror = "2.0.12" tracing = "0.1.41" twmq = { version = "0.1.0", path = "../twmq" } engine-aa-types = { version = "0.1.0", path = "../aa-types" } +engine-eip7702-core = { version = "0.1.0", path = "../eip7702-core" } engine-core = { version = "0.1.0", path = "../core" } engine-aa-core = { version = "0.1.0", path = "../aa-core" } rand = "0.9.1" diff --git a/executors/src/eip7702_executor/confirm.rs b/executors/src/eip7702_executor/confirm.rs index 27f2efc..9ca6f37 100644 --- a/executors/src/eip7702_executor/confirm.rs +++ b/executors/src/eip7702_executor/confirm.rs @@ -15,6 +15,7 @@ use twmq::{ job::{BorrowedJob, JobResult, RequeuePosition, ToJobError, ToJobResult}, }; +use crate::eip7702_executor::send::Eip7702Sender; use crate::{ transaction_registry::TransactionRegistry, webhook::{ @@ -30,7 +31,12 @@ pub struct Eip7702ConfirmationJobData { pub transaction_id: String, pub chain_id: u64, pub bundler_transaction_id: String, - pub eoa_address: Address, + /// ! Deprecated todo: remove this field after all jobs are processed + pub eoa_address: Option
, + + // TODO: make non-optional after all jobs are processed + pub sender_details: Option, + pub rpc_credentials: RpcCredentials, #[serde(default)] pub webhook_options: Vec, @@ -55,7 +61,9 @@ pub struct Eip7702ConfirmationResult { pub transaction_id: String, pub transaction_hash: TxHash, pub receipt: TransactionReceipt, - pub eoa_address: Address, + + #[serde(flatten)] + pub sender_details: Eip7702Sender, } // --- Error Types --- @@ -86,7 +94,7 @@ pub enum Eip7702ConfirmationError { #[error("Transaction failed: {message}")] TransactionFailed { message: String, - receipt: TransactionReceipt, + receipt: Box, }, #[error("Invalid RPC Credentials: {message}")] @@ -242,7 +250,7 @@ where if !success { return Err(Eip7702ConfirmationError::TransactionFailed { message: "Transaction reverted".to_string(), - receipt, + receipt: Box::new(receipt), }) .map_err_fail(); } @@ -254,11 +262,25 @@ where "Transaction confirmed successfully" ); + // todo: remove this after all jobs are processed + let sender_details = job_data + .sender_details + .clone() + .or_else(|| { + job_data + .eoa_address + .map(|eoa_address| Eip7702Sender::Owner { eoa_address }) + }) + .ok_or_else(|| Eip7702ConfirmationError::InternalError { + message: "No sender details found".to_string(), + }) + .map_err_fail()?; + Ok(Eip7702ConfirmationResult { transaction_id: job_data.transaction_id.clone(), transaction_hash, receipt, - eoa_address: job_data.eoa_address, + sender_details, }) } diff --git a/executors/src/eip7702_executor/send.rs b/executors/src/eip7702_executor/send.rs index 4fa9992..3588849 100644 --- a/executors/src/eip7702_executor/send.rs +++ b/executors/src/eip7702_executor/send.rs @@ -1,27 +1,24 @@ use alloy::{ - dyn_abi::TypedData, eips::eip7702::Authorization, - primitives::{Address, Bytes, ChainId, FixedBytes, U256, address}, - providers::Provider, - sol_types::eip712_domain, + primitives::{Address, U256}, }; use engine_core::{ chain::{Chain, ChainService, RpcCredentials}, credentials::SigningCredential, - error::{EngineError, RpcErrorKind}, - execution_options::WebhookOptions, - signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + error::{AlloyRpcErrorToEngineError, EngineError, RpcErrorKind}, + execution_options::{WebhookOptions, eip7702::Eip7702ExecutionOptions}, + signer::EoaSigner, transaction::InnerTransaction, }; -use rand::Rng; +use engine_eip7702_core::delegated_account::DelegatedAccount; use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -use std::sync::Arc; +use serde_json::Value; +use std::{sync::Arc, time::Duration}; use twmq::{ FailHookData, NackHookData, Queue, SuccessHookData, UserCancellable, error::TwmqError, hooks::TransactionContext, - job::{BorrowedJob, JobResult, ToJobResult}, + job::{BorrowedJob, JobResult, ToJobError, ToJobResult}, }; use crate::{ @@ -34,9 +31,6 @@ use crate::{ use super::confirm::{Eip7702ConfirmationHandler, Eip7702ConfirmationJobData}; -const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address = - address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560"); - // --- Job Payload --- #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] @@ -44,7 +38,19 @@ pub struct Eip7702SendJobData { pub transaction_id: String, pub chain_id: u64, pub transactions: Vec, - pub eoa_address: Address, + + // !IMPORTANT TODO + // To preserve backwards compatibility with pre-existing queued jobs, we continue keeping the eoa_address field until the next release + // However, we make it optional now, and rely on the Eip7702ExecutionOptions instead + pub eoa_address: Option
, + + // We must also keep the execution_options as optional to prevent deserialization errors + // when we remove the eoa_address field, we can make execution_options required + // at runtime we resolve from both, with preference to execution_options + // if both are none, we return an error + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_options: Option, + pub signing_credential: SigningCredential, #[serde(default)] pub webhook_options: Vec, @@ -68,11 +74,25 @@ impl HasTransactionMetadata for Eip7702SendJobData { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Eip7702SendResult { - pub eoa_address: Address, pub transaction_id: String, pub wrapped_calls: Value, pub signature: String, pub authorization: Option, + + #[serde(flatten)] + pub sender_details: Eip7702Sender, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum Eip7702Sender { + #[serde(rename_all = "camelCase")] + Owner { eoa_address: Address }, + #[serde(rename_all = "camelCase")] + SessionKey { + session_key_address: Address, + account_address: Address, + }, } // --- Error Types --- @@ -80,31 +100,20 @@ pub struct Eip7702SendResult { #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "errorCode")] pub enum Eip7702SendError { #[error("Chain service error for chainId {chain_id}: {message}")] + #[serde(rename_all = "camelCase")] ChainServiceError { chain_id: u64, message: String }, - #[error("Failed to sign typed data: {message}")] + #[error("Failed to sign authorization or wrapped calls: {inner_error}")] #[serde(rename_all = "camelCase")] - SigningError { - message: String, - inner_error: Option, - }, + SigningFailed { inner_error: EngineError }, - #[error("Failed to check 7702 delegation: {message}")] + #[error("Failed to check delegation or add authorization: {inner_error}")] #[serde(rename_all = "camelCase")] - DelegationCheckError { - message: String, - inner_error: Option, - }, + DelegationCheckFailed { inner_error: EngineError }, - #[error("Failed to fetch nonce: {message}")] + #[error("Failed to call bundler: {inner_error}")] #[serde(rename_all = "camelCase")] - NonceFetchError { - message: String, - inner_error: Option, - }, - - #[error("Failed to call bundler: {message}")] - BundlerCallError { message: String }, + BundlerCallFailed { inner_error: EngineError }, #[error("Invalid RPC Credentials: {message}")] InvalidRpcCredentials { message: String }, @@ -130,22 +139,6 @@ impl UserCancellable for Eip7702SendError { } } -// --- Wrapped Calls Structure --- -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Call { - pub target: Address, - pub value: U256, - pub data: Bytes, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WrappedCalls { - pub calls: Vec, - pub uid: FixedBytes<32>, -} - // --- Handler --- pub struct Eip7702SendHandler where @@ -215,122 +208,129 @@ where let chain = chain.with_new_default_headers(chain_auth_headers); - // 2. Create wrapped calls with random UID - let wrapped_calls = WrappedCalls { - calls: job_data - .transactions - .iter() - .map(|tx| Call { - target: tx.to.unwrap_or_default(), - value: tx.value, - data: tx.data.clone(), - }) - .collect(), - uid: { - let mut rng = rand::rng(); - let mut bytes = [0u8; 32]; - rng.fill(&mut bytes); - FixedBytes::from(bytes) - }, - }; + let owner_address = job_data + .eoa_address + .or(job_data.execution_options.as_ref().map(|e| match e { + Eip7702ExecutionOptions::Owner(o) => o.from, + Eip7702ExecutionOptions::SessionKey(s) => s.session_key_address, + })) + .ok_or(Eip7702SendError::InternalError { + message: "No owner address found".to_string(), + }) + .map_err_fail()?; - // 3. Sign typed data for wrapped calls - let typed_data = create_wrapped_calls_typed_data( - job_data.chain_id, - job_data.eoa_address, - &wrapped_calls, - ); + let account = DelegatedAccount::new(owner_address, chain); + + let session_key_target_address = + job_data.execution_options.as_ref().and_then(|e| match e { + Eip7702ExecutionOptions::Owner(_) => None, + Eip7702ExecutionOptions::SessionKey(s) => Some(s.account_address), + }); - let signing_options = EoaSigningOptions { - from: job_data.eoa_address, - chain_id: Some(ChainId::from(job_data.chain_id)), + let mut transactions = match session_key_target_address { + Some(target_address) => { + account.session_key_transaction(target_address, &job_data.transactions) + } + None => account.owner_transaction(&job_data.transactions), }; - let signature = self - .eoa_signer - .sign_typed_data( - signing_options.clone(), - &typed_data, - &job_data.signing_credential, - ) + let is_authorization_needed = !transactions + .account() + .is_minimal_account() .await - .map_err(|e| Eip7702SendError::SigningError { - message: format!("Failed to sign typed data: {e}"), - inner_error: Some(e), - }) - .map_err_fail()?; + .map_err(|e| Eip7702SendError::DelegationCheckFailed { inner_error: e }) + .map_err_with_max_retries( + Some(Duration::from_secs(2)), + twmq::job::RequeuePosition::Last, + 3, + job.attempts(), + )?; + + if is_authorization_needed { + let authorization = transactions + .account() + .sign_authorization(&self.eoa_signer, &job_data.signing_credential) + .await + .map_err(|e| { + let mapped_error = Eip7702SendError::SigningFailed { + inner_error: e.clone(), + }; + + if is_build_error_retryable(&e) { + mapped_error.nack_with_max_retries( + Some(Duration::from_secs(2)), + twmq::job::RequeuePosition::Last, + 3, + job.attempts(), + ) + } else { + mapped_error.fail() + } + })?; + + transactions.set_authorization(authorization); + } - // 4. Check if wallet has 7702 delegation set - let is_minimal_account = check_is_7702_minimal_account(&chain, job_data.eoa_address) + let (wrapped_calls, signature) = transactions + .build(&self.eoa_signer, &job_data.signing_credential) .await - .map_err(|e| Eip7702SendError::DelegationCheckError { - message: format!("Failed to check if wallet has 7702 delegation: {e}"), - inner_error: Some(e), - }) + .map_err(|e| Eip7702SendError::SigningFailed { inner_error: e }) .map_err_fail()?; - // 5. Sign authorization if needed - let authorization = if !is_minimal_account { - let nonce = get_eoa_nonce(&chain, job_data.eoa_address) - .await - .map_err(|e| Eip7702SendError::NonceFetchError { - message: format!("Failed to fetch nonce: {e}"), - inner_error: Some(e), - }) - .map_err_fail()?; - - let auth = self - .eoa_signer - .sign_authorization( - signing_options.clone(), - job_data.chain_id, - MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, - nonce, - &job_data.signing_credential, - ) - .await - .map_err(|e| Eip7702SendError::SigningError { - message: format!("Failed to sign authorization: {e}"), - inner_error: Some(e), - }) - .map_err_fail()?; - - Some(auth.clone()) - } else { - None - }; - - // 6. Call bundler - let transaction_id = chain + let transaction_id = transactions + .account() + .chain() .bundler_client() .tw_execute( - job_data.eoa_address, - &serde_json::to_value(&wrapped_calls) - .map_err(|e| Eip7702SendError::InternalError { - message: format!("Failed to serialize wrapped calls: {}", e), - }) - .map_err_fail()?, + owner_address, + &wrapped_calls, &signature, - authorization.as_ref(), + transactions.authorization(), ) .await - .map_err(|e| Eip7702SendError::BundlerCallError { - message: e.to_string(), - }) - .map_err_fail()?; + .map_err(|e| { + let engine_error = e.to_engine_bundler_error(transactions.account().chain()); + let wrapped_error = Eip7702SendError::BundlerCallFailed { + inner_error: engine_error.clone(), + }; + if let EngineError::BundlerError { kind, .. } = &engine_error { + if is_retryable_rpc_error(kind) { + wrapped_error.nack_with_max_retries( + Some(Duration::from_secs(2)), + twmq::job::RequeuePosition::Last, + 3, + job.attempts(), + ) + } else { + wrapped_error.fail() + } + } else { + wrapped_error.fail() + } + })?; tracing::debug!(transaction_id = ?transaction_id, "EIP-7702 transaction sent to bundler"); + let sender_details = match session_key_target_address { + Some(target_address) => Eip7702Sender::SessionKey { + session_key_address: owner_address, + account_address: target_address, + }, + None => Eip7702Sender::Owner { + eoa_address: owner_address, + }, + }; + Ok(Eip7702SendResult { - eoa_address: job_data.eoa_address, - transaction_id, + sender_details, + transaction_id: transaction_id.clone(), wrapped_calls: serde_json::to_value(&wrapped_calls) .map_err(|e| Eip7702SendError::InternalError { message: format!("Failed to serialize wrapped calls: {}", e), }) .map_err_fail()?, signature, - authorization: authorization.map(|f| f.inner().clone()), + authorization: transactions.authorization().map(|f| f.inner().clone()), }) } @@ -355,7 +355,8 @@ where transaction_id: job.job.data.transaction_id.clone(), chain_id: job.job.data.chain_id, bundler_transaction_id: success_data.result.transaction_id.clone(), - eoa_address: success_data.result.eoa_address, + eoa_address: None, + sender_details: Some(success_data.result.sender_details.clone()), rpc_credentials: job.job.data.rpc_credentials.clone(), webhook_options: job.job.data.webhook_options.clone(), }) @@ -421,121 +422,73 @@ where } } -// --- Helper Functions --- - -fn create_wrapped_calls_typed_data( - chain_id: u64, - verifying_contract: Address, - wrapped_calls: &WrappedCalls, -) -> TypedData { - let domain = eip712_domain! { - name: "MinimalAccount", - version: "1", - chain_id: chain_id, - verifying_contract: verifying_contract, - }; - - let types_json = json!({ - "Call": [ - {"name": "target", "type": "address"}, - {"name": "value", "type": "uint256"}, - {"name": "data", "type": "bytes"} - ], - "WrappedCalls": [ - {"name": "calls", "type": "Call[]"}, - {"name": "uid", "type": "bytes32"} - ] - }); - - let message = json!({ - "calls": wrapped_calls.calls, - "uid": wrapped_calls.uid - }); - - // Parse the JSON into Eip712Types and create resolver - let eip712_types: alloy::dyn_abi::eip712::Eip712Types = - serde_json::from_value(types_json).expect("Failed to parse EIP712 types"); - - TypedData { - domain, - resolver: eip712_types.into(), - primary_type: "WrappedCalls".to_string(), - message, +/// Determines if an error should be retried based on its type and content +fn is_build_error_retryable(e: &EngineError) -> bool { + match e { + // Standard RPC errors - don't retry client errors (4xx) + EngineError::RpcError { kind, .. } => !is_client_error(kind), + + // Paymaster and Bundler errors - more restrictive retry policy + EngineError::PaymasterError { kind, .. } | EngineError::BundlerError { kind, .. } => { + is_retryable_rpc_error(kind) + } + + // Vault errors are never retryable (auth/encryption issues) + EngineError::VaultError { .. } => false, + + // All other errors are not retryable by default + _ => false, + } +} + +/// Check if an RPC error represents a client error (4xx) that shouldn't be retried +fn is_client_error(kind: &RpcErrorKind) -> bool { + match kind { + RpcErrorKind::TransportHttpError { status, .. } if *status >= 400 && *status < 500 => true, + RpcErrorKind::UnsupportedFeature { .. } => true, + _ => false, } } -async fn check_is_7702_minimal_account( - chain: &impl Chain, - eoa_address: Address, -) -> Result { - // Get the bytecode at the EOA address using eth_getCode - let code = chain - .provider() - .get_code_at(eoa_address) - .await - .map_err(|e| EngineError::RpcError { - chain_id: chain.chain_id(), - rpc_url: chain.rpc_url().to_string(), - message: format!("Failed to get code at address {}: {}", eoa_address, e), - kind: RpcErrorKind::InternalError { - message: e.to_string(), - }, - })?; - - tracing::debug!( - eoa_address = ?eoa_address, - code_length = code.len(), - code_hex = ?alloy::hex::encode(&code), - "Checking EIP-7702 delegation" - ); - - // Check if code exists and starts with EIP-7702 delegation prefix "0xef0100" - if code.len() < 23 || !code.starts_with(&[0xef, 0x01, 0x00]) { - tracing::debug!( - eoa_address = ?eoa_address, - has_delegation = false, - reason = "Code too short or doesn't start with EIP-7702 prefix", - "EIP-7702 delegation check result" - ); - return Ok(false); +/// Determine if an RPC error from paymaster/bundler should be retried +fn is_retryable_rpc_error(kind: &RpcErrorKind) -> bool { + match kind { + // Don't retry client errors (4xx) + RpcErrorKind::TransportHttpError { status, .. } if *status >= 400 && *status < 500 => false, + + // Don't retry 500 errors that contain revert data (contract reverts) + RpcErrorKind::TransportHttpError { status: 500, body } => !contains_revert_data(body), + + // Don't retry specific JSON-RPC error codes + RpcErrorKind::ErrorResp(resp) if is_non_retryable_rpc_code(resp.code) => false, + + // Retry other errors (network issues, temporary server errors, etc.) + _ => true, } +} - // Extract the target address from bytes 3-23 (20 bytes for address) - // EIP-7702 format: 0xef0100 + 20 bytes address - // JS equivalent: code.slice(8, 48) extracts 40 hex chars = 20 bytes - // In hex string: "0xef0100" + address, so address starts at position 8 - // In byte array: [0xef, 0x01, 0x00, address_bytes...] - // The address starts at byte 3 and is 20 bytes long (bytes 3-22) - let target_bytes = &code[3..23]; - let target_address = Address::from_slice(target_bytes); - - // Compare with the minimal account implementation address - let minimal_account_address: Address = MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS; - - let is_delegated = target_address == minimal_account_address; - - tracing::debug!( - eoa_address = ?eoa_address, - target_address = ?target_address, - minimal_account_address = ?minimal_account_address, - has_delegation = is_delegated, - "EIP-7702 delegation check result" - ); - - Ok(is_delegated) +/// Check if the error body contains revert data indicating a contract revert +fn contains_revert_data(body: &str) -> bool { + // Common revert selectors that indicate contract execution failures + const REVERT_SELECTORS: &[&str] = &[ + "0x08c379a0", // Error(string) + "0x4e487b71", // Panic(uint256) + ]; + + // Check if body contains any revert selectors + REVERT_SELECTORS.iter().any(|selector| body.contains(selector)) || + // Also check for common revert-related keywords + body.contains("reverted during simulation") || + body.contains("execution reverted") || + body.contains("UserOperation reverted") } -async fn get_eoa_nonce(chain: &impl Chain, eoa_address: Address) -> Result { - chain - .provider() - .get_transaction_count(eoa_address) - .await - .map_err(|e| EngineError::RpcError { - chain_id: chain.chain_id(), - rpc_url: chain.rpc_url().to_string(), - message: format!("Failed to get nonce for address {}: {}", eoa_address, e), - kind: RpcErrorKind::InternalError { - message: e.to_string(), - }, - }) +/// Check if an RPC error code should not be retried +fn is_non_retryable_rpc_code(code: i64) -> bool { + match code { + -32000 => true, // Invalid input / execution error + -32001 => true, // Chain does not exist / invalid chain + -32603 => true, // Internal error (often indicates invalid params) + _ => false, + } } diff --git a/server/src/execution_router/mod.rs b/server/src/execution_router/mod.rs index 8d35986..b7eb5dc 100644 --- a/server/src/execution_router/mod.rs +++ b/server/src/execution_router/mod.rs @@ -350,7 +350,8 @@ impl ExecutionRouter { transaction_id: base_execution_options.idempotency_key.clone(), chain_id: base_execution_options.chain_id, transactions: transactions.to_vec(), - eoa_address: eip7702_execution_options.from, + eoa_address: None, + execution_options: Some(eip7702_execution_options.clone()), signing_credential, webhook_options, rpc_credentials, diff --git a/twmq/src/job.rs b/twmq/src/job.rs index afc382b..42c5b66 100644 --- a/twmq/src/job.rs +++ b/twmq/src/job.rs @@ -54,6 +54,14 @@ pub enum JobError { pub trait ToJobResult { fn map_err_nack(self, delay: Option, position: RequeuePosition) -> JobResult; fn map_err_fail(self) -> JobResult; + + fn map_err_with_max_retries( + self, + delay: Option, + position: RequeuePosition, + max_retries: u32, + current_attempts: u32, + ) -> JobResult; } impl ToJobResult for Result @@ -71,11 +79,39 @@ where fn map_err_fail(self) -> JobResult { self.map_err(|e| JobError::Fail(e.into())) } + + fn map_err_with_max_retries( + self, + delay: Option, + position: RequeuePosition, + max_retries: u32, + current_attempts: u32, + ) -> JobResult { + self.map_err(|e| { + if current_attempts >= max_retries { + JobError::Fail(e.into()) + } else { + JobError::Nack { + error: e.into(), + delay, + position, + } + } + }) + } } pub trait ToJobError { fn nack(self, delay: Option, position: RequeuePosition) -> JobError; fn fail(self) -> JobError; + + fn nack_with_max_retries( + self, + delay: Option, + position: RequeuePosition, + max_retries: u32, + current_attempts: u32, + ) -> JobError; } impl ToJobError for E { @@ -90,6 +126,24 @@ impl ToJobError for E { fn fail(self) -> JobError { JobError::Fail(self) } + + fn nack_with_max_retries( + self, + delay: Option, + position: RequeuePosition, + max_retries: u32, + current_attempts: u32, + ) -> JobError { + if current_attempts >= max_retries { + JobError::Fail(self) + } else { + JobError::Nack { + error: self, + delay, + position, + } + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] From 509e657f373569722075b3033c063da18ee1158c Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Tue, 22 Jul 2025 20:06:55 +0530 Subject: [PATCH 2/6] clippy --- eip7702-core/src/delegated_account.rs | 2 +- executors/src/eoa/error_classifier.rs | 2 +- thirdweb-core/src/iaw/mod.rs | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/eip7702-core/src/delegated_account.rs b/eip7702-core/src/delegated_account.rs index fb57dfe..097d9b3 100644 --- a/eip7702-core/src/delegated_account.rs +++ b/eip7702-core/src/delegated_account.rs @@ -5,7 +5,7 @@ use alloy::{ use engine_core::{ chain::Chain, credentials::SigningCredential, - error::{AlloyRpcErrorToEngineError, EngineError, RpcErrorKind}, + error::{AlloyRpcErrorToEngineError, EngineError}, signer::{AccountSigner, EoaSigner, EoaSigningOptions}, }; use rand::Rng; diff --git a/executors/src/eoa/error_classifier.rs b/executors/src/eoa/error_classifier.rs index 8dd1c80..e20f76b 100644 --- a/executors/src/eoa/error_classifier.rs +++ b/executors/src/eoa/error_classifier.rs @@ -242,7 +242,7 @@ impl EoaExecutionError { success_factory: impl FnOnce() -> T, error_factory: impl FnOnce(String) -> E, ) -> twmq::job::JobResult { - use twmq::job::{ToJobError, ToJobResult}; + use twmq::job::ToJobError; if strategy.queue_confirmation { // Treat as success since we need to check confirmation diff --git a/thirdweb-core/src/iaw/mod.rs b/thirdweb-core/src/iaw/mod.rs index 84b3617..d12bddf 100644 --- a/thirdweb-core/src/iaw/mod.rs +++ b/thirdweb-core/src/iaw/mod.rs @@ -68,16 +68,13 @@ impl From for IAWError { /// Message format for signing operations #[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Default)] pub enum MessageFormat { + #[default] Text, Hex, } -impl Default for MessageFormat { - fn default() -> Self { - MessageFormat::Text - } -} /// Response data for message signing operations #[derive(Debug, Clone, Serialize, Deserialize)] From 6bcbafcf186a853034a3e4120a6aeaed7a882629 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Wed, 23 Jul 2025 04:31:50 +0530 Subject: [PATCH 3/6] Update dependencies and implement AWS KMS signing support - Updated `Cargo.lock` to reflect new versions for several dependencies, including `alloy-consensus`, `alloy-eips`, and AWS-related packages. - Introduced `AwsKmsCredential` for AWS KMS signing, allowing integration of AWS KMS into the signing process. - Enhanced `SigningCredential` enum to support AWS KMS credentials. - Implemented error handling for AWS KMS signing errors in the `EngineError` enum. - Updated user operation signing logic to utilize AWS KMS for signing user operations. - Refactored HTTP extractors to support AWS KMS credentials extraction from headers. These changes improve the integration of AWS KMS for signing operations, enhancing the overall functionality and security of the application. --- Cargo.lock | 607 +++++++++++++++++++++++++--- aa-types/src/userop.rs | 36 +- core/Cargo.toml | 4 + core/src/constants.rs | 4 - core/src/credentials.rs | 55 ++- core/src/error.rs | 143 +++++++ core/src/execution_options/aa.rs | 12 +- core/src/signer.rs | 88 +++- core/src/userop.rs | 26 +- executors/src/eoa/store/atomic.rs | 2 +- executors/src/eoa/worker/confirm.rs | 6 +- server/Cargo.toml | 1 + server/src/http/error.rs | 3 +- server/src/http/extractors.rs | 164 ++++++-- 14 files changed, 1021 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a3f692..9225ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c6ad411efe0f49e0e99b9c7d8749a1eb55f6dbf74a1bc6953ab285b02c4f67" +checksum = "1b6093bc69509849435a2d68237a2e9fea79d27390c8e62f1e4012c460aabad8" dependencies = [ "alloy-eips", "alloy-primitives", @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf397edad57b696501702d5887e4e14d7d0bbae9fbb6439e148d361f7254f45" +checksum = "8d1cfed4fefd13b5620cb81cdb6ba397866ff0de514c1b24806e6e79cdff5570" dependencies = [ "alloy-consensus", "alloy-eips", @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749b8449e4daf7359bdf1dabdba6ce424ff8b1bdc23bdb795661b2e991a08d87" +checksum = "5937e2d544e9b71000942d875cbc57965b32859a666ea543cc57aae5a06d602d" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc30b0e20fcd0843834ecad2a716661c7b9d5aca2486f8e96b93d5246eb83e06" +checksum = "b590caa6b6d8bc10e6e7a7696c59b1e550e89f27f50d1ee13071150d3a3e3f66" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaeb681024cf71f5ca14f3d812c0a8d8b49f13f7124713538e66d74d3bfe6aff" +checksum = "36fe5af1fca03277daa56ad4ce5f6d623d3f4c2273ea30b9ee8674d18cefc1fa" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03ad273e1c55cc481889b4130e82860e33624e6969e9a08854e0f3ebe659295" +checksum = "793df1e3457573877fbde8872e4906638fde565ee2d3bd16d04aad17d43dbf0e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -360,7 +360,7 @@ dependencies = [ "proptest", "rand 0.9.1", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -467,9 +467,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5b22062142ce3b2ed3374337d4b343437e5de6959397f55d2c9fe2c2ce0162" +checksum = "1e26b4dd90b33bd158975307fb9cf5fafa737a0e33cbb772a8648bf8be13c104" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -478,9 +478,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391e59f81bacbffc7bddd2da3a26d6eec0e2058e9237c279e9b1052bdf21b49e" +checksum = "46586ec3c278639fc0e129f0eb73dbfa3d57f683c44b2ff5e066fab7ba63fa1f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -493,14 +493,15 @@ dependencies = [ "itertools 0.14.0", "serde", "serde_json", + "serde_with", "thiserror 2.0.12", ] [[package]] name = "alloy-serde" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea08bc854235d4dff08fd57df8033285c11b8d7548b20c6da218194e7e6035f" +checksum = "1e1722bc30feef87cc0fa824e43c9013f9639cc6c037be7be28a31361c788be2" dependencies = [ "alloy-primitives", "serde", @@ -509,9 +510,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb3759f85ef5f010a874d9ebd5ee6ce01cac65211510863124e0ebac6552db0" +checksum = "d3674beb29e68fbbc7be302b611cf35fe07b736e308012a280861df5a2361395" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -526,9 +527,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7942b850ec7be43de89b2680321d7921b7620b25be53b9981aae6fb29daa9e97" +checksum = "605b1659b320b16708bb84b41038b2f0e2a60d90972c28319c4f5a4866f0efd4" dependencies = [ "alloy-consensus", "alloy-network", @@ -725,9 +726,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79bf2869e66904b2148c809e7a75e23ca26f5d7b46663a149a1444fb98a69d1d" +checksum = "9f916ff6d52f219c44a9684aea764ce2c7e1d53bd4a724c9b127863aeacc30bb" dependencies = [ "alloy-primitives", "darling", @@ -994,11 +995,52 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-arn" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da0fae6942d45915eebb3c2cfad48c5359d969a58b24696c71baf6a54489a98" +dependencies = [ + "lazy_static", + "regex", + "serde", +] + +[[package]] +name = "aws-config" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd9b83179adf8998576317ce47785948bcff399ec5b15f4dfbdedd44ddf5b92" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + [[package]] name = "aws-credential-types" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465" +checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1006,11 +1048,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "aws-runtime" -version = "1.5.8" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2" +checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1032,9 +1097,53 @@ dependencies = [ [[package]] name = "aws-sdk-kms" +version = "1.79.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5603bd5e0487e90acdef4a9be019f55c841e8eb72d3cb2e88c1c112c67a59db" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.76.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bf26698dd6d238ef1486bdda46f22a589dc813368ba868dc3d94c8d27b56ba" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" version = "1.77.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd57d0c1a5bd6c7eaa2b26462e046d5ca7b72189346718d2435dfc48bfa988b" +checksum = "09cd07ed1edd939fae854a22054299ae3576500f4e0fadc560ca44f9c6ea1664" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1052,6 +1161,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-sts" +version = "1.78.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37f7766d2344f56d10d12f3c32993da36d78217f32594fe4fb8e57a538c1cdea" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sigv4" version = "1.3.3" @@ -1105,6 +1237,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-http-client" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.11", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.6.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tower", + "tracing", +] + [[package]] name = "aws-smithy-json" version = "0.61.4" @@ -1123,14 +1284,25 @@ dependencies = [ "aws-smithy-runtime-api", ] +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + [[package]] name = "aws-smithy-runtime" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e" +checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1148,9 +1320,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8531b6d8882fd8f48f82a9754e682e29dd44cff27154af51fa3eb730f59efb" +checksum = "38280ac228bc479f347fcfccf4bf4d22d68f3bb4629685cb591cabd856567bbc" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1172,6 +1344,7 @@ dependencies = [ "base64-simd", "bytes", "bytes-utils", + "futures-core", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", @@ -1184,6 +1357,17 @@ dependencies = [ "ryu", "serde", "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +dependencies = [ + "xmlparser", ] [[package]] @@ -1214,7 +1398,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "itoa", "matchit", @@ -1344,6 +1528,29 @@ dependencies = [ "virtue", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.104", + "which", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1487,9 +1694,20 @@ version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -1579,6 +1797,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.40" @@ -1604,6 +1833,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "coins-ledger" version = "0.12.0" @@ -2159,6 +2397,10 @@ name = "engine-core" version = "0.1.0" dependencies = [ "alloy", + "alloy-signer-aws", + "aws-config", + "aws-credential-types", + "aws-sdk-kms", "engine-aa-types", "schemars 0.8.22", "serde", @@ -2354,6 +2596,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2465,7 +2713,7 @@ dependencies = [ "bytes", "chrono", "futures", - "hyper", + "hyper 1.6.0", "jsonwebtoken", "once_cell", "prost", @@ -2558,6 +2806,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.11" @@ -2680,6 +2947,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -2748,6 +3024,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2757,7 +3057,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2769,6 +3069,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -2776,13 +3092,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper", + "hyper 1.6.0", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots", ] @@ -2793,7 +3109,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2808,7 +3124,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -2829,7 +3145,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper", + "hyper 1.6.0", "ipnet", "libc", "percent-encoding", @@ -3090,6 +3406,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -3165,12 +3491,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + [[package]] name = "libm" version = "0.2.15" @@ -3189,6 +3531,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -3302,6 +3650,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3383,6 +3737,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3840,6 +4204,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn 2.0.104", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3960,8 +4334,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls", + "rustc-hash 2.1.1", + "rustls 0.23.28", "socket2", "thiserror 2.0.12", "tokio", @@ -3980,8 +4354,8 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", - "rustls", + "rustc-hash 2.1.1", + "rustls 0.23.28", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4232,12 +4606,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.6.0", + "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", "js-sys", @@ -4248,8 +4622,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", "serde_json", @@ -4257,7 +4631,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower", "tower-http", @@ -4375,6 +4749,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4405,6 +4785,19 @@ dependencies = [ "semver 1.0.26", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -4414,25 +4807,50 @@ dependencies = [ "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4445,6 +4863,15 @@ dependencies = [ "security-framework 3.2.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4455,12 +4882,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4569,6 +5007,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -5076,7 +5524,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -5104,6 +5552,7 @@ dependencies = [ "aide", "alloy", "anyhow", + "aws-arn", "axum", "config", "engine-aa-core", @@ -5303,13 +5752,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.28", "tokio", ] @@ -5382,20 +5841,20 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project 1.1.10", "prost", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "socket2", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-stream", "tower", "tower-layer", @@ -5663,6 +6122,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5965,6 +6430,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6318,6 +6795,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust2" version = "0.10.3" diff --git a/aa-types/src/userop.rs b/aa-types/src/userop.rs index 7e51133..16a9d46 100644 --- a/aa-types/src/userop.rs +++ b/aa-types/src/userop.rs @@ -1,6 +1,6 @@ use alloy::{ core::sol_types::SolValue, - primitives::{Address, B256, Bytes, ChainId, U256, keccak256}, + primitives::{Address, B256, Bytes, ChainId, U256, address, keccak256}, rpc::types::{PackedUserOperation, UserOperation}, }; use serde::{Deserialize, Serialize}; @@ -13,6 +13,9 @@ pub enum VersionedUserOp { V0_7(PackedUserOperation), } +pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6 +pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7 + /// Error type for UserOp operations #[derive( Debug, @@ -182,3 +185,34 @@ pub fn compute_user_op_v07_hash( let final_hash = keccak256(&outer_encoded); Ok(final_hash) } + +impl VersionedUserOp { + pub fn hash(&self, chain_id: ChainId) -> Result { + match self { + VersionedUserOp::V0_6(op) => { + compute_user_op_v06_hash(op, ENTRYPOINT_ADDRESS_V0_6, chain_id) + } + VersionedUserOp::V0_7(op) => { + compute_user_op_v07_hash(op, ENTRYPOINT_ADDRESS_V0_7, chain_id) + } + } + } + + pub fn hash_with_custom_entrypoint( + &self, + chain_id: ChainId, + entrypoint: Address, + ) -> Result { + match self { + VersionedUserOp::V0_6(op) => compute_user_op_v06_hash(op, entrypoint, chain_id), + VersionedUserOp::V0_7(op) => compute_user_op_v07_hash(op, entrypoint, chain_id), + } + } + + pub fn default_entrypoint(&self) -> Address { + match self { + VersionedUserOp::V0_6(_) => ENTRYPOINT_ADDRESS_V0_6, + VersionedUserOp::V0_7(_) => ENTRYPOINT_ADDRESS_V0_7, + } + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 5427590..5594326 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,3 +19,7 @@ thirdweb-core = { version = "0.1.0", path = "../thirdweb-core" } uuid = { version = "1.17.0", features = ["v4"] } utoipa = { version = "5.4.0", features = ["preserve_order"] } serde_with = "3.13.0" +alloy-signer-aws = { version = "1.0.23", features = ["eip712"] } +aws-config = "1.8.2" +aws-sdk-kms = "1.79.0" +aws-credential-types = "1.2.4" diff --git a/core/src/constants.rs b/core/src/constants.rs index bb799c6..87d9cc9 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -11,7 +11,3 @@ pub const DEFAULT_FACTORY_ADDRESS_V0_6: Address = pub const DEFAULT_IMPLEMENTATION_ADDRESS_V0_6: Address = address!("0xf22175c80c6e074C171811C59C6c0087e2a6a346"); - -pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6 - -pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7 diff --git a/core/src/credentials.rs b/core/src/credentials.rs index ff311fd..c7b000b 100644 --- a/core/src/credentials.rs +++ b/core/src/credentials.rs @@ -1,13 +1,58 @@ +use alloy::primitives::ChainId; +use alloy_signer_aws::AwsSigner; +use aws_config::BehaviorVersion; +use aws_credential_types::provider::future::ProvideCredentials as ProvideCredentialsFuture; +use aws_sdk_kms::config::{Credentials, ProvideCredentials}; use serde::{Deserialize, Serialize}; use thirdweb_core::auth::ThirdwebAuth; use thirdweb_core::iaw::AuthToken; -use vault_types::enclave::auth::Auth; +use vault_types::enclave::auth::Auth as VaultAuth; + +use crate::error::EngineError; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SigningCredential { - Vault(Auth), - Iaw { - auth_token: AuthToken, - thirdweb_auth: ThirdwebAuth + Vault(VaultAuth), + Iaw { + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, }, + AwsKms(AwsKmsCredential), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AwsKmsCredential { + pub access_key_id: String, + pub secret_access_key: String, + pub key_id: String, + pub region: String, +} + +impl ProvideCredentials for AwsKmsCredential { + fn provide_credentials<'a>(&'a self) -> ProvideCredentialsFuture<'a> + where + Self: 'a, + { + let credentials = Credentials::new( + self.access_key_id.clone(), + self.secret_access_key.clone(), + None, + None, + "engine-core", + ); + ProvideCredentialsFuture::ready(Ok(credentials)) + } +} + +impl AwsKmsCredential { + pub async fn get_signer(&self, chain_id: Option) -> Result { + let config = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(self.clone()) + .load() + .await; + let client = aws_sdk_kms::Client::new(&config); + + let signer = AwsSigner::new(client, self.key_id.clone(), chain_id).await?; + Ok(signer) + } } diff --git a/core/src/error.rs b/core/src/error.rs index d3ef448..ae808f0 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::defs::AddressDef; use alloy::{ primitives::Address, @@ -5,6 +7,8 @@ use alloy::{ RpcError as AlloyRpcError, TransportErrorKind, http::reqwest::header::InvalidHeaderValue, }, }; +use alloy_signer_aws::AwsSignerError; +use aws_sdk_kms::error::SdkError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thirdweb_core::error::ThirdwebError; @@ -242,11 +246,150 @@ pub enum EngineError { #[error("Thirdweb error: {message}")] ThirdwebError { message: String }, + #[schema(title = "AWS KMS Error")] + #[error(transparent)] + #[serde(rename_all = "camelCase")] + AwsKmsSignerError { + #[serde(flatten)] + error: SerialisableAwsSignerError, + }, + #[schema(title = "Engine Internal Error")] #[error("Internal error: {message}")] InternalError { message: String }, } +#[derive(thiserror::Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] +pub enum SerialisableAwsSdkError { + /// The request failed during construction. It was not dispatched over the network. + #[error("Construction failure: {message}")] + ConstructionFailure { message: String }, + + /// The request failed due to a timeout. The request MAY have been sent and received. + #[error("Timeout error: {message}")] + TimeoutError { message: String }, + + /// The request failed during dispatch. An HTTP response was not received. The request MAY + /// have been sent. + #[error("Dispatch failure: {message}")] + DispatchFailure { message: String }, + + /// A response was received but it was not parseable according the the protocol (for example + /// the server hung up without sending a complete response) + #[error("Response error: {message}")] + ResponseError { message: String }, + + /// An error response was received from the service + #[error("Service error: {message}")] + ServiceError { message: String }, + + #[error("Other error: {message}")] + Other { message: String }, +} + +#[derive(Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] +pub enum SerialisableAwsSignerError { + /// Thrown when the AWS KMS API returns a signing error. + #[error(transparent)] + Sign { + aws_sdk_error: SerialisableAwsSdkError, + }, + + /// Thrown when the AWS KMS API returns an error. + #[error(transparent)] + GetPublicKey { + aws_sdk_error: SerialisableAwsSdkError, + }, + + /// [`ecdsa`] error. + #[error("ECDSA error: {message}")] + K256 { message: String }, + + /// [`spki`] error. + #[error("SPKI error: {message}")] + Spki { message: String }, + + /// [`hex`](mod@hex) error. + #[error("Hex error: {message}")] + Hex { message: String }, + + /// Thrown when the AWS KMS API returns a response without a signature. + #[error("signature not found in response")] + SignatureNotFound, + + /// Thrown when the AWS KMS API returns a response without a public key. + #[error("public key not found in response")] + PublicKeyNotFound, + + #[error("Unknown error: {message}")] + Unknown { message: String }, +} + +impl From> for SerialisableAwsSdkError { + fn from(err: SdkError) -> Self { + match err { + SdkError::ConstructionFailure(err) => SerialisableAwsSdkError::ConstructionFailure { + message: format!("{:?}", err), + }, + SdkError::TimeoutError(err) => SerialisableAwsSdkError::TimeoutError { + message: format!("{:?}", err), + }, + SdkError::DispatchFailure(err) => SerialisableAwsSdkError::DispatchFailure { + message: format!("{:?}", err), + }, + SdkError::ResponseError(err) => SerialisableAwsSdkError::ResponseError { + message: format!("{:?}", err), + }, + SdkError::ServiceError(err) => SerialisableAwsSdkError::ServiceError { + message: format!("{:?}", err), + }, + _ => SerialisableAwsSdkError::Other { + message: format!("{:?}", err), + }, + } + } +} + +impl From for EngineError { + fn from(err: AwsSignerError) -> Self { + match err { + AwsSignerError::Sign(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: err.into(), + }, + }, + AwsSignerError::GetPublicKey(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::GetPublicKey { + aws_sdk_error: err.into(), + }, + }, + AwsSignerError::K256(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::K256 { + message: err.to_string(), + }, + }, + AwsSignerError::Spki(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Spki { + message: err.to_string(), + }, + }, + AwsSignerError::Hex(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Hex { + message: err.to_string(), + }, + }, + AwsSignerError::SignatureNotFound => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::SignatureNotFound, + }, + AwsSignerError::PublicKeyNotFound => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::PublicKeyNotFound, + }, + } + } +} + impl From for EngineError { fn from(err: vault_sdk::error::VaultError) -> Self { let message = match &err { diff --git a/core/src/execution_options/aa.rs b/core/src/execution_options/aa.rs index fb894a4..7820d52 100644 --- a/core/src/execution_options/aa.rs +++ b/core/src/execution_options/aa.rs @@ -1,13 +1,13 @@ -use crate::{ - constants::{DEFAULT_FACTORY_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_6}, - defs::AddressDef, - error::EngineError, +use crate::{constants::DEFAULT_FACTORY_ADDRESS_V0_6, defs::AddressDef, error::EngineError}; +use alloy::{ + hex::FromHex, + primitives::{Address, Bytes}, }; -use alloy::{hex::FromHex, primitives::{Address, Bytes}}; +use engine_aa_types::{ENTRYPOINT_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_7}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use crate::constants::{DEFAULT_FACTORY_ADDRESS_V0_7, ENTRYPOINT_ADDRESS_V0_7}; +use crate::constants::DEFAULT_FACTORY_ADDRESS_V0_7; #[derive(Deserialize, Serialize, Debug, JsonSchema, Clone, Copy, utoipa::ToSchema)] pub enum EntrypointVersion { diff --git a/core/src/signer.rs b/core/src/signer.rs index 4698a19..2711bf1 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -2,9 +2,11 @@ use alloy::{ consensus::TypedTransaction, dyn_abi::TypedData, eips::eip7702::SignedAuthorization, - hex::FromHex, + hex::{self, FromHex}, + network::TxSigner, primitives::{Address, Bytes, ChainId, U256}, rpc::types::Authorization, + signers::Signer, }; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, PickFirst, serde_as}; @@ -15,7 +17,7 @@ use vault_types::enclave::encrypted::eoa::MessageFormat; use crate::{ credentials::SigningCredential, defs::AddressDef, - error::EngineError, + error::{EngineError, SerialisableAwsSdkError, SerialisableAwsSignerError}, execution_options::aa::{EntrypointAndFactoryDetails, EntrypointAndFactoryDetailsDeserHelper}, }; @@ -273,6 +275,30 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let message = match format { + MessageFormat::Text => message.to_string().into_bytes(), + MessageFormat::Hex => { + hex::decode(message).map_err(|_| EngineError::ValidationError { + message: "Invalid hex string".to_string(), + })? + } + }; + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer.sign_message(&message).await.map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } } } @@ -310,6 +336,26 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } } } @@ -347,6 +393,26 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let mut transaction = transaction.clone(); + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer + .sign_transaction(&mut transaction) + .await + .map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } } } @@ -394,10 +460,26 @@ impl AccountSigner for EoaSigner { // Return the signed authorization as Authorization Ok(iaw_result.signed_authorization) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let authorization_hash = authorization.signature_hash(); + + let signature = signer.sign_hash(&authorization_hash).await.map_err(|e| { + tracing::error!("Error signing authorization with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + + Ok(authorization.into_signed(signature)) + } } } } - /// Parameters for signing a message (used in routes) pub struct MessageSignerParams { pub credentials: SigningCredential, diff --git a/core/src/userop.rs b/core/src/userop.rs index cbdefcb..ec5762e 100644 --- a/core/src/userop.rs +++ b/core/src/userop.rs @@ -1,6 +1,7 @@ use alloy::{ hex::FromHex, primitives::{Address, Bytes, ChainId}, + signers::Signer, }; use thirdweb_core::iaw::IAWClient; use vault_sdk::VaultClient; @@ -9,7 +10,10 @@ use vault_types::{ userop::{UserOperationV06Input, UserOperationV07Input}, }; -use crate::{credentials::SigningCredential, error::EngineError}; +use crate::{ + credentials::SigningCredential, + error::{EngineError, SerialisableAwsSdkError, SerialisableAwsSignerError}, +}; // Re-export for convenience pub use engine_aa_types::VersionedUserOp; @@ -120,6 +124,26 @@ impl UserOpSigner { } })?) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(Some(params.chain_id)).await?; + let userophash = params.userop.hash(params.chain_id).map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to hash userop: {}", e), + } + })?; + + let signature = signer.sign_hash(&userophash).await.map_err(|e| { + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + + Ok(Bytes::copy_from_slice(&signature.as_bytes())) + } } } } diff --git a/executors/src/eoa/store/atomic.rs b/executors/src/eoa/store/atomic.rs index 9794e4a..e054be6 100644 --- a/executors/src/eoa/store/atomic.rs +++ b/executors/src/eoa/store/atomic.rs @@ -19,7 +19,7 @@ use crate::{ }, submitted::{ CleanAndGetRecycledNonces, CleanSubmittedTransactions, CleanupReport, - SubmittedNoopTransaction, SubmittedTransaction, + SubmittedNoopTransaction, }, }, worker::error::EoaExecutorWorkerError, diff --git a/executors/src/eoa/worker/confirm.rs b/executors/src/eoa/worker/confirm.rs index fbb9412..e21a002 100644 --- a/executors/src/eoa/worker/confirm.rs +++ b/executors/src/eoa/worker/confirm.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::eoa::{ store::{ - CleanupReport, ConfirmedTransaction, ReplacedTransaction, SubmittedTransaction, - SubmittedTransactionDehydrated, TransactionData, TransactionStoreError, + CleanupReport, ConfirmedTransaction, ReplacedTransaction, SubmittedTransactionDehydrated, + TransactionData, TransactionStoreError, }, worker::{ EoaExecutorWorker, @@ -111,7 +111,7 @@ impl EoaExecutorWorker { } // Fetch receipts and categorize transactions - let (confirmed_txs, replaced_txs) = + let (confirmed_txs, _replaced_txs) = self.fetch_confirmed_transaction_receipts(waiting_txs).await; // Process confirmed transactions diff --git a/server/Cargo.toml b/server/Cargo.toml index 783be6d..7d89e27 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -41,3 +41,4 @@ utoipa = { version = "5.4.0", features = [ utoipa-axum = "0.2.0" utoipa-scalar = { version = "0.3.0", features = ["axum"] } serde_with = "3.14.0" +aws-arn = "0.3.1" diff --git a/server/src/http/error.rs b/server/src/http/error.rs index 827cba7..edaa18b 100644 --- a/server/src/http/error.rs +++ b/server/src/http/error.rs @@ -66,7 +66,7 @@ impl ApiEngineError { _ => StatusCode::INTERNAL_SERVER_ERROR, }, - EngineError::VaultError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + EngineError::VaultError { .. } => StatusCode::BAD_GATEWAY, EngineError::IawError { error } => match error { thirdweb_core::iaw::IAWError::ApiError { .. } => StatusCode::INTERNAL_SERVER_ERROR, thirdweb_core::iaw::IAWError::SerializationError { .. } => StatusCode::BAD_REQUEST, @@ -83,6 +83,7 @@ impl ApiEngineError { EngineError::ValidationError { .. } => StatusCode::BAD_REQUEST, EngineError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR, EngineError::ThirdwebError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + EngineError::AwsKmsSignerError { .. } => StatusCode::BAD_GATEWAY, } } } diff --git a/server/src/http/extractors.rs b/server/src/http/extractors.rs index 200cc73..c66b2d6 100644 --- a/server/src/http/extractors.rs +++ b/server/src/http/extractors.rs @@ -1,15 +1,30 @@ use aide::OperationIo; +use aws_arn::known; use axum::{ Json, extract::{FromRequestParts, rejection::JsonRejection}, http::request::Parts, }; -use engine_core::{chain::RpcCredentials, credentials::SigningCredential, error::EngineError}; +use engine_core::{ + chain::RpcCredentials, + credentials::{AwsKmsCredential, SigningCredential}, + error::EngineError, +}; use thirdweb_core::auth::ThirdwebAuth; use vault_types::enclave::auth::Auth; use crate::http::error::ApiEngineError; +// Header name constants +const HEADER_THIRDWEB_SECRET_KEY: &str = "x-thirdweb-secret-key"; +const HEADER_THIRDWEB_CLIENT_ID: &str = "x-thirdweb-client-id"; +const HEADER_THIRDWEB_SERVICE_KEY: &str = "x-thirdweb-service-key"; +const HEADER_WALLET_ACCESS_TOKEN: &str = "x-wallet-access-token"; +const HEADER_VAULT_ACCESS_TOKEN: &str = "x-vault-access-token"; +const HEADER_AWS_KMS_ARN: &str = "x-aws-kms-arn"; +const HEADER_AWS_KMS_ACCESS_KEY_ID: &str = "x-aws-kms-access-key-id"; +const HEADER_AWS_SECRET_ACCESS_KEY: &str = "x-aws-secret-access-key"; + /// Extractor for RPC credentials from headers #[derive(OperationIo)] pub struct RpcCredentialsExtractor(pub RpcCredentials); @@ -24,7 +39,7 @@ where // try secret key first let secret_key = parts .headers - .get("x-thirdweb-secret-key") + .get(HEADER_THIRDWEB_SECRET_KEY) .and_then(|v| v.to_str().ok()); if let Some(secret_key) = secret_key { @@ -36,7 +51,7 @@ where // if not, try client id and service key let client_id = parts .headers - .get("x-thirdweb-client-id") + .get(HEADER_THIRDWEB_CLIENT_ID) .and_then(|v| v.to_str().ok()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { @@ -46,7 +61,7 @@ where let service_key = parts .headers - .get("x-thirdweb-service-key") + .get(HEADER_THIRDWEB_SERVICE_KEY) .and_then(|v| v.to_str().ok()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { @@ -92,19 +107,103 @@ where type Rejection = ApiEngineError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // Check for IAW credentials first (x-wallet-access-token) - // TODO: this will be deprecated in the future, we should use x-vault-access-token instead for all wallets - if let Some(wallet_token) = parts - .headers - .get("x-wallet-access-token") - .and_then(|v| v.to_str().ok()) - { - // Try client ID and service key combination - let client_id = parts - .headers - .get("x-thirdweb-client-id") - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { + // Try AWS KMS credentials first + if let Some(aws_kms) = Self::try_extract_aws_kms(parts)? { + return Ok(SigningCredentialsExtractor(SigningCredential::AwsKms( + aws_kms, + ))); + } + + // Try IAW credentials second + if let Some(iaw) = Self::try_extract_iaw(parts)? { + return Ok(SigningCredentialsExtractor(SigningCredential::Iaw { + auth_token: iaw.0, + thirdweb_auth: iaw.1, + })); + } + + // Try Vault credentials last + if let Some(vault_token) = Self::get_header_value(parts, HEADER_VAULT_ACCESS_TOKEN) { + return Ok(SigningCredentialsExtractor(SigningCredential::Vault( + Auth::AccessToken { + access_token: vault_token.to_string(), + }, + ))); + } + + // No valid credentials found + Err(ApiEngineError(EngineError::ValidationError { + message: "Missing valid authentication credentials. Provide either AWS KMS headers (x-aws-kms-arn, x-aws-kms-access-key-id, x-aws-secret-access-key), IAW credentials (x-wallet-access-token + x-thirdweb-client-id + x-thirdweb-service-key), or Vault credentials (x-vault-access-token)".to_string(), + })) + } +} + +impl SigningCredentialsExtractor { + /// Extract header value as string + fn get_header_value<'a>(parts: &'a Parts, header_name: &str) -> Option<&'a str> { + parts.headers.get(header_name).and_then(|v| v.to_str().ok()) + } + + /// Try to extract AWS KMS credentials from headers + fn try_extract_aws_kms(parts: &Parts) -> Result, ApiEngineError> { + let arn = Self::get_header_value(parts, HEADER_AWS_KMS_ARN); + let access_key_id = Self::get_header_value(parts, HEADER_AWS_KMS_ACCESS_KEY_ID); + let secret_access_key = Self::get_header_value(parts, HEADER_AWS_SECRET_ACCESS_KEY); + + match (arn, access_key_id, secret_access_key) { + (Some(arn), Some(access_key_id), Some(secret_access_key)) => { + let (key_id, region) = Self::parse_kms_arn(arn)?; + Ok(Some(AwsKmsCredential { + access_key_id: access_key_id.to_string(), + secret_access_key: secret_access_key.to_string(), + key_id, + region, + })) + } + _ => Ok(None), + } + } + + /// Parse and validate KMS ARN, returning (key_id, region) + fn parse_kms_arn(arn: &str) -> Result<(String, String), ApiEngineError> { + let parsed_arn: aws_arn::ResourceName = arn.parse().map_err(|e| { + ApiEngineError(EngineError::ValidationError { + message: format!("Invalid AWS ARN format: {}", e), + }) + })?; + + // Validate it's a KMS service + if parsed_arn.service != known::Service::KeyManagement.into() { + return Err(ApiEngineError(EngineError::ValidationError { + message: format!("ARN must be for KMS service, got: {}", parsed_arn.service), + })); + } + + // Extract and validate key ID + let key_id = parsed_arn.resource.to_string(); + if key_id.is_empty() { + return Err(ApiEngineError(EngineError::ValidationError { + message: "KMS ARN must contain a valid key ID in the resource part".to_string(), + })); + } + + // Extract and validate region + let region = parsed_arn.region.ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "KMS ARN must contain a valid region".to_string(), + }) + })?; + + Ok((key_id, region.to_string())) + } + + /// Try to extract IAW credentials from headers, returning (auth_token, thirdweb_auth) + fn try_extract_iaw(parts: &Parts) -> Result, ApiEngineError> { + let wallet_token = Self::get_header_value(parts, HEADER_WALLET_ACCESS_TOKEN); + + if let Some(wallet_token) = wallet_token { + let client_id = + Self::get_header_value(parts, HEADER_THIRDWEB_CLIENT_ID).ok_or_else(|| { ApiEngineError(EngineError::ValidationError { message: "Missing x-thirdweb-client-id header when using x-wallet-access-token" @@ -112,10 +211,7 @@ where }) })?; - let service_key = parts - .headers - .get("x-thirdweb-service-key") - .and_then(|v| v.to_str().ok()) + let service_key = Self::get_header_value(parts, HEADER_THIRDWEB_SERVICE_KEY) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { message: @@ -131,28 +227,10 @@ where }, ); - return Ok(SigningCredentialsExtractor(SigningCredential::Iaw { - auth_token: wallet_token.to_string(), - thirdweb_auth, - })); - }; - - // Fall back to Vault credentials - let vault_access_token = parts - .headers - .get("x-vault-access-token") - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - ApiEngineError(EngineError::ValidationError { - message: "Missing x-vault-access-token or x-wallet-token header".to_string(), - }) - })?; - - Ok(SigningCredentialsExtractor(SigningCredential::Vault( - Auth::AccessToken { - access_token: vault_access_token.to_string(), - }, - ))) + Ok(Some((wallet_token.to_string(), thirdweb_auth))) + } else { + Ok(None) + } } } From 799a1a8d38c6c6b969e5d542a6b35476b9768c8f Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Wed, 23 Jul 2025 05:05:33 +0530 Subject: [PATCH 4/6] Enhance AWS KMS integration and error handling - Updated AWS KMS credential extraction to use a more consistent header naming convention. - Improved error handling for retryable RPC errors in the EoaExecutorWorkerError, allowing for better job management. - Refactored AWS KMS credential validation to ensure proper extraction of key IDs and regions from ARNs. These changes aim to streamline AWS KMS operations and enhance the robustness of error handling in the executor worker. --- core/src/credentials.rs | 3 ++- executors/src/eoa/worker/error.rs | 16 ++++++++++++++++ server/src/http/extractors.rs | 25 +++++++++++++++++-------- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/core/src/credentials.rs b/core/src/credentials.rs index c7b000b..1ae255a 100644 --- a/core/src/credentials.rs +++ b/core/src/credentials.rs @@ -1,6 +1,6 @@ use alloy::primitives::ChainId; use alloy_signer_aws::AwsSigner; -use aws_config::BehaviorVersion; +use aws_config::{BehaviorVersion, Region}; use aws_credential_types::provider::future::ProvideCredentials as ProvideCredentialsFuture; use aws_sdk_kms::config::{Credentials, ProvideCredentials}; use serde::{Deserialize, Serialize}; @@ -48,6 +48,7 @@ impl AwsKmsCredential { pub async fn get_signer(&self, chain_id: Option) -> Result { let config = aws_config::defaults(BehaviorVersion::latest()) .credentials_provider(self.clone()) + .region(Region::new(self.region.clone())) .load() .await; let client = aws_sdk_kms::Client::new(&config); diff --git a/executors/src/eoa/worker/error.rs b/executors/src/eoa/worker/error.rs index 44cb1de..cc54363 100644 --- a/executors/src/eoa/worker/error.rs +++ b/executors/src/eoa/worker/error.rs @@ -94,6 +94,17 @@ impl EoaExecutorWorkerError { inner_error: TransactionStoreError::LockLost { .. }, .. } => JobError::Fail(self), + EoaExecutorWorkerError::RpcError { .. } => { + if is_retryable_preparation_error(&self) { + JobError::Nack { + error: self, + delay: Some(Duration::from_secs(10)), + position: RequeuePosition::Last, + } + } else { + JobError::Fail(self) + } + } _ => JobError::Nack { error: self, delay: Some(Duration::from_secs(10)), @@ -219,6 +230,11 @@ pub fn is_retryable_rpc_error(kind: &RpcErrorKind) -> bool { match kind { RpcErrorKind::TransportHttpError { status, .. } if *status >= 400 && *status < 500 => false, RpcErrorKind::UnsupportedFeature { .. } => false, + RpcErrorKind::ErrorResp(resp) => { + let message = resp.message.to_lowercase(); + // if the error message contains "invalid chain", it's not retryable + !message.contains("invalid chain") + } _ => true, } } diff --git a/server/src/http/extractors.rs b/server/src/http/extractors.rs index c66b2d6..cb5d5e7 100644 --- a/server/src/http/extractors.rs +++ b/server/src/http/extractors.rs @@ -22,7 +22,7 @@ const HEADER_THIRDWEB_SERVICE_KEY: &str = "x-thirdweb-service-key"; const HEADER_WALLET_ACCESS_TOKEN: &str = "x-wallet-access-token"; const HEADER_VAULT_ACCESS_TOKEN: &str = "x-vault-access-token"; const HEADER_AWS_KMS_ARN: &str = "x-aws-kms-arn"; -const HEADER_AWS_KMS_ACCESS_KEY_ID: &str = "x-aws-kms-access-key-id"; +const HEADER_AWS_ACCESS_KEY_ID: &str = "x-aws-access-key-id"; const HEADER_AWS_SECRET_ACCESS_KEY: &str = "x-aws-secret-access-key"; /// Extractor for RPC credentials from headers @@ -147,7 +147,7 @@ impl SigningCredentialsExtractor { /// Try to extract AWS KMS credentials from headers fn try_extract_aws_kms(parts: &Parts) -> Result, ApiEngineError> { let arn = Self::get_header_value(parts, HEADER_AWS_KMS_ARN); - let access_key_id = Self::get_header_value(parts, HEADER_AWS_KMS_ACCESS_KEY_ID); + let access_key_id = Self::get_header_value(parts, HEADER_AWS_ACCESS_KEY_ID); let secret_access_key = Self::get_header_value(parts, HEADER_AWS_SECRET_ACCESS_KEY); match (arn, access_key_id, secret_access_key) { @@ -180,12 +180,19 @@ impl SigningCredentialsExtractor { } // Extract and validate key ID - let key_id = parsed_arn.resource.to_string(); - if key_id.is_empty() { - return Err(ApiEngineError(EngineError::ValidationError { - message: "KMS ARN must contain a valid key ID in the resource part".to_string(), - })); - } + let key_id = parsed_arn + .resource + .path_split() + .last() + .map(|id| { + dbg!(&id); + id.to_string() + }) + .ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "KMS ARN must contain a valid key ID in the resource part".to_string(), + }) + })?; // Extract and validate region let region = parsed_arn.region.ok_or_else(|| { @@ -194,6 +201,8 @@ impl SigningCredentialsExtractor { }) })?; + dbg!(®ion); + Ok((key_id, region.to_string())) } From 638fce689b7104041b189f4d614f85cc05a3b8dd Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Wed, 23 Jul 2025 12:56:25 +0530 Subject: [PATCH 5/6] Update dependencies and enhance signing capabilities - Updated `Cargo.lock` and `Cargo.toml` to reflect the new version `1.0.23` for the `alloy` package and added `serde_repr` as a dependency. - Introduced `PrivateKeySigner` for local signing in the `SigningCredential` enum, allowing for random private key generation for testing. - Enhanced the `AccountSigner` trait to support signing operations with the new `PrivateKey` variant. - Added 7702 tests for session keys as well as owner execution These changes improve the signing capabilities and facilitate testing with local private keys. --- Cargo.lock | 120 ++-- Cargo.toml | 2 +- core/src/credentials.rs | 12 + core/src/signer.rs | 68 +- core/src/userop.rs | 15 + eip7702-core/Cargo.toml | 19 +- eip7702-core/src/delegated_account.rs | 6 +- eip7702-core/src/transaction.rs | 90 ++- eip7702-core/tests/bytecode/erc20.hex | 1 + eip7702-core/tests/bytecode/erc20.sol | 73 +++ eip7702-core/tests/integration_tests.rs | 821 ++++++++++++++++++++++++ executors/src/eip7702_executor/send.rs | 60 +- 12 files changed, 1187 insertions(+), 100 deletions(-) create mode 100644 eip7702-core/tests/bytecode/erc20.hex create mode 100644 eip7702-core/tests/bytecode/erc20.sol create mode 100644 eip7702-core/tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9225ce2..5425507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d1aecf3cab3d0e7383064ce488616434b4ade10d8904dff422e74203c712f" +checksum = "5ecf116474faea3e30ecb03cb14548598ca8243d5316ce50f820e67b3e848473" dependencies = [ "alloy-consensus", "alloy-contract", @@ -88,6 +88,7 @@ dependencies = [ "alloy-genesis", "alloy-json-rpc", "alloy-network", + "alloy-node-bindings", "alloy-provider", "alloy-rpc-client", "alloy-rpc-types", @@ -153,9 +154,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977b97d271159578afcb26e39e1ca5ce1a7f937697793d7d571b0166dd8b8225" +checksum = "f28074a21cd4f7c3a7ab218c4f38fae6be73944e1feae3b670c68b60bf85ca40" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -261,9 +262,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fcbae2107f3f2df2b02bb7d9e81e8aa730ae371ca9dd7fd0c81c3d0cb78a452" +checksum = "c51b4c13e02a8104170a4de02ccf006d7c233e6c10ab290ee16e7041e6ac221d" dependencies = [ "alloy-eips", "alloy-primitives", @@ -273,6 +274,19 @@ dependencies = [ "serde_with", ] +[[package]] +name = "alloy-hardforks" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819a3620fe125e0fff365363315ee5e24c23169173b19747dfd6deba33db8990" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + [[package]] name = "alloy-json-abi" version = "1.2.1" @@ -339,6 +353,27 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-node-bindings" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de75f0d0af3c6cb0bd3648e530289b2c542b7bf57e7d4296d1c29281418a476" +dependencies = [ + "alloy-genesis", + "alloy-hardforks", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "alloy-signer-local", + "k256", + "rand 0.8.5", + "serde_json", + "tempfile", + "thiserror 2.0.12", + "tracing", + "url", +] + [[package]] name = "alloy-primitives" version = "1.2.1" @@ -368,9 +403,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc164acf8c41c756e76c7aea3be8f0fb03f8a3ef90a33e3ddcea5d1614d8779" +checksum = "d59879a772ebdcde9dc4eb38b2535d32e8503d3175687cc09e763a625c5fcf32" dependencies = [ "alloy-chains", "alloy-consensus", @@ -378,8 +413,10 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", + "alloy-node-bindings", "alloy-primitives", "alloy-rpc-client", + "alloy-rpc-types-anvil", "alloy-rpc-types-eth", "alloy-signer", "alloy-sol-types", @@ -430,15 +467,14 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c44d31bcb9afad460915fe1fba004a2af5a07a3376c307b9bdfeec3678c209" +checksum = "7f060e3bb9f319eb01867a2d6d1ff9e0114e8877f5ca8f5db447724136106cae" dependencies = [ "alloy-json-rpc", "alloy-primitives", "alloy-transport", "alloy-transport-http", - "async-stream", "futures", "pin-project 1.1.10", "reqwest", @@ -448,16 +484,27 @@ dependencies = [ "tokio-stream", "tower", "tracing", - "tracing-futures", "url", "wasmtimer", ] [[package]] name = "alloy-rpc-types" -version = "1.0.17" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d47b637369245d2dafef84b223b1ff5ea59e6cd3a98d2d3516e32788a0b216df" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-anvil" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba2cf3d3c6ece87f1c6bb88324a997f28cf0ad7e98d5e0b6fa91c4003c30916" +checksum = "c0b1f499acb3fc729615147bc113b8b798b17379f19d43058a687edc5792c102" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -545,9 +592,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74809e45053bd43d24338e618202ebea68d5660aa9632d77b0244faa2dcaa9d1" +checksum = "0a207671ef0bf6f61e9c80c9ccb6d203439071252fb35886d6a89aae5431cd9c" dependencies = [ "alloy-consensus", "alloy-network", @@ -563,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c7e67367bc2b1d5790236448d2402865a4f0bc2b53cfda06d71b7ba3dbdffd" +checksum = "cce5e8c97a526d39052035a99652b9cfacf0d646d4a3625fac9c919d20a46fb0" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -583,9 +630,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d95902d29e1290809e1c967a1e974145b44b78f6e3e12fc07a60c1225e3df0" +checksum = "ad7094c39cd41b03ed642145b0bd37251e31a9cf2ed19e1ce761f089867356a6" dependencies = [ "alloy-consensus", "alloy-network", @@ -672,9 +719,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdf4b7fc58ebb2605b2fc5a33dae5cf15527ea70476978351cc0db1c596ea93" +checksum = "f89bec2f59a41c0e259b6fe92f78dfc49862c17d10f938db9c33150d5a7f42b6" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -695,9 +742,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4b0f3a9c28bcd3761504d9eb3578838d6d115c8959fc1ea05f59a3a8f691af" +checksum = "0d3615ec64d775fec840f4e9d5c8e1f739eb1854d8d28db093fb3d4805e0cb53" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -2426,9 +2473,11 @@ dependencies = [ "rand 0.9.1", "serde", "serde_json", + "serde_repr", "thiserror 2.0.12", "tokio", "tracing", + "vault-types", ] [[package]] @@ -5201,6 +5250,17 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -5957,18 +6017,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "futures", - "futures-task", - "pin-project 1.1.10", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 72ea61b..c2593ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,6 @@ members = [ resolver = "2" [workspace.dependencies] -alloy = { version = "1.0.8" } +alloy = { version = "1.0.23" } vault-types = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" } vault-sdk = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" } diff --git a/core/src/credentials.rs b/core/src/credentials.rs index 1ae255a..8871d8b 100644 --- a/core/src/credentials.rs +++ b/core/src/credentials.rs @@ -1,4 +1,5 @@ use alloy::primitives::ChainId; +use alloy::signers::local::PrivateKeySigner; use alloy_signer_aws::AwsSigner; use aws_config::{BehaviorVersion, Region}; use aws_credential_types::provider::future::ProvideCredentials as ProvideCredentialsFuture; @@ -10,6 +11,13 @@ use vault_types::enclave::auth::Auth as VaultAuth; use crate::error::EngineError; +impl SigningCredential { + /// Create a random private key credential for testing + pub fn random_local() -> Self { + SigningCredential::PrivateKey(PrivateKeySigner::random()) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SigningCredential { Vault(VaultAuth), @@ -18,6 +26,10 @@ pub enum SigningCredential { thirdweb_auth: ThirdwebAuth, }, AwsKms(AwsKmsCredential), + /// Private key signer for testing and development + /// Note: This should only be used in test environments + #[serde(skip)] + PrivateKey(PrivateKeySigner), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/core/src/signer.rs b/core/src/signer.rs index 2711bf1..a8948ca 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -6,7 +6,7 @@ use alloy::{ network::TxSigner, primitives::{Address, Bytes, ChainId, U256}, rpc::types::Authorization, - signers::Signer, + signers::{Signer, SignerSync}, }; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, PickFirst, serde_as}; @@ -162,13 +162,11 @@ pub enum SigningOptions { ERC4337(Erc4337SigningOptions), } -/// Account signer trait using impl Future pattern like TWMQ pub trait AccountSigner { - type SigningOptions; /// Sign a message fn sign_message( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, message: &str, format: MessageFormat, credentials: &SigningCredential, @@ -177,7 +175,7 @@ pub trait AccountSigner { /// Sign typed data fn sign_typed_data( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, typed_data: &TypedData, credentials: &SigningCredential, ) -> impl std::future::Future> + Send; @@ -185,7 +183,7 @@ pub trait AccountSigner { /// Sign a transaction fn sign_transaction( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, transaction: &TypedTransaction, credentials: &SigningCredential, ) -> impl std::future::Future> + Send; @@ -193,7 +191,7 @@ pub trait AccountSigner { /// Sign EIP-7702 authorization fn sign_authorization( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, chain_id: u64, address: Address, nonce: u64, @@ -219,8 +217,6 @@ impl EoaSigner { } impl AccountSigner for EoaSigner { - type SigningOptions = EoaSigningOptions; - async fn sign_message( &self, options: EoaSigningOptions, @@ -299,6 +295,24 @@ impl AccountSigner for EoaSigner { })?; Ok(signature.to_string()) } + SigningCredential::PrivateKey(signer) => { + let message_bytes = match format { + MessageFormat::Text => message.to_string().into_bytes(), + MessageFormat::Hex => { + alloy::hex::decode(message).map_err(|_| EngineError::ValidationError { + message: "Invalid hex string".to_string(), + })? + } + }; + + let signature = signer.sign_message(&message_bytes).await.map_err(|e| { + tracing::error!("Error signing message with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign message: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -356,6 +370,18 @@ impl AccountSigner for EoaSigner { })?; Ok(signature.to_string()) } + SigningCredential::PrivateKey(signer) => { + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| { + tracing::error!("Error signing typed data with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign typed data: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -413,6 +439,19 @@ impl AccountSigner for EoaSigner { })?; Ok(signature.to_string()) } + SigningCredential::PrivateKey(signer) => { + let mut transaction = transaction.clone(); + let signature = signer + .sign_transaction(&mut transaction) + .await + .map_err(|e| { + tracing::error!("Error signing transaction with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign transaction: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -475,6 +514,17 @@ impl AccountSigner for EoaSigner { } })?; + Ok(authorization.into_signed(signature)) + } + SigningCredential::PrivateKey(signer) => { + let authorization_hash = authorization.signature_hash(); + let signature = signer.sign_hash_sync(&authorization_hash).map_err(|e| { + tracing::error!("Error signing authorization with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign authorization: {}", e), + } + })?; + Ok(authorization.into_signed(signature)) } } diff --git a/core/src/userop.rs b/core/src/userop.rs index ec5762e..e30a6ba 100644 --- a/core/src/userop.rs +++ b/core/src/userop.rs @@ -142,6 +142,21 @@ impl UserOpSigner { } })?; + Ok(Bytes::copy_from_slice(&signature.as_bytes())) + } + SigningCredential::PrivateKey(signer) => { + let userophash = params.userop.hash(params.chain_id).map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to hash userop: {}", e), + } + })?; + + let signature = signer.sign_hash(&userophash).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign userop: {}", e), + } + })?; + Ok(Bytes::copy_from_slice(&signature.as_bytes())) } } diff --git a/eip7702-core/Cargo.toml b/eip7702-core/Cargo.toml index c838160..a18cf83 100644 --- a/eip7702-core/Cargo.toml +++ b/eip7702-core/Cargo.toml @@ -11,4 +11,21 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0" tracing = "0.1.41" rand = "0.9" -thiserror = "2.0" \ No newline at end of file +thiserror = "2.0" +serde_repr = "0.1.20" + +[dev-dependencies] +alloy = { workspace = true, features = [ + "sol-types", + "providers", + "transports", + "rpc-types", + "consensus", + "network", + "signers", + "node-bindings", + "serde", + "contract", + "eip712", +] } +vault-types = { workspace = true } diff --git a/eip7702-core/src/delegated_account.rs b/eip7702-core/src/delegated_account.rs index 097d9b3..8f329fa 100644 --- a/eip7702-core/src/delegated_account.rs +++ b/eip7702-core/src/delegated_account.rs @@ -6,7 +6,7 @@ use engine_core::{ chain::Chain, credentials::SigningCredential, error::{AlloyRpcErrorToEngineError, EngineError}, - signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + signer::{AccountSigner, EoaSigningOptions}, }; use rand::Rng; @@ -99,9 +99,9 @@ impl DelegatedAccount { } /// Sign authorization for EIP-7702 delegation (automatically fetches nonce) - pub async fn sign_authorization( + pub async fn sign_authorization( &self, - eoa_signer: &EoaSigner, + eoa_signer: &S, credentials: &SigningCredential, ) -> Result { let nonce = self.get_nonce().await?; diff --git a/eip7702-core/src/transaction.rs b/eip7702-core/src/transaction.rs index 812fdfe..51092a1 100644 --- a/eip7702-core/src/transaction.rs +++ b/eip7702-core/src/transaction.rs @@ -12,24 +12,87 @@ use engine_core::{ transaction::InnerTransaction, }; use serde_json::Value; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::delegated_account::DelegatedAccount; sol!( - #[derive(serde::Serialize)] + #[derive(serde::Serialize, serde::Deserialize)] struct Call { address target; uint256 value; bytes data; } - #[derive(serde::Serialize)] + #[derive(serde::Serialize, serde::Deserialize)] struct WrappedCalls { Call[] calls; bytes32 uid; } function execute(Call[] calldata calls) external payable; + function executeWithSig(WrappedCalls calldata wrappedCalls, bytes calldata signature) external payable; + + #[derive(Serialize_repr, Deserialize_repr)] + enum LimitType { + Unlimited, + Lifetime, + Allowance, + } + + #[derive(Serialize_repr, Deserialize_repr)] + enum Condition { + Unconstrained, + Equal, + Greater, + Less, + GreaterOrEqual, + LessOrEqual, + NotEqual, + } + + #[derive(serde::Serialize)] + struct UsageLimit { + LimitType limitType; + uint256 limit; // ignored if limitType == Unlimited + uint256 period; // ignored if limitType != Allowance + } + + #[derive(serde::Serialize)] + struct Constraint { + Condition condition; + uint64 index; + bytes32 refValue; + UsageLimit limit; + } + + #[derive(serde::Serialize)] + struct CallSpec { + address target; + bytes4 selector; + uint256 maxValuePerUse; + UsageLimit valueLimit; + Constraint[] constraints; + } + + #[derive(serde::Serialize)] + struct TransferSpec { + address target; + uint256 maxValuePerUse; + UsageLimit valueLimit; + } + + #[derive(serde::Serialize)] + struct SessionSpec { + address signer; + bool isWildcard; + uint256 expiresAt; + CallSpec[] callPolicies; + TransferSpec[] transferPolicies; + bytes32 uid; + } + + function createSessionWithSig(SessionSpec calldata sessionSpec, bytes calldata signature) external; ); /// A transaction for a minimal account that supports signing and execution via bundler @@ -126,9 +189,9 @@ impl MinimalAccountTransaction { self.authorization = Some(authorization); } - pub async fn add_authorization_if_needed( + pub async fn add_authorization_if_needed( mut self, - signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result { if self.account.is_minimal_account().await? { @@ -141,12 +204,12 @@ impl MinimalAccountTransaction { } /// Build the transaction data as JSON for bundler execution with automatic signing - pub async fn build( + pub async fn build( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result<(Value, String), EngineError> { - let signature = self.sign_wrapped_calls(eoa_signer, credentials).await?; + let signature = self.sign_wrapped_calls(signer, credentials).await?; // Serialize wrapped calls to JSON let wrapped_calls_json = serde_json::to_value(&self.wrapped_calls).map_err(|e| { @@ -190,20 +253,19 @@ impl MinimalAccountTransaction { self.authorization.as_ref() } - pub async fn sign_wrapped_calls( + pub async fn sign_wrapped_calls( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result { let typed_data = self.create_wrapped_calls_typed_data(); - self.sign_typed_data(eoa_signer, credentials, &typed_data) - .await + self.sign_typed_data(signer, credentials, &typed_data).await } /// Sign typed data with EOA signer - async fn sign_typed_data( + async fn sign_typed_data( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, typed_data: &TypedData, ) -> Result { @@ -212,7 +274,7 @@ impl MinimalAccountTransaction { chain_id: Some(self.account.chain().chain_id()), }; - eoa_signer + signer .sign_typed_data(signing_options, typed_data, credentials) .await } diff --git a/eip7702-core/tests/bytecode/erc20.hex b/eip7702-core/tests/bytecode/erc20.hex new file mode 100644 index 0000000..7a86853 --- /dev/null +++ b/eip7702-core/tests/bytecode/erc20.hex @@ -0,0 +1 @@ +60806040523461031d57610bc38038038061001981610321565b92833981019060408183031261031d5780516001600160401b03811161031d5782610045918301610346565b60208201519092906001600160401b03811161031d576100659201610346565b81516001600160401b03811161023057600354600181811c91168015610313575b602082101461021257601f81116102b0575b50602092601f821160011461024f57928192935f92610244575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161023057600454600181811c91168015610226575b602082101461021257601f81116101af575b50602091601f821160011461014f579181925f92610144575b50508160011b915f199060031b1c1916176004555b601260ff19600554161760055560405161082b90816103988239f35b015190505f80610113565b601f1982169260045f52805f20915f5b8581106101975750836001951061017f575b505050811b01600455610128565b01515f1960f88460031b161c191690555f8080610171565b9192602060018192868501518155019401920161015f565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610208575b601f0160051c01905b8181106101fd57506100fa565b5f81556001016101f0565b90915081906101e7565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100e8565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b2565b601f1982169360035f52805f20915f5b8681106102985750836001959610610280575b505050811b016003556100c7565b01515f1960f88460031b161c191690555f8080610272565b9192602060018192868501518155019401920161025f565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610309575b601f0160051c01905b8181106102fe5750610098565b5f81556001016102f1565b90915081906102e8565b90607f1690610086565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761023057604052565b81601f8201121561031d578051906001600160401b03821161023057610375601f8301601f1916602001610321565b928284526020838301011161031d57815f9260208093018386015e830101529056fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610651578063095ea7b3146105d957806318160ddd146105bc57806323b872dd14610474578063313ce5671461045457806342966c68146103d3578063449a52f81461035957806370a082311461032257806395d89b4114610204578063a0712d681461019a578063a9059cbb146100f55763dd62ed3e1461009d575f80fd5b346100f15760403660031901126100f1576100b6610750565b6001600160a01b036100c6610766565b91165f5260016020526001600160a01b0360405f2091165f52602052602060405f2054604051908152f35b5f80fd5b346100f15760403660031901126100f15761010e610750565b6001600160a01b0360243591335f525f6020526101318360405f2054101561077c565b335f525f60205260405f206101478482546107c7565b90551690815f525f60205260405f206101618282546107e8565b90556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b346100f15760203660031901126100f157600435335f525f60205260405f206101c48282546107e8565b90556101d2816002546107e8565b6002556040519081525f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203393a3005b346100f1575f3660031901126100f1576040515f6004548060011c90600181168015610318575b602083108114610304578285529081156102e85750600114610292575b50819003601f01601f191681019067ffffffffffffffff82118183101761027e576040829052819061027a9082610726565b0390f35b634e487b7160e01b5f52604160045260245ffd5b60045f9081529091507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8282106102d257506020915082010182610248565b60018160209254838588010152019101906102bd565b90506020925060ff191682840152151560051b82010182610248565b634e487b7160e01b5f52602260045260245ffd5b91607f169161022b565b346100f15760203660031901126100f1576001600160a01b03610343610750565b165f525f602052602060405f2054604051908152f35b346100f15760403660031901126100f157610372610750565b5f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206001600160a01b03602435941693848452838252604084206103b98282546107e8565b90556103c7816002546107e8565b600255604051908152a3005b346100f15760203660031901126100f1575f600435338252816020526103ff816040842054101561077c565b33825281602052604082206104158282546107c7565b9055610423816002546107c7565b6002556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3005b346100f1575f3660031901126100f157602060ff60055416604051908152f35b346100f15760603660031901126100f15761048d610750565b610495610766565b6001600160a01b03604435921690815f525f6020526104ba8360405f2054101561077c565b815f52600160205260405f206001600160a01b0333165f526020528260405f2054106105785760206001600160a01b037fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92845f525f835260405f206105218782546107c7565b90551693845f525f825260405f2061053a8282546107e8565b9055835f526001825260405f206001600160a01b0333165f52825260405f206105648282546107c7565b9055604051908152a3602060405160018152f35b606460405162461bcd60e51b815260206004820152601660248201527f496e73756666696369656e7420616c6c6f77616e6365000000000000000000006044820152fd5b346100f1575f3660031901126100f1576020600254604051908152f35b346100f15760403660031901126100f1576105f2610750565b6001600160a01b0360243591335f52600160205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100f1575f3660031901126100f1576040515f6003548060011c9060018116801561071c575b602083108114610304578285529081156102e857506001146106c65750819003601f01601f191681019067ffffffffffffffff82118183101761027e576040829052819061027a9082610726565b60035f9081529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b82821061070657506020915082010182610248565b60018160209254838588010152019101906106f1565b91607f1691610678565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100f157565b602435906001600160a01b03821682036100f157565b1561078357565b606460405162461bcd60e51b815260206004820152601460248201527f496e73756666696369656e742062616c616e63650000000000000000000000006044820152fd5b919082039182116107d457565b634e487b7160e01b5f52601160045260245ffd5b919082018092116107d45756fea26469706673582212200c7568949b7a79953600837c1717f912a69b1a7899a28de749c4a45445b20e6264736f6c634300081a0033 \ No newline at end of file diff --git a/eip7702-core/tests/bytecode/erc20.sol b/eip7702-core/tests/bytecode/erc20.sol new file mode 100644 index 0000000..3533f07 --- /dev/null +++ b/eip7702-core/tests/bytecode/erc20.sol @@ -0,0 +1,73 @@ +contract MockERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + uint256 public totalSupply; + string public name; + string public symbol; + uint8 public decimals; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + decimals = 18; + } + + function mint(uint256 amount) public { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + function mintTo(address to, uint256 amount) public { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public returns (bool) { + require(balanceOf[from] >= amount, "Insufficient balance"); + require( + allowance[from][msg.sender] >= amount, + "Insufficient allowance" + ); + + balanceOf[from] -= amount; + balanceOf[to] += amount; + allowance[from][msg.sender] -= amount; + + emit Transfer(from, to, amount); + return true; + } + + function burn(uint256 amount) public { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } +} diff --git a/eip7702-core/tests/integration_tests.rs b/eip7702-core/tests/integration_tests.rs new file mode 100644 index 0000000..60f5d97 --- /dev/null +++ b/eip7702-core/tests/integration_tests.rs @@ -0,0 +1,821 @@ +use std::time::Duration; + +use alloy::{ + consensus::{SignableTransaction, TypedTransaction}, + eips::{BlockNumberOrTag, eip7702::SignedAuthorization}, + hex, + network::{EthereumWallet, TransactionBuilder, TransactionBuilder7702, TxSigner}, + node_bindings::{Anvil, AnvilInstance}, + primitives::{Address, BlockNumber, Bytes, TxHash, U256}, + providers::{ + DynProvider, Identity, Provider, ProviderBuilder, RootProvider, + ext::AnvilApi, + fillers::{ + BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, + WalletFiller, + }, + layers::AnvilProvider, + }, + rpc::types::{TransactionReceipt, TransactionRequest}, + signers::Signer, + sol, + sol_types::SolCall, +}; +use engine_core::{ + chain::Chain, + credentials::SigningCredential, + error::EngineError, + signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + transaction::InnerTransaction, +}; +use engine_eip7702_core::{ + constants::MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, + delegated_account::DelegatedAccount, + transaction::{CallSpec, LimitType, SessionSpec, WrappedCalls}, +}; +use serde_json::Value; +use tokio::time::sleep; + +use crate::MockERC20::{MockERC20Calls, MockERC20Instance}; + +// Mock ERC20 contract +sol! { + #[allow(missing_docs)] + #[sol(rpc, bytecode=include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/bytecode/erc20.hex")))] + contract MockERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + uint256 public totalSupply; + string public name; + string public symbol; + uint8 public decimals; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + decimals = 18; + } + + function mint(uint256 amount) public { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + function mintTo(address to, uint256 amount) public { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + require(balanceOf[from] >= amount, "Insufficient balance"); + require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); + + balanceOf[from] -= amount; + balanceOf[to] += amount; + allowance[from][msg.sender] -= amount; + + emit Transfer(from, to, amount); + return true; + } + + function burn(uint256 amount) public { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + } +} + +// Test chain implementation +#[derive(Clone)] +struct TestChain { + chain_id: u64, + provider: alloy::providers::DynProvider, + rpc_url: alloy::transports::http::reqwest::Url, +} + +impl Chain for TestChain { + fn chain_id(&self) -> u64 { + self.chain_id + } + + fn rpc_url(&self) -> alloy::transports::http::reqwest::Url { + self.rpc_url.clone() + } + + fn bundler_url(&self) -> alloy::transports::http::reqwest::Url { + // For tests, we'll just use the same URL + self.rpc_url.clone() + } + + fn paymaster_url(&self) -> alloy::transports::http::reqwest::Url { + // For tests, we'll just use the same URL + self.rpc_url.clone() + } + + fn provider(&self) -> &alloy::providers::RootProvider { + self.provider.root() + } + + fn bundler_client(&self) -> &engine_core::rpc_clients::BundlerClient { + // For tests, we don't need real bundler client + panic!("bundler_client not implemented for test chain") + } + + fn paymaster_client(&self) -> &engine_core::rpc_clients::PaymasterClient { + // For tests, we don't need real paymaster client + panic!("paymaster_client not implemented for test chain") + } + + fn bundler_client_with_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> engine_core::rpc_clients::BundlerClient { + panic!("bundler_client_with_headers not implemented for test chain") + } + + fn paymaster_client_with_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> engine_core::rpc_clients::PaymasterClient { + panic!("paymaster_client_with_headers not implemented for test chain") + } + + fn with_new_default_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> Self { + self.clone() + } +} + +// Mock EoaSigner for tests +struct MockEoaSigner; + +impl AccountSigner for MockEoaSigner { + async fn sign_message( + &self, + _options: EoaSigningOptions, + _message: &str, + _format: vault_types::enclave::encrypted::eoa::MessageFormat, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let message_bytes = _message.as_bytes(); + let signature = signer.sign_message(message_bytes).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign message: {}", e), + } + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_typed_data( + &self, + _options: EoaSigningOptions, + typed_data: &alloy::dyn_abi::TypedData, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| EngineError::ValidationError { + message: format!("Failed to sign typed data: {}", e), + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_transaction( + &self, + _options: EoaSigningOptions, + transaction: &TypedTransaction, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let mut tx = transaction.clone(); + let signature = signer.sign_transaction(&mut tx).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign transaction: {}", e), + } + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_authorization( + &self, + _options: EoaSigningOptions, + chain_id: u64, + address: Address, + nonce: u64, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let authorization = alloy::rpc::types::Authorization { + chain_id: U256::from(chain_id), + address, + nonce, + }; + let authorization_hash = authorization.signature_hash(); + let signature = signer.sign_hash(&authorization_hash).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign authorization: {}", e), + } + })?; + Ok(authorization.into_signed(signature)) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } +} + +struct TestSetup { + chain: TestChain, + + anvil_provider: FillProvider< + JoinFill< + JoinFill< + Identity, + JoinFill>>, + >, + WalletFiller, + >, + AnvilProvider, + >, + mock_erc20_contract: crate::MockERC20::MockERC20Instance, + + executor_credentials: SigningCredential, + developer_credentials: SigningCredential, + user_credentials: SigningCredential, + + executor_address: Address, + developer_address: Address, + user_address: Address, + + signer: MockEoaSigner, +} + +const ANVIL_PORT: u16 = 8545; + +impl TestSetup { + async fn new() -> Result> { + // let wallet = anvil.wallet().unwrap(); + let provider = ProviderBuilder::new().connect_anvil_with_wallet_and_config(|anvil| { + anvil + .port(ANVIL_PORT) + .prague() + .chain_id(31337) + .block_time(1) + })?; + // .wallet(wallet) + // .connect_http(anvil.endpoint_url()); + + // Deploy the contract directly with sol! macro + let contract = MockERC20::deploy( + provider.clone().erased(), + "TestToken".to_string(), + "TEST".to_string(), + ) + .await?; + + println!("✅ Deployed MockERC20 at: {}", contract.address()); + let chain = TestChain { + chain_id: 31337, // Anvil default chain ID + provider: provider.clone().erased(), + rpc_url: "http://localhost:8545".parse()?, + }; + + // Create random credentials for our test EOAs + let executor_credentials = SigningCredential::random_local(); + let developer_credentials = SigningCredential::random_local(); + let user_credentials = SigningCredential::random_local(); + + // Extract addresses from the credentials + let executor_address = match &executor_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + let developer_address = match &developer_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + let user_address = match &user_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + + // Create a mock EoaSigner for tests - we'll implement the trait ourselves + let signer = MockEoaSigner; + + // Fund the test accounts + Self::fund_account(&chain, executor_address).await?; + + Ok(TestSetup { + chain, + executor_credentials, + developer_credentials, + user_credentials, + executor_address, + developer_address, + user_address, + signer, + mock_erc20_contract: contract, + anvil_provider: provider, + }) + } + + async fn fund_account( + chain: &TestChain, + address: Address, + ) -> Result<(), Box> { + // Use anvil_setBalance to fund the account + let balance = U256::from(100).pow(U256::from(18)); + let _: () = chain + .provider() + .client() + .request("anvil_setBalance", (address, format!("0x{:x}", balance))) + .await?; + + Ok(()) + } + + async fn fetch_and_set_bytecode(&self) -> Result<(), Box> { + // Fetch bytecode from Base Sepolia + let base_sepolia_url = "https://84532.rpc.thirdweb.com".parse()?; + let base_sepolia_provider = ProviderBuilder::new().connect_http(base_sepolia_url); + + let bytecode = base_sepolia_provider + .get_code_at(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS) + .await?; + // Set bytecode on our Anvil chain + let _: () = self + .anvil_provider + .anvil_set_code(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, bytecode) + .await?; + + println!( + "Set bytecode for minimal account implementation at {}", + MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS + ); + + Ok(()) + } + + async fn executor_broadcasts_authorization_for_account( + &self, + target_address: Address, // the account being delegated + authorization: SignedAuthorization, + ) -> Result<(), Box> { + // Executor pays gas to delegate someone else's account + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(target_address) // Self-transaction on target to trigger delegation + .with_value(U256::ZERO) + .with_authorization_list(vec![authorization]) + .with_gas_limit(100000) + .with_max_fee_per_gas(20_000_000_000u128) // 20 gwei + .with_max_priority_fee_per_gas(1_000_000_000u128); // 1 gwei + + // Get the current nonce for executor + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + let tx_request = tx_request.with_nonce(nonce); + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + // Convert to TypedTransaction for signing + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + // Executor signs the transaction + let signature_str = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => { + let sig = signer.sign_transaction(&mut typed_tx).await?; + sig.to_string() + } + _ => panic!("Expected PrivateKey credential for tests"), + }; + + // Parse signature and create signed transaction + let signature: alloy::primitives::Signature = signature_str.parse()?; + let signed_tx = typed_tx.into_signed(signature); + + // Send the transaction + let pending = self + .chain + .provider() + .send_tx_envelope(signed_tx.into()) + .await?; + let _receipt = pending.get_receipt().await?; + + Ok(()) + } + + async fn execute_wrapped_calls_via_delegated_account( + &self, + target_address: Address, + wrapped_calls: &Value, + signature: &str, + authorization: Option<&SignedAuthorization>, + ) -> Result> { + use engine_eip7702_core::transaction::executeWithSigCall; + + // Parse the wrapped calls back to the proper format + let wrapped_calls: WrappedCalls = serde_json::from_value(wrapped_calls.clone())?; + + // Create executeWithSig call data + let execute_call = executeWithSigCall { + wrappedCalls: wrapped_calls, + signature: signature.parse::()?, + }; + + let call_data = execute_call.abi_encode(); + + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(target_address) + .with_value(U256::ZERO) + .with_input(call_data) + .with_gas_limit(500000); + + // Add authorization if provided (for initial delegation) + let tx_request = if let Some(auth) = authorization { + tx_request.with_authorization_list(vec![auth.clone()]) + } else { + tx_request + }; + + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + let signature_str = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => { + let sig = signer.sign_transaction(&mut typed_tx).await?; + sig.to_string() + } + _ => panic!("Expected PrivateKey credential for tests"), + }; + + let signature: alloy::primitives::Signature = signature_str.parse()?; + let signed_tx = typed_tx.into_signed(signature); + + let pending = self + .anvil_provider + .send_tx_envelope(signed_tx.into()) + .await?; + let receipt = pending.get_receipt().await?; + + Ok(receipt) + } + + fn create_session_spec_for_granter_and_grantee(&self) -> SessionSpec { + use engine_eip7702_core::transaction::UsageLimit; + use std::time::{SystemTime, UNIX_EPOCH}; + + let expires_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + 3600; // 1 hour from now + + SessionSpec { + signer: self.developer_address, // developer (grantee) is the signer who can use this session + isWildcard: false, + expiresAt: U256::from(expires_at), + callPolicies: vec![CallSpec { + target: self.mock_erc20_contract.address().to_owned(), + selector: MockERC20::mintCall::SELECTOR.into(), + maxValuePerUse: U256::ZERO, + valueLimit: UsageLimit { + limitType: LimitType::Unlimited, + limit: U256::ZERO, + period: U256::ZERO, + }, + constraints: vec![], + }], + transferPolicies: vec![], + uid: [0u8; 32].into(), + } + } + + async fn user_signs_session_spec( + &self, + session_spec: &SessionSpec, + ) -> Result> { + use alloy::dyn_abi::TypedData; + use alloy::sol_types::eip712_domain; + + let domain = eip712_domain! { + name: "MinimalAccount", + version: "1", + chain_id: self.chain.chain_id(), + verifying_contract: self.user_address, + }; + + let typed_data = TypedData::from_struct(session_spec, Some(domain)); + + let signature = self + .signer + .sign_typed_data( + EoaSigningOptions { + from: self.user_address, + chain_id: Some(self.chain.chain_id()), + }, + &typed_data, + &self.user_credentials, + ) + .await?; + + Ok(signature) + } + + async fn executor_establishes_session_on_granter_account( + &self, + session_spec: &SessionSpec, + granter_session_signature: &str, + ) -> Result> { + use engine_eip7702_core::transaction::createSessionWithSigCall; + + let create_session_call = createSessionWithSigCall { + sessionSpec: session_spec.clone(), + signature: granter_session_signature.parse::()?, + }; + + let call_data = create_session_call.abi_encode(); + + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(self.user_address) // call createSessionWithSig on granter's delegated account + .with_value(U256::ZERO) + .with_input(call_data) + .with_gas_limit(500000); + + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + let tx_request = tx_request.with_nonce(nonce); + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + let signature = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => signer.sign_transaction(&mut typed_tx).await?, + _ => panic!("Expected PrivateKey credential for tests"), + }; + + let signed_tx = typed_tx.into_signed(signature); + + let pending = self + .chain + .provider() + .send_tx_envelope(signed_tx.into()) + .await?; + let receipt = pending.get_receipt().await?; + + Ok(receipt) + } +} + +#[tokio::test] +async fn test_eip7702_integration() -> Result<(), Box> { + // Set up test environment + let setup = TestSetup::new().await?; + + // Step 1: Fetch and set bytecode from Base Sepolia + setup.fetch_and_set_bytecode().await?; + // Step 2: Create delegated accounts for each EOA + let developer_account = DelegatedAccount::new(setup.developer_address, setup.chain.clone()); + let user_account = DelegatedAccount::new(setup.user_address, setup.chain.clone()); + + // Step 3: Test is_minimal_account - all should be false initially + assert!( + !developer_account.is_minimal_account().await?, + "Developer should not be minimal account initially" + ); + assert!( + !user_account.is_minimal_account().await?, + "User should not be minimal account initially" + ); + println!("✓ All accounts are not minimal accounts initially"); + + // Step 7: Test owner_transaction - developer mints tokens to themselves + let mint_call_data = MockERC20::mintCall { + amount: U256::from(1000), + } + .abi_encode(); + + let erc20_address = setup.mock_erc20_contract.address().to_owned(); + + let mint_transaction = InnerTransaction { + to: Some(erc20_address), + data: mint_call_data.into(), + value: U256::ZERO, + gas_limit: None, + transaction_type_data: None, + }; + + let developer_tx = developer_account + .clone() + .owner_transaction(&[mint_transaction]) + .add_authorization_if_needed(&setup.signer, &setup.developer_credentials) + .await?; + + let (wrapped_calls_json, signature) = developer_tx + .build(&setup.signer, &setup.developer_credentials) + .await?; + + // Execute the wrapped calls via executeWithSig on the delegated developer account + setup + .execute_wrapped_calls_via_delegated_account( + setup.developer_address, + &wrapped_calls_json, + &signature, + developer_tx.authorization(), + ) + .await?; + + // Check developer's balance - this should now reflect the executed wrapped calls + let developer_balance = setup + .mock_erc20_contract + .balanceOf(setup.developer_address) + .call() + .await?; + assert_eq!( + developer_balance, + U256::from(1000), + "Developer should have 1000 tokens" + ); + println!( + "✓ Developer successfully minted tokens using owner_transaction via EIP-7702 delegation" + ); + + assert!( + developer_account.is_minimal_account().await?, + "Developer should be minimal account after minting" + ); + + // Step 8: Delegate user account (session key granter) + // User signs authorization but executor broadcasts it (user has no funds) + let user_authorization = user_account + .sign_authorization(&setup.signer, &setup.user_credentials) + .await?; + + // Executor broadcasts the user's delegation transaction + setup + .executor_broadcasts_authorization_for_account(setup.user_address, user_authorization) + .await?; + + assert!( + user_account.is_minimal_account().await?, + "User (session key granter) should be minimal account after delegation" + ); + println!("✓ User (session key granter) is now a minimal account (delegated by executor)"); + + // Step 9: Developer is already delegated via add_authorization_if_needed in owner_transaction + assert!( + developer_account.is_minimal_account().await?, + "Developer (session key grantee) should already be minimal account from earlier delegation" + ); + println!("✓ Developer (session key grantee) was already delegated in previous step"); + + // Step 10: User (granter) creates and signs session spec allowing developer (grantee) to mint tokens + let session_spec = setup.create_session_spec_for_granter_and_grantee(); + + // User (granter) signs the session spec (EIP-712 signature) + let granter_session_signature = setup.user_signs_session_spec(&session_spec).await?; + + // Executor calls createSessionWithSig on the granter's (user's) delegated account to establish the session + let _receipt = setup + .executor_establishes_session_on_granter_account(&session_spec, &granter_session_signature) + .await?; + + println!( + "✓ Session key established: developer (grantee) can now act on behalf of user (granter)" + ); + + // Step 11: Developer (grantee) creates transaction to mint tokens for user (granter) + let mint_for_granter_transaction = InnerTransaction { + to: Some(erc20_address), + data: MockERC20::mintCall { + amount: U256::from(500), + } + .abi_encode() + .into(), + value: U256::ZERO, + gas_limit: Some(100000), + transaction_type_data: None, + }; + + // Developer (grantee) creates session_key_transaction to act on behalf of user (granter) + let grantee_session_tx = developer_account.clone().session_key_transaction( + setup.user_address, // granter's account (target) + &[mint_for_granter_transaction], + ); + + let (grantee_wrapped_calls, grantee_signature) = grantee_session_tx + .build(&setup.signer, &setup.developer_credentials) + .await?; + + // Executor executes the session: calls executeWithSig on grantee's account + // which then calls execute on granter's account + setup + .execute_wrapped_calls_via_delegated_account( + setup.developer_address, // grantee's account (where executeWithSig is called) + &grantee_wrapped_calls, + &grantee_signature, + None, // No authorization needed for grantee account (already delegated) + ) + .await?; + + sleep(Duration::from_secs(1)).await; + + // Check granter's (user's) balance increased from session key transaction + let granter_balance = setup + .mock_erc20_contract + .balanceOf(setup.user_address) + .call() + .await?; + assert_eq!( + granter_balance, + U256::from(500), + "Granter (user) should have 500 tokens from session key transaction" + ); + println!( + "✓ Session key transaction successful: grantee (developer) minted tokens for granter (user)" + ); + + println!("✓ All EIP-7702 integration tests passed!"); + + Ok(()) +} diff --git a/executors/src/eip7702_executor/send.rs b/executors/src/eip7702_executor/send.rs index 3588849..a70f5d2 100644 --- a/executors/src/eip7702_executor/send.rs +++ b/executors/src/eip7702_executor/send.rs @@ -13,7 +13,7 @@ use engine_core::{ use engine_eip7702_core::delegated_account::DelegatedAccount; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{sync::Arc, time::Duration}; +use std::{ops::Deref, sync::Arc, time::Duration}; use twmq::{ FailHookData, NackHookData, Queue, SuccessHookData, UserCancellable, error::TwmqError, @@ -227,52 +227,40 @@ where Eip7702ExecutionOptions::SessionKey(s) => Some(s.account_address), }); - let mut transactions = match session_key_target_address { + let transactions = match session_key_target_address { Some(target_address) => { account.session_key_transaction(target_address, &job_data.transactions) } None => account.owner_transaction(&job_data.transactions), }; - let is_authorization_needed = !transactions - .account() - .is_minimal_account() + let transactions = transactions + .add_authorization_if_needed(self.eoa_signer.deref(), &job_data.signing_credential) .await - .map_err(|e| Eip7702SendError::DelegationCheckFailed { inner_error: e }) - .map_err_with_max_retries( - Some(Duration::from_secs(2)), - twmq::job::RequeuePosition::Last, - 3, - job.attempts(), - )?; - - if is_authorization_needed { - let authorization = transactions - .account() - .sign_authorization(&self.eoa_signer, &job_data.signing_credential) - .await - .map_err(|e| { - let mapped_error = Eip7702SendError::SigningFailed { + .map_err(|e| { + let mapped_error = match e { + EngineError::RpcError { .. } => Eip7702SendError::DelegationCheckFailed { inner_error: e.clone(), - }; - - if is_build_error_retryable(&e) { - mapped_error.nack_with_max_retries( - Some(Duration::from_secs(2)), - twmq::job::RequeuePosition::Last, - 3, - job.attempts(), - ) - } else { - mapped_error.fail() - } - })?; + }, + _ => Eip7702SendError::SigningFailed { + inner_error: e.clone(), + }, + }; - transactions.set_authorization(authorization); - } + if is_build_error_retryable(&e) { + mapped_error.nack_with_max_retries( + Some(Duration::from_secs(2)), + twmq::job::RequeuePosition::Last, + 3, + job.attempts(), + ) + } else { + mapped_error.fail() + } + })?; let (wrapped_calls, signature) = transactions - .build(&self.eoa_signer, &job_data.signing_credential) + .build(self.eoa_signer.deref(), &job_data.signing_credential) .await .map_err(|e| Eip7702SendError::SigningFailed { inner_error: e }) .map_err_fail()?; From b8c90b8a4b264f6b21504a3d212003d5eea162c9 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Wed, 23 Jul 2025 13:00:24 +0530 Subject: [PATCH 6/6] remove bad dbg --- server/src/http/extractors.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/src/http/extractors.rs b/server/src/http/extractors.rs index cb5d5e7..928c8f7 100644 --- a/server/src/http/extractors.rs +++ b/server/src/http/extractors.rs @@ -184,10 +184,7 @@ impl SigningCredentialsExtractor { .resource .path_split() .last() - .map(|id| { - dbg!(&id); - id.to_string() - }) + .map(|id| id.to_string()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { message: "KMS ARN must contain a valid key ID in the resource part".to_string(), @@ -201,8 +198,6 @@ impl SigningCredentialsExtractor { }) })?; - dbg!(®ion); - Ok((key_id, region.to_string())) }