Skip to content

Commit 242e11b

Browse files
committed
webhook metadata + eoa retry changes
1 parent a36e121 commit 242e11b

File tree

10 files changed

+188
-40
lines changed

10 files changed

+188
-40
lines changed

aa-types/src/userop.rs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use alloy::{
22
core::sol_types::SolValue,
3-
primitives::{keccak256, Address, ChainId, Bytes, U256, B256},
3+
primitives::{Address, B256, Bytes, ChainId, U256, keccak256},
44
rpc::types::{PackedUserOperation, UserOperation},
55
};
66
use serde::{Deserialize, Serialize};
@@ -14,7 +14,15 @@ pub enum VersionedUserOp {
1414
}
1515

1616
/// Error type for UserOp operations
17-
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
17+
#[derive(
18+
Debug,
19+
Clone,
20+
thiserror::Error,
21+
serde::Serialize,
22+
serde::Deserialize,
23+
schemars::JsonSchema,
24+
utoipa::ToSchema,
25+
)]
1826
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
1927
pub enum UserOpError {
2028
#[error("Unexpected error: {0}")]
@@ -68,7 +76,12 @@ pub fn compute_user_op_v07_hash(
6876
// Construct initCode from factory and factoryData
6977
let init_code: Bytes = if let Some(factory) = op.factory {
7078
if factory != Address::ZERO {
71-
[&factory[..], &op.factory_data.clone().unwrap_or_default()[..]].concat().into()
79+
[
80+
&factory[..],
81+
&op.factory_data.clone().unwrap_or_default()[..],
82+
]
83+
.concat()
84+
.into()
7285
} else {
7386
op.factory_data.clone().unwrap_or_default()
7487
}
@@ -80,9 +93,10 @@ pub fn compute_user_op_v07_hash(
8093
let vgl_u128: u128 = op.verification_gas_limit.try_into().map_err(|_| {
8194
UserOpError::UnexpectedError("verification_gas_limit too large".to_string())
8295
})?;
83-
let cgl_u128: u128 = op.call_gas_limit.try_into().map_err(|_| {
84-
UserOpError::UnexpectedError("call_gas_limit too large".to_string())
85-
})?;
96+
let cgl_u128: u128 = op
97+
.call_gas_limit
98+
.try_into()
99+
.map_err(|_| UserOpError::UnexpectedError("call_gas_limit too large".to_string()))?;
86100

87101
let mut account_gas_limits_bytes = [0u8; 32];
88102
account_gas_limits_bytes[0..16].copy_from_slice(&vgl_u128.to_be_bytes());
@@ -93,9 +107,10 @@ pub fn compute_user_op_v07_hash(
93107
let mpfpg_u128: u128 = op.max_priority_fee_per_gas.try_into().map_err(|_| {
94108
UserOpError::UnexpectedError("max_priority_fee_per_gas too large".to_string())
95109
})?;
96-
let mfpg_u128: u128 = op.max_fee_per_gas.try_into().map_err(|_| {
97-
UserOpError::UnexpectedError("max_fee_per_gas too large".to_string())
98-
})?;
110+
let mfpg_u128: u128 = op
111+
.max_fee_per_gas
112+
.try_into()
113+
.map_err(|_| UserOpError::UnexpectedError("max_fee_per_gas too large".to_string()))?;
99114

100115
let mut gas_fees_bytes = [0u8; 32];
101116
gas_fees_bytes[0..16].copy_from_slice(&mpfpg_u128.to_be_bytes());
@@ -105,12 +120,24 @@ pub fn compute_user_op_v07_hash(
105120
// Construct paymasterAndData
106121
let paymaster_and_data: Bytes = if let Some(paymaster) = op.paymaster {
107122
if paymaster != Address::ZERO {
108-
let pm_vgl_u128: u128 = op.paymaster_verification_gas_limit.unwrap_or_default().try_into().map_err(|_| {
109-
UserOpError::UnexpectedError("paymaster_verification_gas_limit too large".to_string())
110-
})?;
111-
let pm_pogl_u128: u128 = op.paymaster_post_op_gas_limit.unwrap_or_default().try_into().map_err(|_| {
112-
UserOpError::UnexpectedError("paymaster_post_op_gas_limit too large".to_string())
113-
})?;
123+
let pm_vgl_u128: u128 = op
124+
.paymaster_verification_gas_limit
125+
.unwrap_or_default()
126+
.try_into()
127+
.map_err(|_| {
128+
UserOpError::UnexpectedError(
129+
"paymaster_verification_gas_limit too large".to_string(),
130+
)
131+
})?;
132+
let pm_pogl_u128: u128 = op
133+
.paymaster_post_op_gas_limit
134+
.unwrap_or_default()
135+
.try_into()
136+
.map_err(|_| {
137+
UserOpError::UnexpectedError(
138+
"paymaster_post_op_gas_limit too large".to_string(),
139+
)
140+
})?;
114141
[
115142
&paymaster[..],
116143
&pm_vgl_u128.to_be_bytes()[..],
@@ -154,4 +181,4 @@ pub fn compute_user_op_v07_hash(
154181
let outer_encoded = outer_tuple.abi_encode();
155182
let final_hash = keccak256(&outer_encoded);
156183
Ok(final_hash)
157-
}
184+
}

core/src/execution_options/mod.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,42 @@ pub struct ExecutionOptions {
7676
pub specific: SpecificExecutionOptions,
7777
}
7878

79+
const MAX_USER_METADATA_SIZE: usize = 4096; // 4KB limit
80+
81+
fn validate_user_metadata_size(metadata: &Option<String>) -> Result<(), String> {
82+
if let Some(meta) = metadata {
83+
if meta.len() > MAX_USER_METADATA_SIZE {
84+
return Err(format!(
85+
"User metadata exceeds maximum size of {} bytes (provided: {} bytes)",
86+
MAX_USER_METADATA_SIZE,
87+
meta.len()
88+
));
89+
}
90+
}
91+
Ok(())
92+
}
93+
94+
fn deserialize_and_validate_user_metadata<'de, D>(
95+
deserializer: D,
96+
) -> Result<Option<String>, D::Error>
97+
where
98+
D: Deserializer<'de>,
99+
{
100+
let metadata: Option<String> = Option::deserialize(deserializer)?;
101+
validate_user_metadata_size(&metadata).map_err(D::Error::custom)?;
102+
Ok(metadata)
103+
}
104+
79105
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, utoipa::ToSchema)]
106+
#[serde(rename_all = "camelCase")]
80107
pub struct WebhookOptions {
81108
pub url: String,
82109
pub secret: Option<String>,
110+
/// Custom metadata provided by the user to be included in webhook notifications.
111+
/// Limited to 4KB (4096 bytes) to prevent abuse.
112+
#[serde(default, skip_serializing_if = "Option::is_none")]
113+
#[serde(deserialize_with = "deserialize_and_validate_user_metadata")]
114+
pub user_metadata: Option<String>,
83115
}
84116

85117
/// Incoming transaction request, parsed into InnerTransaction
@@ -152,3 +184,42 @@ impl ExecutionOptions {
152184
&self.base.idempotency_key
153185
}
154186
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::*;
191+
192+
#[test]
193+
fn test_webhook_options_user_metadata_validation() {
194+
// Test valid metadata
195+
let valid_json = r#"{
196+
"url": "https://example.com/webhook",
197+
"secret": "test_secret",
198+
"userMetadata": "test metadata"
199+
}"#;
200+
201+
let webhook_options: Result<WebhookOptions, _> = serde_json::from_str(valid_json);
202+
assert!(webhook_options.is_ok());
203+
assert_eq!(webhook_options.unwrap().user_metadata, Some("test metadata".to_string()));
204+
205+
// Test metadata that's too large (over 4KB)
206+
let large_metadata = "x".repeat(5000); // 5KB string
207+
let invalid_json = format!(r#"{{
208+
"url": "https://example.com/webhook",
209+
"secret": "test_secret",
210+
"userMetadata": "{}"
211+
}}"#, large_metadata);
212+
213+
let webhook_options: Result<WebhookOptions, _> = serde_json::from_str(&invalid_json);
214+
assert!(webhook_options.is_err());
215+
216+
// Test missing metadata (should default to None)
217+
let minimal_json = r#"{
218+
"url": "https://example.com/webhook"
219+
}"#;
220+
221+
let webhook_options: Result<WebhookOptions, _> = serde_json::from_str(minimal_json);
222+
assert!(webhook_options.is_ok());
223+
assert_eq!(webhook_options.unwrap().user_metadata, None);
224+
}
225+
}

executors/src/eoa/store/hydrate.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{HashMap, HashSet, VecDeque};
1+
use std::collections::HashMap;
22

33
use crate::eoa::{
44
EoaExecutorStore, EoaTransactionRequest,

executors/src/eoa/worker/error.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
use alloy::transports::{RpcError, TransportErrorKind};
1+
use std::time::Duration;
2+
3+
use alloy::{
4+
primitives::U256,
5+
transports::{RpcError, TransportErrorKind},
6+
};
27
use engine_core::{
38
chain::Chain,
49
error::{AlloyRpcErrorToEngineError, EngineError, RpcErrorKind},
510
};
611
use serde::{Deserialize, Serialize};
712
use thirdweb_core::iaw::IAWError;
8-
use twmq::{UserCancellable, error::TwmqError};
13+
use twmq::{
14+
UserCancellable,
15+
error::TwmqError,
16+
job::{JobError, RequeuePosition},
17+
};
918

1019
use crate::eoa::{
11-
EoaTransactionRequest,
12-
store::{
13-
BorrowedTransaction, BorrowedTransactionData, SubmissionResult, SubmissionResultType,
14-
SubmittedTransaction, TransactionStoreError,
15-
},
20+
store::{BorrowedTransaction, SubmissionResult, SubmissionResultType, TransactionStoreError},
1621
worker::EoaExecutorWorkerResult,
1722
};
1823

@@ -64,13 +69,40 @@ pub enum EoaExecutorWorkerError {
6469
#[error("Work still remaining: {result:?}")]
6570
WorkRemaining { result: EoaExecutorWorkerResult },
6671

72+
#[error("EOA out of funds: {balance} < {balance_threshold} wei")]
73+
EoaOutOfFunds {
74+
balance: U256,
75+
balance_threshold: U256,
76+
},
77+
6778
#[error("Internal error: {message}")]
6879
InternalError { message: String },
6980

7081
#[error("User cancelled")]
7182
UserCancelled,
7283
}
7384

85+
impl EoaExecutorWorkerError {
86+
pub fn handle(self) -> JobError<EoaExecutorWorkerError> {
87+
match &self {
88+
EoaExecutorWorkerError::EoaOutOfFunds { .. } => JobError::Nack {
89+
error: self,
90+
delay: Some(Duration::from_secs(60)),
91+
position: RequeuePosition::Last,
92+
},
93+
EoaExecutorWorkerError::StoreError {
94+
inner_error: TransactionStoreError::LockLost { .. },
95+
..
96+
} => JobError::Fail(self),
97+
_ => JobError::Nack {
98+
error: self,
99+
delay: Some(Duration::from_secs(10)),
100+
position: RequeuePosition::Last,
101+
},
102+
}
103+
}
104+
}
105+
74106
impl From<TwmqError> for EoaExecutorWorkerError {
75107
fn from(error: TwmqError) -> Self {
76108
EoaExecutorWorkerError::InternalError {
@@ -220,6 +252,7 @@ pub fn is_retryable_preparation_error(error: &EoaExecutorWorkerError) -> bool {
220252
EoaExecutorWorkerError::TransactionSendError { .. } => false, // Different context
221253
EoaExecutorWorkerError::SignatureParsingFailed { .. } => false, // Deterministic
222254
EoaExecutorWorkerError::WorkRemaining { .. } => false, // Different context
255+
EoaExecutorWorkerError::EoaOutOfFunds { .. } => false, // Deterministic
223256
}
224257
}
225258

executors/src/eoa/worker/mod.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ where
153153
)
154154
.acquire_eoa_lock_aggressively(&job.lease_token)
155155
.await
156-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
156+
.map_err(|e| Into::<EoaExecutorWorkerError>::into(e).handle())?;
157157

158158
let worker = EoaExecutorWorker {
159159
store: scoped,
@@ -169,7 +169,9 @@ where
169169
};
170170

171171
let result = worker.execute_main_workflow().await?;
172-
worker.release_eoa_lock().await;
172+
if let Err(e) = worker.release_eoa_lock().await {
173+
tracing::error!("Error releasing EOA lock: {}", e);
174+
}
173175

174176
if result.is_work_remaining() {
175177
Err(EoaExecutorWorkerError::WorkRemaining { result })
@@ -267,7 +269,7 @@ impl<C: Chain> EoaExecutorWorker<C> {
267269
tracing::error!("Error in recover_borrowed_state: {}", e);
268270
e
269271
})
270-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
272+
.map_err(|e| e.handle())?;
271273

272274
// 2. CONFIRM FLOW
273275
let confirmations_report = self
@@ -277,7 +279,7 @@ impl<C: Chain> EoaExecutorWorker<C> {
277279
tracing::error!("Error in confirm flow: {}", e);
278280
e
279281
})
280-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
282+
.map_err(|e| e.handle())?;
281283

282284
// 3. SEND FLOW
283285
let sent = self
@@ -287,7 +289,7 @@ impl<C: Chain> EoaExecutorWorker<C> {
287289
tracing::error!("Error in send_flow: {}", e);
288290
e
289291
})
290-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
292+
.map_err(|e| e.handle())?;
291293

292294
// 4. CHECK FOR REMAINING WORK
293295
let pending_count = self
@@ -298,8 +300,9 @@ impl<C: Chain> EoaExecutorWorker<C> {
298300
tracing::error!("Error in peek_pending_transactions: {}", e);
299301
e
300302
})
301-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?
303+
.map_err(|e| Into::<EoaExecutorWorkerError>::into(e).handle())?
302304
.len();
305+
303306
let borrowed_count = self
304307
.store
305308
.peek_borrowed_transactions()
@@ -308,8 +311,9 @@ impl<C: Chain> EoaExecutorWorker<C> {
308311
tracing::error!("Error in peek_borrowed_transactions: {}", e);
309312
e
310313
})
311-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?
314+
.map_err(|e| Into::<EoaExecutorWorkerError>::into(e).handle())?
312315
.len();
316+
313317
let recycled_count = self
314318
.store
315319
.peek_recycled_nonces()
@@ -318,8 +322,9 @@ impl<C: Chain> EoaExecutorWorker<C> {
318322
tracing::error!("Error in peek_recycled_nonces: {}", e);
319323
e
320324
})
321-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?
325+
.map_err(|e| Into::<EoaExecutorWorkerError>::into(e).handle())?
322326
.len();
327+
323328
let submitted_count = self
324329
.store
325330
.get_submitted_transactions_count()
@@ -328,7 +333,7 @@ impl<C: Chain> EoaExecutorWorker<C> {
328333
tracing::error!("Error in get_submitted_transactions_count: {}", e);
329334
e
330335
})
331-
.map_err_nack(Some(Duration::from_secs(10)), RequeuePosition::Last)?;
336+
.map_err(|e| Into::<EoaExecutorWorkerError>::into(e).handle())?;
332337

