1use alloy_primitives::{Address, Bytes, U256};
16use alloy_sol_types::{sol, SolCall};
17
18use crate::{Error, Result};
19
20pub const DATA_EDGE_ADDRESS: &str = "0xC445af1D4EB9fce4e1E61fE96ea7B8feBF03c5ca";
22
23pub const ENTRYPOINT_ADDRESS: &str = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
25
26pub const SIMPLE_ACCOUNT_FACTORY: &str = "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985";
28
29pub const MAX_BATCH_SIZE: usize = 15;
31
32sol! {
34 function execute(address dest, uint256 value, bytes data);
36
37 function executeBatch(address[] dest, uint256[] values, bytes[] data);
39}
40
41pub fn encode_single_call(protobuf_payload: &[u8]) -> Vec<u8> {
46 let dest: Address = DATA_EDGE_ADDRESS.parse().unwrap();
47 let value = U256::ZERO;
48 let data = Bytes::copy_from_slice(protobuf_payload);
49
50 let call = executeCall { dest, value, data };
51 call.abi_encode()
52}
53
54pub fn encode_batch_call(protobuf_payloads: &[Vec<u8>]) -> Result<Vec<u8>> {
59 if protobuf_payloads.is_empty() {
60 return Err(Error::Crypto("Batch must contain at least 1 payload".into()));
61 }
62 if protobuf_payloads.len() > MAX_BATCH_SIZE {
63 return Err(Error::Crypto(format!(
64 "Batch size {} exceeds maximum of {}",
65 protobuf_payloads.len(),
66 MAX_BATCH_SIZE
67 )));
68 }
69
70 if protobuf_payloads.len() == 1 {
72 return Ok(encode_single_call(&protobuf_payloads[0]));
73 }
74
75 let dest: Address = DATA_EDGE_ADDRESS.parse().unwrap();
76 let dests: Vec<Address> = vec![dest; protobuf_payloads.len()];
77 let values: Vec<U256> = vec![U256::ZERO; protobuf_payloads.len()];
78 let datas: Vec<Bytes> = protobuf_payloads
79 .iter()
80 .map(|p| Bytes::copy_from_slice(p))
81 .collect();
82
83 let call = executeBatchCall {
84 dest: dests,
85 values,
86 data: datas,
87 };
88 Ok(call.abi_encode())
89}
90
91#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct UserOperationV7 {
102 pub sender: String,
103 pub nonce: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub factory: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub factory_data: Option<String>,
108 pub call_data: String,
109 pub call_gas_limit: String,
110 pub verification_gas_limit: String,
111 pub pre_verification_gas: String,
112 pub max_fee_per_gas: String,
113 pub max_priority_fee_per_gas: String,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub paymaster: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub paymaster_verification_gas_limit: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub paymaster_post_op_gas_limit: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub paymaster_data: Option<String>,
122 pub signature: String,
123}
124
125pub fn hash_userop(
141 userop: &UserOperationV7,
142 entrypoint: &str,
143 chain_id: u64,
144) -> Result<[u8; 32]> {
145 let decode_hex = |s: &str| -> Vec<u8> {
146 hex::decode(s.trim_start_matches("0x")).unwrap_or_default()
147 };
148
149 let init_code = if let Some(ref factory) = userop.factory {
151 let factory_bytes = decode_hex(factory);
152 let factory_data = userop
153 .factory_data
154 .as_deref()
155 .map(|s| decode_hex(s))
156 .unwrap_or_default();
157 let mut ic = factory_bytes;
158 ic.extend_from_slice(&factory_data);
159 ic
160 } else {
161 vec![]
162 };
163
164 let vgl = parse_hex_u128(&userop.verification_gas_limit);
166 let cgl = parse_hex_u128(&userop.call_gas_limit);
167 let mut account_gas_limits = [0u8; 32];
168 account_gas_limits[..16].copy_from_slice(&vgl.to_be_bytes());
169 account_gas_limits[16..].copy_from_slice(&cgl.to_be_bytes());
170
171 let pvg = parse_hex_u128(&userop.pre_verification_gas);
173
174 let mpfpg = parse_hex_u128(&userop.max_priority_fee_per_gas);
176 let mfpg = parse_hex_u128(&userop.max_fee_per_gas);
177 let mut gas_fees = [0u8; 32];
178 gas_fees[..16].copy_from_slice(&mpfpg.to_be_bytes());
179 gas_fees[16..].copy_from_slice(&mfpg.to_be_bytes());
180
181 let paymaster_and_data = if let Some(ref pm) = userop.paymaster {
183 let pm_bytes = decode_hex(pm);
184 let pm_vgl = parse_hex_u128(
185 userop
186 .paymaster_verification_gas_limit
187 .as_deref()
188 .unwrap_or("0x0"),
189 );
190 let pm_pgl = parse_hex_u128(
191 userop
192 .paymaster_post_op_gas_limit
193 .as_deref()
194 .unwrap_or("0x0"),
195 );
196 let pm_data = userop
197 .paymaster_data
198 .as_deref()
199 .map(|s| decode_hex(s))
200 .unwrap_or_default();
201
202 let mut pad = pm_bytes;
203 pad.extend_from_slice(&pm_vgl.to_be_bytes());
204 pad.extend_from_slice(&pm_pgl.to_be_bytes());
205 pad.extend_from_slice(&pm_data);
206 pad
207 } else {
208 vec![]
209 };
210
211 let mut packed = Vec::new();
214 packed.extend_from_slice(&[0u8; 12]);
216 packed.extend_from_slice(&decode_hex(&userop.sender));
217 let nonce_hex_str = userop.nonce.trim_start_matches("0x");
219 let nonce_padded = if nonce_hex_str.len() % 2 != 0 {
221 format!("0{}", nonce_hex_str)
222 } else {
223 nonce_hex_str.to_string()
224 };
225 let nonce_raw = hex::decode(&nonce_padded).unwrap_or_default();
226 let mut nonce_bytes = [0u8; 32];
227 if !nonce_raw.is_empty() && nonce_raw.len() <= 32 {
228 nonce_bytes[32 - nonce_raw.len()..].copy_from_slice(&nonce_raw);
229 }
230 packed.extend_from_slice(&nonce_bytes);
231 packed.extend_from_slice(&keccak256_hash(&init_code));
233 packed.extend_from_slice(&keccak256_hash(&decode_hex(&userop.call_data)));
235 packed.extend_from_slice(&account_gas_limits);
237 let mut pvg_bytes = [0u8; 32];
239 pvg_bytes[16..].copy_from_slice(&pvg.to_be_bytes());
240 packed.extend_from_slice(&pvg_bytes);
241 packed.extend_from_slice(&gas_fees);
243 packed.extend_from_slice(&keccak256_hash(&paymaster_and_data));
245
246 let inner_hash = keccak256_hash(&packed);
247
248 let mut outer = Vec::with_capacity(96);
250 outer.extend_from_slice(&inner_hash);
251 let ep_bytes = hex::decode(entrypoint.trim_start_matches("0x"))
252 .map_err(|e| Error::Crypto(e.to_string()))?;
253 outer.extend_from_slice(&[0u8; 12]);
254 outer.extend_from_slice(&ep_bytes);
255 let mut chain_bytes = [0u8; 32];
256 chain_bytes[24..].copy_from_slice(&chain_id.to_be_bytes());
257 outer.extend_from_slice(&chain_bytes);
258
259 Ok(keccak256_hash(&outer))
260}
261
262pub fn sign_userop(hash: &[u8; 32], private_key: &[u8; 32]) -> Result<Vec<u8>> {
270 use k256::ecdsa::SigningKey;
271
272 let signing_key = SigningKey::from_bytes(private_key.into())
273 .map_err(|e| Error::Crypto(format!("Invalid signing key: {}", e)))?;
274
275 let mut prefixed = Vec::with_capacity(60);
277 prefixed.extend_from_slice(b"\x19Ethereum Signed Message:\n32");
278 prefixed.extend_from_slice(hash);
279 let msg_hash = keccak256_hash(&prefixed);
280
281 let (sig, recovery_id) = signing_key
282 .sign_prehash_recoverable(&msg_hash)
283 .map_err(|e| Error::Crypto(format!("Signing failed: {}", e)))?;
284
285 let mut signature = Vec::with_capacity(65);
287 signature.extend_from_slice(&sig.to_bytes());
288 signature.push(recovery_id.to_byte() + 27); Ok(signature)
291}
292
293fn parse_hex_u128(hex_str: &str) -> u128 {
298 u128::from_str_radix(hex_str.trim_start_matches("0x"), 16).unwrap_or(0)
299}
300
301fn keccak256_hash(data: &[u8]) -> [u8; 32] {
302 use tiny_keccak::{Hasher, Keccak};
303 let mut keccak = Keccak::v256();
304 let mut hash = [0u8; 32];
305 keccak.update(data);
306 keccak.finalize(&mut hash);
307 hash
308}
309
310#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_encode_single_call() {
320 let payload = b"test protobuf data";
321 let encoded = encode_single_call(payload);
322 assert_eq!(&encoded[..4], &[0xb6, 0x1d, 0x27, 0xf6]);
324 assert!(encoded.len() > 100); }
326
327 #[test]
328 fn test_encode_batch_call() {
329 let payloads = vec![
330 b"fact one".to_vec(),
331 b"fact two".to_vec(),
332 b"fact three".to_vec(),
333 ];
334 let encoded = encode_batch_call(&payloads).unwrap();
335 assert_eq!(&encoded[..4], &[0x47, 0xe1, 0xda, 0x2a]);
337 }
338
339 #[test]
340 fn test_single_payload_uses_execute() {
341 let payloads = vec![b"single fact".to_vec()];
342 let encoded = encode_batch_call(&payloads).unwrap();
343 assert_eq!(&encoded[..4], &[0xb6, 0x1d, 0x27, 0xf6]);
345 }
346
347 #[test]
348 fn test_empty_batch_rejected() {
349 let result = encode_batch_call(&[]);
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn test_oversized_batch_rejected() {
355 let payloads: Vec<Vec<u8>> = (0..16).map(|i| vec![i as u8]).collect();
356 let result = encode_batch_call(&payloads);
357 assert!(result.is_err());
358 }
359
360 #[test]
361 fn test_userop_hash_v7_parity() {
362 let userop = UserOperationV7 {
364 sender: "0x949bc374325a4f41e46e8e78a07d910332934542".to_string(),
365 nonce: "0x0".to_string(),
366 factory: Some("0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985".to_string()),
367 factory_data: Some("0x5fbfb9cf0000000000000000000000008eb626f727e92a73435f2b85dd6fd0c6da5dbb720000000000000000000000000000000000000000000000000000000000000000".to_string()),
368 call_data: "0xb61d27f6".to_string(),
369 call_gas_limit: "0x186a0".to_string(), verification_gas_limit: "0x30d40".to_string(), pre_verification_gas: "0xc350".to_string(), max_fee_per_gas: "0xf4240".to_string(), max_priority_fee_per_gas: "0x7a120".to_string(), paymaster: Some("0x0000000000000039cd5e8ae05257ce51c473ddd1".to_string()),
375 paymaster_verification_gas_limit: Some("0x186a0".to_string()), paymaster_post_op_gas_limit: Some("0xc350".to_string()), paymaster_data: Some("0xabcd".to_string()),
378 signature: format!("0x{}", "00".repeat(65)),
379 };
380
381 let hash = hash_userop(
382 &userop,
383 "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
384 84532,
385 )
386 .unwrap();
387
388 assert_eq!(
389 format!("0x{}", hex::encode(hash)),
390 "0x4525d2a8a555a1a56f6313735b83fe3ee55f81d504d905ea85613524973f97c2",
391 "v0.7 UserOp hash must match viem's getUserOperationHash"
392 );
393 }
394
395 #[test]
396 fn test_signing_parity() {
397 let private_key_hex = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
399 let mut private_key = [0u8; 32];
400 private_key.copy_from_slice(&hex::decode(private_key_hex).unwrap());
401
402 let hash_hex = "1b25552f7901991cd4e2793945f694a09c9d0b9454a86cee16123ac9e84bd2de";
403 let mut hash = [0u8; 32];
404 hash.copy_from_slice(&hex::decode(hash_hex).unwrap());
405
406 let sig = sign_userop(&hash, &private_key).unwrap();
407 let sig_hex = hex::encode(&sig);
408
409 assert_eq!(
411 sig_hex,
412 "24b6fabd386f1580aa1fc09b04dd274ea334a9bf63e4fc994e0bef9a505f618335cb2b7d20454a0526f5c66f52ed73b9e76e9696ab5959998e7fc3984fba91691c",
413 "Signature must match viem's signMessage({{message: {{raw: hash}}}})"
414 );
415 }
416
417 #[test]
418 fn test_signing_parity_abandon_mnemonic() {
419 let private_key_hex = "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727";
421 let mut private_key = [0u8; 32];
422 private_key.copy_from_slice(&hex::decode(private_key_hex).unwrap());
423
424 let hash_hex = "6de60c2ca586227294ffce39e30a3c6ec8ddf6ae01d0d579344e8d2e2dbf8b26";
426 let mut hash = [0u8; 32];
427 hash.copy_from_slice(&hex::decode(hash_hex).unwrap());
428
429 let sig = sign_userop(&hash, &private_key).unwrap();
430 let sig_hex = hex::encode(&sig);
431
432 assert_eq!(
433 sig_hex,
434 "a5ad7388dd018236a6cfc25556f35d0d05fff7a9a59ef29fef65b1855298f767107418521a5ca48e56a4d5de67e954df5d6dd49fe98eba3d1c45ad22eeae3fd11c",
435 "Signature must match viem's signMessage for abandon mnemonic"
436 );
437 }
438
439 #[test]
440 fn test_keccak256_hash() {
441 let hash = keccak256_hash(b"hello");
442 assert_eq!(
443 hex::encode(hash),
444 "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
445 );
446 }
447
448 #[test]
449 fn test_userop_hash_v7_real_paymaster() {
450 let userop = UserOperationV7 {
453 sender: "0x695241674733a452a5373b16baf2dc2d9435be8e".to_string(),
454 nonce: "0x0".to_string(),
455 factory: Some("0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985".to_string()),
456 factory_data: Some("0x5fbfb9cf000000000000000000000000cd894ed607b25d52e9ac776cf48e9407d3a263d30000000000000000000000000000000000000000000000000000000000000000".to_string()),
457 call_data: "0xb61d27f6000000000000000000000000c445af1d4eb9fce4e1e61fe96ea7b8febf03c5ca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000".to_string(),
458 call_gas_limit: "0x4623".to_string(),
459 verification_gas_limit: "0x41bab".to_string(),
460 pre_verification_gas: "0xc9c9".to_string(),
461 max_fee_per_gas: "0x757e20".to_string(),
462 max_priority_fee_per_gas: "0x10c8e0".to_string(),
463 paymaster: Some("0x777777777777AeC03fd955926DbF81597e66834C".to_string()),
464 paymaster_verification_gas_limit: Some("0x8a8e".to_string()),
465 paymaster_post_op_gas_limit: Some("0x1".to_string()),
466 paymaster_data: Some("0x01000069cb37390000000000006568f8cf98f823f68c4fedbde90b241f30e9323b436eeb3cddeb688e0859b23565005402808774472237b1808f0006721bd065729a72a84710fdceaa465737f61c".to_string()),
467 signature: format!("0x{}", "00".repeat(65)),
468 };
469
470 let hash = hash_userop(
471 &userop,
472 "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
473 84532,
474 )
475 .unwrap();
476
477 assert_eq!(
478 format!("0x{}", hex::encode(hash)),
479 "0x3d4467d9a3c070eea659ef9a7cf42f1b4e87e14cd91792d869faf031b6fea3e8",
480 "v0.7 hash with real paymaster data must match viem"
481 );
482 }
483
484 #[test]
485 fn test_nonce_uint256_parsing() {
486 let userop = UserOperationV7 {
488 sender: "0x949bc374325a4f41e46e8e78a07d910332934542".to_string(),
489 nonce: "0x19d41c68d5e0000000000000000".to_string(), factory: None,
491 factory_data: None,
492 call_data: "0xb61d27f6".to_string(),
493 call_gas_limit: "0x186a0".to_string(),
494 verification_gas_limit: "0x30d40".to_string(),
495 pre_verification_gas: "0xc350".to_string(),
496 max_fee_per_gas: "0xf4240".to_string(),
497 max_priority_fee_per_gas: "0x7a120".to_string(),
498 paymaster: None,
499 paymaster_verification_gas_limit: None,
500 paymaster_post_op_gas_limit: None,
501 paymaster_data: None,
502 signature: format!("0x{}", "00".repeat(65)),
503 };
504
505 let hash = hash_userop(
507 &userop,
508 "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
509 84532,
510 )
511 .unwrap();
512 assert_ne!(hex::encode(hash), "0".repeat(64));
513 }
514}