Skip to content

Commit 426ff32

Browse files
committed
wire up executor to API
1 parent e3758fc commit 426ff32

File tree

18 files changed

+961
-101
lines changed

18 files changed

+961
-101
lines changed

core/src/chain.rs

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -126,48 +126,73 @@ impl Chain for ThirdwebChain {
126126

127127
impl ThirdwebChainConfig<'_> {
128128
pub fn to_chain(&self) -> Result<ThirdwebChain, EngineError> {
129-
let rpc_url = Url::parse(&format!(
130-
"https://{chain_id}.{base_url}/{client_id}",
131-
chain_id = self.chain_id,
132-
base_url = self.rpc_base_url,
133-
client_id = self.client_id,
134-
))
135-
.map_err(|e| EngineError::RpcConfigError {
136-
message: format!("Failed to parse RPC URL: {}", e),
137-
})?;
138-
139-
let bundler_url = Url::parse(&format!(
140-
"https://{chain_id}.{base_url}/v2",
141-
chain_id = self.chain_id,
142-
base_url = self.bundler_base_url,
143-
))
144-
.map_err(|e| EngineError::RpcConfigError {
145-
message: format!("Failed to parse Bundler URL: {}", e),
146-
})?;
147-
148-
let paymaster_url = Url::parse(&format!(
149-
"https://{chain_id}.{base_url}/v2",
150-
chain_id = self.chain_id,
151-
base_url = self.paymaster_base_url,
152-
))
153-
.map_err(|e| EngineError::RpcConfigError {
154-
message: format!("Failed to parse Paymaster URL: {}", e),
155-
})?;
129+
// Special handling for chain ID 31337 (local anvil)
130+
let (rpc_url, bundler_url, paymaster_url) = if self.chain_id == 31337 {
131+
// For local anvil, use localhost URLs
132+
let local_rpc_url = "http://127.0.0.1:8545";
133+
let rpc_url = Url::parse(local_rpc_url).map_err(|e| EngineError::RpcConfigError {
134+
message: format!("Failed to parse local anvil RPC URL: {}", e),
135+
})?;
136+
137+
// For bundler and paymaster, use the same local RPC URL
138+
// since anvil doesn't have separate bundler/paymaster services
139+
let bundler_url = rpc_url.clone();
140+
let paymaster_url = rpc_url.clone();
141+
142+
(rpc_url, bundler_url, paymaster_url)
143+
} else {
144+
// Standard URL construction for other chains
145+
let rpc_url = Url::parse(&format!(
146+
"https://{chain_id}.{base_url}/{client_id}",
147+
chain_id = self.chain_id,
148+
base_url = self.rpc_base_url,
149+
client_id = self.client_id,
150+
))
151+
.map_err(|e| EngineError::RpcConfigError {
152+
message: format!("Failed to parse RPC URL: {}", e),
153+
})?;
154+
155+
let bundler_url = Url::parse(&format!(
156+
"https://{chain_id}.{base_url}/v2",
157+
chain_id = self.chain_id,
158+
base_url = self.bundler_base_url,
159+
))
160+
.map_err(|e| EngineError::RpcConfigError {
161+
message: format!("Failed to parse Bundler URL: {}", e),
162+
})?;
163+
164+
let paymaster_url = Url::parse(&format!(
165+
"https://{chain_id}.{base_url}/v2",
166+
chain_id = self.chain_id,
167+
base_url = self.paymaster_base_url,
168+
))
169+
.map_err(|e| EngineError::RpcConfigError {
170+
message: format!("Failed to parse Paymaster URL: {}", e),
171+
})?;
172+
173+
(rpc_url, bundler_url, paymaster_url)
174+
};
156175

157176
let mut sensitive_headers = HeaderMap::new();
158-
sensitive_headers.insert(
159-
"x-client-id",
160-
HeaderValue::from_str(self.client_id).map_err(|e| EngineError::RpcConfigError {
161-
message: format!("Unserialisable client-id used: {e}"),
162-
})?,
163-
);
164-
165-
sensitive_headers.insert(
166-
"x-secret-key",
167-
HeaderValue::from_str(self.secret_key).map_err(|e| EngineError::RpcConfigError {
168-
message: format!("Unserialisable secret-key used: {e}"),
169-
})?,
170-
);
177+
178+
// Only add auth headers for non-local chains
179+
if self.chain_id != 31337 {
180+
sensitive_headers.insert(
181+
"x-client-id",
182+
HeaderValue::from_str(self.client_id).map_err(|e| EngineError::RpcConfigError {
183+
message: format!("Unserialisable client-id used: {e}"),
184+
})?,
185+
);
186+
187+
sensitive_headers.insert(
188+
"x-secret-key",
189+
HeaderValue::from_str(self.secret_key).map_err(|e| {
190+
EngineError::RpcConfigError {
191+
message: format!("Unserialisable secret-key used: {e}"),
192+
}
193+
})?,
194+
);
195+
}
171196

172197
let reqwest_client =
173198
HttpClientBuilder::new()
@@ -181,10 +206,19 @@ impl ThirdwebChainConfig<'_> {
181206
let paymaster_transport = transport_builder.default_transport(paymaster_url.clone());
182207
let bundler_transport = transport_builder.default_transport(bundler_url.clone());
183208

184-
let sensitive_bundler_transport =
185-
transport_builder.with_headers(bundler_url.clone(), sensitive_headers.clone());
186-
let sensitive_paymaster_transport =
187-
transport_builder.with_headers(paymaster_url.clone(), sensitive_headers);
209+
let sensitive_bundler_transport = if self.chain_id == 31337 {
210+
// For local anvil, use the same transport as non-sensitive
211+
transport_builder.default_transport(bundler_url.clone())
212+
} else {
213+
transport_builder.with_headers(bundler_url.clone(), sensitive_headers.clone())
214+
};
215+
216+
let sensitive_paymaster_transport = if self.chain_id == 31337 {
217+
// For local anvil, use the same transport as non-sensitive
218+
transport_builder.default_transport(paymaster_url.clone())
219+
} else {
220+
transport_builder.with_headers(paymaster_url.clone(), sensitive_headers)
221+
};
188222

189223
let paymaster_rpc_client = RpcClient::builder().transport(paymaster_transport, false);
190224
let bundler_rpc_client = RpcClient::builder().transport(bundler_transport, false);

core/src/error.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ pub enum RpcErrorKind {
5656
#[error("HTTP error {status}")]
5757
TransportHttpError { status: u16, body: String },
5858

59-
#[error("Other transport error: {0}")]
60-
OtherTransportError(String),
59+
#[error("Other transport error: {message}")]
60+
OtherTransportError { message: String },
6161
}
6262

6363
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, utoipa::ToSchema)]
@@ -345,8 +345,12 @@ fn to_engine_rpc_error_kind(err: &AlloyRpcError<TransportErrorKind>) -> RpcError
345345
status: err.status,
346346
body: err.body.to_string(),
347347
},
348-
TransportErrorKind::Custom(err) => RpcErrorKind::OtherTransportError(err.to_string()),
349-
_ => RpcErrorKind::OtherTransportError(err.to_string()),
348+
TransportErrorKind::Custom(err) => RpcErrorKind::OtherTransportError {
349+
message: err.to_string(),
350+
},
351+
_ => RpcErrorKind::OtherTransportError {
352+
message: err.to_string(),
353+
},
350354
},
351355
}
352356
}