333338
Ok(EoaExecutorWorkerResult {
334339
recovered_transactions: recovered,
@@ -493,7 +498,8 @@ impl<C: Chain> EoaExecutorWorker<C> {
493498
Ok(())
494499
}
495500

496-
async fn release_eoa_lock(self) {
497-
self.store.release_eoa_lock().await;
501+
async fn release_eoa_lock(self) -> Result<(), EoaExecutorWorkerError> {
502+
self.store.release_eoa_lock().await?;
503+
Ok(())
498504
}
499505
}

executors/src/eoa/worker/send.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ impl<C: Chain> EoaExecutorWorker<C> {
4949
"EOA has insufficient balance (<= {} wei), skipping send flow",
5050
health.balance_threshold
5151
);
52-
return Ok(0);
52+
return Err(EoaExecutorWorkerError::EoaOutOfFunds {
53+
balance: health.balance,
54+
balance_threshold: health.balance_threshold,
55+
});
5356
}
5457
}
5558

executors/src/eoa/worker/transaction.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use crate::eoa::{
2323
EoaTransactionRequest,
2424
store::{
2525
BorrowedTransaction, BorrowedTransactionData, PendingTransaction, SubmittedNoopTransaction,
26-
TransactionData,
2726
},
2827
worker::{
2928
EoaExecutorWorker,

0 commit comments

Comments
 (0)