core/src/execution_options/eoa.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use crate::defs::AddressDef;
2+
use alloy::eips::eip7702::SignedAuthorization;
3+
use alloy::primitives::{Address, U256};
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
/// ### EOA Execution Options
8+
/// This struct configures EOA (Externally Owned Account) direct execution.
9+
///
10+
/// EOA execution sends transactions directly from an EOA address without
11+
/// smart contract abstraction. This is the most basic form of transaction
12+
/// execution and is suitable for simple transfers and contract interactions.
13+
///
14+
/// ### Use Cases
15+
/// - Direct ETH transfers
16+
/// - Simple contract interactions
17+
/// - Gas-efficient transactions
18+
/// - When smart account features are not needed
19+
///
20+
/// ### Features
21+
/// - Direct transaction execution from EOA
22+
/// - Automatic nonce management
23+
/// - Gas price optimization
24+
/// - Transaction confirmation tracking
25+
/// - Retry and recovery mechanisms
26+
/// - Support for EIP-1559, EIP-2930, and Legacy transactions
27+
/// - Support for EIP-7702 delegated transactions
28+
#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
29+
#[serde(rename_all = "camelCase")]
30+
pub struct EoaExecutionOptions {
31+
/// The EOA address to send transactions from
32+
/// This account must have sufficient balance to pay for gas and transaction value
33+
#[schemars(with = "AddressDef")]
34+
#[schema(value_type = AddressDef)]
35+
pub from: Address,
36+
37+
/// The gas limit to use for the transaction
38+
/// If not provided, the system will auto-detect the best gas limit
39+
#[schemars(with = "Option<u64>")]
40+
#[schema(value_type = Option<u64>)]
41+
pub gas_limit: Option<u64>,
42+
43+
// /// Maximum number of in-flight transactions for this EOA
44+
// /// Controls how many transactions can be pending confirmation at once
45+
// /// Defaults to 100 if not specified
46+
// #[serde(default = "default_max_inflight")]
47+
// pub max_inflight: u64,
48+
49+
// /// Maximum number of recycled nonces to keep
50+
// /// When transactions fail, their nonces are recycled for reuse
51+
// /// Defaults to 50 if not specified
52+
// #[serde(default = "default_max_recycled_nonces")]
53+
// pub max_recycled_nonces: u64,
54+
/// Transaction type-specific data for gas configuration
55+
/// If not provided, the system will auto-detect the best transaction type
56+
#[serde(flatten)]
57+
pub transaction_type_data: Option<EoaTransactionTypeData>,
58+
}
59+
60+
/// EOA Transaction type-specific data for different EIP standards
61+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
62+
#[serde(untagged)]
63+
pub enum EoaTransactionTypeData {
64+
/// EIP-7702 transaction with authorization list and EIP-1559 gas pricing
65+
Eip7702(EoaSend7702JobData),
66+
/// EIP-1559 transaction with priority fee and max fee per gas
67+
Eip1559(EoaSend1559JobData),
68+
/// Legacy transaction with simple gas price
69+
Legacy(EoaSendLegacyJobData),
70+
}
71+
72+
/// EIP-7702 transaction configuration
73+
/// Allows delegation of EOA to smart contract logic temporarily
74+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
75+
#[serde(rename_all = "camelCase")]
76+
pub struct EoaSend7702JobData {
77+
/// List of signed authorizations for contract delegation
78+
/// Each authorization allows the EOA to temporarily delegate to a smart contract
79+
#[schemars(with = "Option<Vec<SignedAuthorizationSchema>>")]
80+
#[schema(value_type = Option<Vec<SignedAuthorizationSchema>>)]
81+
pub authorization_list: Option<Vec<SignedAuthorization>>,
82+
83+
/// Maximum fee per gas willing to pay (in wei)
84+
/// This is the total fee cap including base fee and priority fee
85+
pub max_fee_per_gas: Option<u128>,
86+
87+
/// Maximum priority fee per gas willing to pay (in wei)
88+
/// This is the tip paid to validators for transaction inclusion
89+
pub max_priority_fee_per_gas: Option<u128>,
90+
}
91+
92+
/// EIP-1559 transaction configuration
93+
/// Uses base fee + priority fee model for more predictable gas pricing
94+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
95+
#[serde(rename_all = "camelCase")]
96+
pub struct EoaSend1559JobData {
97+
/// Maximum fee per gas willing to pay (in wei)
98+
/// This is the total fee cap including base fee and priority fee
99+
pub max_fee_per_gas: Option<u128>,
100+
101+
/// Maximum priority fee per gas willing to pay (in wei)
102+
/// This is the tip paid to validators for transaction inclusion
103+
pub max_priority_fee_per_gas: Option<u128>,
104+
}
105+
106+
/// Legacy transaction configuration
107+
/// Uses simple gas price model (pre-EIP-1559)
108+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
109+
#[serde(rename_all = "camelCase")]
110+
pub struct EoaSendLegacyJobData {
111+
/// Gas price willing to pay (in wei)
112+
/// This is the total price per unit of gas for legacy transactions
113+
pub gas_price: Option<u128>,
114+
}
115+
116+
/// EIP-7702 Authorization structure for OpenAPI schema
117+
/// Represents an unsigned authorization that allows an EOA to delegate to a smart contract
118+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
119+
#[serde(rename_all = "camelCase")]
120+
pub struct AuthorizationSchema {
121+
/// The chain ID of the authorization
122+
/// Must match the chain where the transaction will be executed
123+
#[schemars(with = "String")]
124+
#[schema(value_type = String, example = "1")]
125+
pub chain_id: U256,
126+
127+
/// The smart contract address to delegate to
128+
/// This contract will be able to execute logic on behalf of the EOA
129+
#[schemars(with = "AddressDef")]
130+
#[schema(value_type = AddressDef)]
131+
pub address: Address,
132+
133+
/// The nonce for the authorization
134+
/// Must be the current nonce of the authorizing account
135+
#[schema(example = 42)]
136+
pub nonce: u64,
137+
}
138+
139+
/// EIP-7702 Signed Authorization structure for OpenAPI schema
140+
/// Contains an authorization plus the cryptographic signature
141+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
142+
#[serde(rename_all = "camelCase")]
143+
pub struct SignedAuthorizationSchema {
144+
/// The chain ID of the authorization
145+
/// Must match the chain where the transaction will be executed
146+
#[schemars(with = "String")]
147+
#[schema(value_type = String, example = "1")]
148+
pub chain_id: U256,
149+
150+
/// The smart contract address to delegate to
151+
/// This contract will be able to execute logic on behalf of the EOA
152+
#[schemars(with = "AddressDef")]
153+
#[schema(value_type = AddressDef)]
154+
pub address: Address,
155+
156+
/// The nonce for the authorization
157+
/// Must be the current nonce of the authorizing account
158+
#[schema(example = 42)]
159+
pub nonce: u64,
160+
161+
/// Signature parity value (0 or 1)
162+
/// Used for ECDSA signature recovery
163+
#[serde(rename = "yParity", alias = "v")]
164+
#[schema(example = 0)]
165+
pub y_parity: u8,
166+
167+
/// Signature r value
168+
/// First component of the ECDSA signature
169+
#[schemars(with = "String")]
170+
#[schema(value_type = String, example = "0x1234567890abcdef...")]
171+
pub r: U256,
172+
173+
/// Signature s value
174+
/// Second component of the ECDSA signature
175+
#[schemars(with = "String")]
176+
#[schema(value_type = String, example = "0xfedcba0987654321...")]
177+
pub s: U256,
178+
}
179+
180+
fn default_max_inflight() -> u64 {
181+
100
182+
}
183+
184+
fn default_max_recycled_nonces() -> u64 {
185+
50
186+
}

core/src/execution_options/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::collections::HashMap;
77
use crate::transaction::InnerTransaction;
88
pub mod aa;
99
pub mod auto;
10+
pub mod eoa;
1011

1112
// Base execution options for all transactions
1213
// All specific execution options share this
@@ -35,6 +36,10 @@ pub enum SpecificExecutionOptions {
3536

3637
#[schema(title = "ERC-4337 Execution Options")]
3738
ERC4337(aa::Erc4337ExecutionOptions),
39+
40+
#[serde(rename = "eoa")]
41+
#[schema(title = "EOA Execution Options")]
42+
EOA(eoa::EoaExecutionOptions),
3843
}
3944

4045
fn deserialize_with_default_auto<'de, D>(
@@ -118,13 +123,16 @@ pub struct QueuedTransactionsResponse {
118123
pub enum ExecutorType {
119124
#[serde(rename = "ERC4337")]
120125
Erc4337,
126+
#[serde(rename = "EOA")]
127+
Eoa,
121128
}
122129

123130
impl ExecutionOptions {
124131
pub fn executor_type(&self) -> ExecutorType {
125132
match &self.specific {
126133
SpecificExecutionOptions::ERC4337(_) => ExecutorType::Erc4337,
127134
SpecificExecutionOptions::Auto(_) => ExecutorType::Erc4337,
135+
SpecificExecutionOptions::EOA(_) => ExecutorType::Eoa,
128136
}
129137
}
130138

executors/src/eoa/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ pub mod error_classifier;
22
pub mod store;
33
pub mod worker;
44
pub use error_classifier::{EoaErrorMapper, EoaExecutionError, RecoveryStrategy};
5+
pub use store::{EoaExecutorStore, EoaTransactionRequest};
6+
pub use worker::{EoaExecutorWorker, EoaExecutorWorkerJobData};

0 commit comments

Comments
 (0)