Skip to main content

chains_sdk/ethereum/
smart_wallet.rs

1//! **Smart Wallet** and **Account Abstraction (EIP-4337 v0.7)** encoding/decoding.
2//!
3//! Provides:
4//! - `PackedUserOperation` — EIP-4337 v0.7 packed format
5//! - `handleOps` calldata encoding for the EntryPoint v0.7
6//! - Smart wallet `execute`/`executeBatch` encoding
7//! - ERC-1271 `isValidSignature` encoding and decoding
8//! - Paymaster data encoding
9//!
10//! # Example
11//! ```no_run
12//! use chains_sdk::ethereum::smart_wallet::{
13//!     PackedUserOperation, encode_execute, encode_execute_batch, ExecuteCall,
14//!     encode_is_valid_signature, is_valid_signature_magic,
15//! };
16//!
17//! // Encode a smart wallet execute call
18//! let calldata = encode_execute([0xBB; 20], 0, &[]);
19//!
20//! // Batch execution
21//! let calls = vec![
22//!     ExecuteCall { target: [0xAA; 20], value: 0, data: vec![0x01] },
23//!     ExecuteCall { target: [0xBB; 20], value: 100, data: vec![0x02] },
24//! ];
25//! let batch = encode_execute_batch(&calls);
26//! ```
27
28use crate::error::SignerError;
29use crate::ethereum::abi::{self, AbiValue};
30use sha3::{Digest, Keccak256};
31
32// ─── EIP-4337 v0.7 Packed User Operation ───────────────────────────
33
34/// A packed user operation for EIP-4337 v0.7 (EntryPoint v0.7).
35///
36/// This is the "packed" format introduced in ERC-4337 v0.7 where gas fields
37/// are combined into `bytes32` for more efficient calldata.
38#[derive(Debug, Clone)]
39pub struct PackedUserOperation {
40    /// The account making the operation.
41    pub sender: [u8; 20],
42    /// Anti-replay parameter (nonce from the entrypoint, includes key).
43    pub nonce: [u8; 32],
44    /// Account initCode (needed if account is not yet deployed).
45    pub init_code: Vec<u8>,
46    /// The calldata to execute on the account.
47    pub call_data: Vec<u8>,
48    /// Packed gas limits: `verificationGasLimit (16 bytes) || callGasLimit (16 bytes)`.
49    pub account_gas_limits: [u8; 32],
50    /// Pre-verification gas.
51    pub pre_verification_gas: [u8; 32],
52    /// Packed gas fees: `maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes)`.
53    pub gas_fees: [u8; 32],
54    /// Paymaster data: `paymaster (20 bytes) || paymasterVerificationGasLimit (16 bytes) || paymasterPostOpGasLimit (16 bytes) || paymasterData`.
55    pub paymaster_and_data: Vec<u8>,
56    /// The signature for this operation.
57    pub signature: Vec<u8>,
58}
59
60impl PackedUserOperation {
61    /// Pack the user operation for hashing (excludes the signature).
62    ///
63    /// Returns the ABI-encoded struct for computing the user operation hash.
64    #[must_use]
65    pub fn pack(&self) -> Vec<u8> {
66        let mut buf = Vec::with_capacity(11 * 32);
67
68        // Pack as: sender, nonce, keccak256(initCode), keccak256(callData),
69        // accountGasLimits, preVerificationGas, gasFees, keccak256(paymasterAndData)
70        buf.extend_from_slice(&pad_address(&self.sender));
71        buf.extend_from_slice(&self.nonce);
72        buf.extend_from_slice(&keccak256(&self.init_code));
73        buf.extend_from_slice(&keccak256(&self.call_data));
74        buf.extend_from_slice(&self.account_gas_limits);
75        buf.extend_from_slice(&self.pre_verification_gas);
76        buf.extend_from_slice(&self.gas_fees);
77        buf.extend_from_slice(&keccak256(&self.paymaster_and_data));
78
79        buf
80    }
81
82    /// Compute the user operation hash.
83    ///
84    /// `keccak256(abi.encode(pack(userOp), entryPoint, chainId))`
85    #[must_use]
86    pub fn hash(&self, entry_point: &[u8; 20], chain_id: u64) -> [u8; 32] {
87        let packed_hash = keccak256(&self.pack());
88        let mut buf = Vec::with_capacity(3 * 32);
89        buf.extend_from_slice(&packed_hash);
90        buf.extend_from_slice(&pad_address(entry_point));
91        buf.extend_from_slice(&pad_u64(chain_id));
92        keccak256(&buf)
93    }
94
95    /// Sign this packed user operation.
96    pub fn sign(
97        &self,
98        signer: &super::EthereumSigner,
99        entry_point: &[u8; 20],
100        chain_id: u64,
101    ) -> Result<super::EthereumSignature, SignerError> {
102        let hash = self.hash(entry_point, chain_id);
103        signer.sign_digest(&hash)
104    }
105
106    /// Pack gas limits into the `accountGasLimits` field.
107    ///
108    /// `verificationGasLimit (16 bytes) || callGasLimit (16 bytes)`
109    #[must_use]
110    pub fn pack_account_gas_limits(
111        verification_gas_limit: u128,
112        call_gas_limit: u128,
113    ) -> [u8; 32] {
114        let mut buf = [0u8; 32];
115        buf[..16].copy_from_slice(&verification_gas_limit.to_be_bytes());
116        buf[16..].copy_from_slice(&call_gas_limit.to_be_bytes());
117        buf
118    }
119
120    /// Unpack gas limits from the `accountGasLimits` field.
121    #[must_use]
122    pub fn unpack_account_gas_limits(packed: &[u8; 32]) -> (u128, u128) {
123        let mut vgl = [0u8; 16];
124        let mut cgl = [0u8; 16];
125        vgl.copy_from_slice(&packed[..16]);
126        cgl.copy_from_slice(&packed[16..]);
127        (u128::from_be_bytes(vgl), u128::from_be_bytes(cgl))
128    }
129
130    /// Pack gas fees into the `gasFees` field.
131    ///
132    /// `maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes)`
133    #[must_use]
134    pub fn pack_gas_fees(max_priority_fee: u128, max_fee: u128) -> [u8; 32] {
135        let mut buf = [0u8; 32];
136        buf[..16].copy_from_slice(&max_priority_fee.to_be_bytes());
137        buf[16..].copy_from_slice(&max_fee.to_be_bytes());
138        buf
139    }
140
141    /// Unpack gas fees from the `gasFees` field.
142    #[must_use]
143    pub fn unpack_gas_fees(packed: &[u8; 32]) -> (u128, u128) {
144        let mut mpf = [0u8; 16];
145        let mut mf = [0u8; 16];
146        mpf.copy_from_slice(&packed[..16]);
147        mf.copy_from_slice(&packed[16..]);
148        (u128::from_be_bytes(mpf), u128::from_be_bytes(mf))
149    }
150}
151
152// ─── EntryPoint handleOps ──────────────────────────────────────────
153
154/// The EntryPoint v0.7 contract address.
155///
156/// Deployed at the same address on all EVM chains.
157pub const ENTRY_POINT_V07: [u8; 20] = [
158    0x00, 0x00, 0x00, 0x00, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
159    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
160];
161
162/// ABI-encode `handleOps(PackedUserOperation[], address beneficiary)` for EntryPoint v0.7.
163///
164/// This is the function called by bundlers to submit user operations.
165#[must_use]
166pub fn encode_handle_ops(
167    ops: &[PackedUserOperation],
168    beneficiary: [u8; 20],
169) -> Vec<u8> {
170    let func = abi::Function::new(
171        "handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address)",
172    );
173
174    let op_tuples: Vec<AbiValue> = ops
175        .iter()
176        .map(|op| {
177            AbiValue::Tuple(vec![
178                AbiValue::Address(op.sender),
179                AbiValue::Uint256(op.nonce),
180                AbiValue::Bytes(op.init_code.clone()),
181                AbiValue::Bytes(op.call_data.clone()),
182                AbiValue::Uint256(op.account_gas_limits),
183                AbiValue::Uint256(op.pre_verification_gas),
184                AbiValue::Uint256(op.gas_fees),
185                AbiValue::Bytes(op.paymaster_and_data.clone()),
186                AbiValue::Bytes(op.signature.clone()),
187            ])
188        })
189        .collect();
190
191    func.encode(&[AbiValue::Array(op_tuples), AbiValue::Address(beneficiary)])
192}
193
194// ─── Paymaster Data ────────────────────────────────────────────────
195
196/// Encode paymaster data for the `paymasterAndData` field.
197///
198/// Format: `paymaster (20 bytes) || verificationGasLimit (16 bytes) || postOpGasLimit (16 bytes) || data`
199#[must_use]
200pub fn encode_paymaster_data(
201    paymaster: [u8; 20],
202    verification_gas_limit: u128,
203    post_op_gas_limit: u128,
204    data: &[u8],
205) -> Vec<u8> {
206    let mut buf = Vec::with_capacity(20 + 16 + 16 + data.len());
207    buf.extend_from_slice(&paymaster);
208    buf.extend_from_slice(&verification_gas_limit.to_be_bytes());
209    buf.extend_from_slice(&post_op_gas_limit.to_be_bytes());
210    buf.extend_from_slice(data);
211    buf
212}
213
214/// Decode paymaster data from the `paymasterAndData` field.
215///
216/// Returns `(paymaster, verificationGasLimit, postOpGasLimit, data)`.
217pub fn decode_paymaster_data(
218    encoded: &[u8],
219) -> Result<([u8; 20], u128, u128, Vec<u8>), SignerError> {
220    if encoded.len() < 52 {
221        return Err(SignerError::EncodingError(format!(
222            "paymasterAndData too short: {} bytes, need at least 52",
223            encoded.len()
224        )));
225    }
226    let mut paymaster = [0u8; 20];
227    paymaster.copy_from_slice(&encoded[..20]);
228
229    let mut vgl_bytes = [0u8; 16];
230    vgl_bytes.copy_from_slice(&encoded[20..36]);
231    let verification_gas_limit = u128::from_be_bytes(vgl_bytes);
232
233    let mut pogl_bytes = [0u8; 16];
234    pogl_bytes.copy_from_slice(&encoded[36..52]);
235    let post_op_gas_limit = u128::from_be_bytes(pogl_bytes);
236
237    let data = encoded[52..].to_vec();
238
239    Ok((paymaster, verification_gas_limit, post_op_gas_limit, data))
240}
241
242// ─── Smart Wallet execute ──────────────────────────────────────────
243
244/// ABI-encode `execute(address dest, uint256 value, bytes func)`.
245///
246/// Standard smart wallet execution function (e.g. SimpleAccount, Kernel, etc.).
247#[must_use]
248pub fn encode_execute(dest: [u8; 20], value: u128, func: &[u8]) -> Vec<u8> {
249    let f = abi::Function::new("execute(address,uint256,bytes)");
250    f.encode(&[
251        AbiValue::Address(dest),
252        AbiValue::from_u128(value),
253        AbiValue::Bytes(func.to_vec()),
254    ])
255}
256
257/// A call entry for batch execution.
258#[derive(Debug, Clone)]
259pub struct ExecuteCall {
260    /// Target contract address.
261    pub target: [u8; 20],
262    /// ETH value in wei.
263    pub value: u128,
264    /// Calldata to execute.
265    pub data: Vec<u8>,
266}
267
268/// ABI-encode `executeBatch(address[] dest, uint256[] values, bytes[] func)`.
269///
270/// Standard batched execution for smart wallets.
271#[must_use]
272pub fn encode_execute_batch(calls: &[ExecuteCall]) -> Vec<u8> {
273    let f = abi::Function::new("executeBatch(address[],uint256[],bytes[])");
274    let dests: Vec<AbiValue> = calls.iter().map(|c| AbiValue::Address(c.target)).collect();
275    let values: Vec<AbiValue> = calls.iter().map(|c| AbiValue::from_u128(c.value)).collect();
276    let funcs: Vec<AbiValue> = calls
277        .iter()
278        .map(|c| AbiValue::Bytes(c.data.clone()))
279        .collect();
280    f.encode(&[
281        AbiValue::Array(dests),
282        AbiValue::Array(values),
283        AbiValue::Array(funcs),
284    ])
285}
286
287// ─── ERC-1271 Signature Validation ─────────────────────────────────
288
289/// The ERC-1271 magic value indicating a valid signature.
290///
291/// `bytes4(keccak256("isValidSignature(bytes32,bytes)"))` = `0x1626ba7e`
292pub const ERC1271_MAGIC: [u8; 4] = [0x16, 0x26, 0xba, 0x7e];
293
294/// ABI-encode `isValidSignature(bytes32 hash, bytes signature)`.
295///
296/// ERC-1271 standard for smart contract signature validation.
297#[must_use]
298pub fn encode_is_valid_signature(hash: &[u8; 32], signature: &[u8]) -> Vec<u8> {
299    let f = abi::Function::new("isValidSignature(bytes32,bytes)");
300    f.encode(&[
301        AbiValue::Uint256(*hash),
302        AbiValue::Bytes(signature.to_vec()),
303    ])
304}
305
306/// Check if the return value from `isValidSignature` indicates a valid signature.
307///
308/// Returns `true` if the first 4 bytes match `0x1626ba7e`.
309#[must_use]
310pub fn is_valid_signature_magic(return_data: &[u8]) -> bool {
311    if return_data.len() < 32 {
312        return false;
313    }
314    // The return value is a bytes4 left-padded to 32 bytes
315    return_data[..4] == [0u8; 4]
316        && return_data[4..28] == [0u8; 24]
317        && return_data[28..32] == ERC1271_MAGIC
318}
319
320/// Check if the return value from `isValidSignature` indicates a valid signature
321/// (non-padded, just the raw 4 bytes).
322#[must_use]
323pub fn is_valid_signature_magic_raw(return_data: &[u8]) -> bool {
324    if return_data.len() < 4 {
325        return false;
326    }
327    return_data[..4] == ERC1271_MAGIC
328}
329
330// ─── Account Factory ───────────────────────────────────────────────
331
332/// ABI-encode `createAccount(address owner, uint256 salt)`.
333///
334/// Standard account factory for deploying new smart accounts.
335#[must_use]
336pub fn encode_create_account(owner: [u8; 20], salt: u64) -> Vec<u8> {
337    let f = abi::Function::new("createAccount(address,uint256)");
338    f.encode(&[AbiValue::Address(owner), AbiValue::from_u64(salt)])
339}
340
341/// ABI-encode `getAddress(address owner, uint256 salt)`.
342///
343/// Query the counterfactual address of a smart account.
344#[must_use]
345pub fn encode_get_address(owner: [u8; 20], salt: u64) -> Vec<u8> {
346    let f = abi::Function::new("getAddress(address,uint256)");
347    f.encode(&[AbiValue::Address(owner), AbiValue::from_u64(salt)])
348}
349
350// ─── Nonce Management ──────────────────────────────────────────────
351
352/// ABI-encode `getNonce(address sender, uint192 key)` for EntryPoint nonce query.
353#[must_use]
354pub fn encode_get_nonce(sender: [u8; 20], key: u64) -> Vec<u8> {
355    let f = abi::Function::new("getNonce(address,uint192)");
356    f.encode(&[AbiValue::Address(sender), AbiValue::from_u64(key)])
357}
358
359// ─── Internal Helpers ──────────────────────────────────────────────
360
361fn keccak256(data: &[u8]) -> [u8; 32] {
362    let mut hasher = Keccak256::new();
363    hasher.update(data);
364    hasher.finalize().into()
365}
366
367fn pad_address(addr: &[u8; 20]) -> [u8; 32] {
368    let mut buf = [0u8; 32];
369    buf[12..32].copy_from_slice(addr);
370    buf
371}
372
373fn pad_u64(val: u64) -> [u8; 32] {
374    let mut buf = [0u8; 32];
375    buf[24..32].copy_from_slice(&val.to_be_bytes());
376    buf
377}
378
379// ─── Tests ─────────────────────────────────────────────────────────
380
381#[cfg(test)]
382#[allow(clippy::unwrap_used, clippy::expect_used)]
383mod tests {
384    use super::*;
385    use crate::traits::KeyPair;
386
387    fn sample_op() -> PackedUserOperation {
388        PackedUserOperation {
389            sender: [0xAA; 20],
390            nonce: [0u8; 32],
391            init_code: vec![],
392            call_data: vec![0x01, 0x02],
393            account_gas_limits: PackedUserOperation::pack_account_gas_limits(100_000, 200_000),
394            pre_verification_gas: {
395                let mut buf = [0u8; 32];
396                buf[24..32].copy_from_slice(&50_000u64.to_be_bytes());
397                buf
398            },
399            gas_fees: PackedUserOperation::pack_gas_fees(1_000_000_000, 2_000_000_000),
400            paymaster_and_data: vec![],
401            signature: vec![],
402        }
403    }
404
405    // ─── Pack/Hash ────────────────────────────────────────────
406
407    #[test]
408    fn test_pack_deterministic() {
409        let op = sample_op();
410        assert_eq!(op.pack(), op.pack());
411    }
412
413    #[test]
414    fn test_pack_length() {
415        let op = sample_op();
416        // 8 fields × 32 bytes = 256
417        assert_eq!(op.pack().len(), 256);
418    }
419
420    #[test]
421    fn test_hash_deterministic() {
422        let op = sample_op();
423        let ep = [0xFF; 20];
424        assert_eq!(op.hash(&ep, 1), op.hash(&ep, 1));
425    }
426
427    #[test]
428    fn test_hash_changes_with_chain_id() {
429        let op = sample_op();
430        let ep = [0xFF; 20];
431        assert_ne!(op.hash(&ep, 1), op.hash(&ep, 137));
432    }
433
434    #[test]
435    fn test_hash_changes_with_entry_point() {
436        let op = sample_op();
437        assert_ne!(op.hash(&[0xAA; 20], 1), op.hash(&[0xBB; 20], 1));
438    }
439
440    #[test]
441    fn test_hash_changes_with_calldata() {
442        let mut op1 = sample_op();
443        let mut op2 = sample_op();
444        op2.call_data = vec![0x03, 0x04];
445        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
446    }
447
448    #[test]
449    fn test_hash_changes_with_sender() {
450        let mut op1 = sample_op();
451        let mut op2 = sample_op();
452        op2.sender = [0xBB; 20];
453        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
454    }
455
456    #[test]
457    fn test_hash_changes_with_nonce() {
458        let mut op1 = sample_op();
459        let mut op2 = sample_op();
460        op2.nonce[31] = 1;
461        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
462    }
463
464    // ─── Sign ─────────────────────────────────────────────────
465
466    #[test]
467    fn test_sign_produces_valid_signature() {
468        let signer = super::super::EthereumSigner::generate().unwrap();
469        let op = sample_op();
470        let sig = op.sign(&signer, &[0xFF; 20], 1).unwrap();
471        assert!(sig.v == 27 || sig.v == 28);
472        assert_ne!(sig.r, [0u8; 32]);
473    }
474
475    #[test]
476    fn test_sign_recovers_correct_address() {
477        let signer = super::super::EthereumSigner::generate().unwrap();
478        let op = sample_op();
479        let sig = op.sign(&signer, &[0xFF; 20], 1).unwrap();
480        let hash = op.hash(&[0xFF; 20], 1);
481        let recovered = super::super::ecrecover_digest(&hash, &sig).unwrap();
482        assert_eq!(recovered, signer.address());
483    }
484
485    // ─── Gas Packing ──────────────────────────────────────────
486
487    #[test]
488    fn test_pack_account_gas_limits_roundtrip() {
489        let packed = PackedUserOperation::pack_account_gas_limits(100_000, 200_000);
490        let (vgl, cgl) = PackedUserOperation::unpack_account_gas_limits(&packed);
491        assert_eq!(vgl, 100_000);
492        assert_eq!(cgl, 200_000);
493    }
494
495    #[test]
496    fn test_pack_gas_fees_roundtrip() {
497        let packed = PackedUserOperation::pack_gas_fees(1_000_000_000, 2_000_000_000);
498        let (mpf, mf) = PackedUserOperation::unpack_gas_fees(&packed);
499        assert_eq!(mpf, 1_000_000_000);
500        assert_eq!(mf, 2_000_000_000);
501    }
502
503    #[test]
504    fn test_pack_gas_limits_zero() {
505        let packed = PackedUserOperation::pack_account_gas_limits(0, 0);
506        assert_eq!(packed, [0u8; 32]);
507        let (vgl, cgl) = PackedUserOperation::unpack_account_gas_limits(&packed);
508        assert_eq!(vgl, 0);
509        assert_eq!(cgl, 0);
510    }
511
512    #[test]
513    fn test_pack_gas_fees_max() {
514        let max = u128::MAX;
515        let packed = PackedUserOperation::pack_gas_fees(max, max);
516        let (mpf, mf) = PackedUserOperation::unpack_gas_fees(&packed);
517        assert_eq!(mpf, max);
518        assert_eq!(mf, max);
519    }
520
521    // ─── handleOps ────────────────────────────────────────────
522
523    #[test]
524    fn test_encode_handle_ops_selector() {
525        let ops = vec![sample_op()];
526        let calldata = encode_handle_ops(&ops, [0xFF; 20]);
527        let expected = abi::function_selector(
528            "handleOps((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[],address)",
529        );
530        assert_eq!(&calldata[..4], &expected);
531    }
532
533    #[test]
534    fn test_encode_handle_ops_empty() {
535        let calldata = encode_handle_ops(&[], [0xFF; 20]);
536        assert!(calldata.len() > 4);
537    }
538
539    // ─── Paymaster Data ───────────────────────────────────────
540
541    #[test]
542    fn test_paymaster_data_roundtrip() {
543        let paymaster = [0xAA; 20];
544        let vgl = 100_000u128;
545        let pogl = 50_000u128;
546        let data = vec![0xDE, 0xAD];
547
548        let encoded = encode_paymaster_data(paymaster, vgl, pogl, &data);
549        let (dec_pm, dec_vgl, dec_pogl, dec_data) = decode_paymaster_data(&encoded).unwrap();
550
551        assert_eq!(dec_pm, paymaster);
552        assert_eq!(dec_vgl, vgl);
553        assert_eq!(dec_pogl, pogl);
554        assert_eq!(dec_data, data);
555    }
556
557    #[test]
558    fn test_paymaster_data_empty_extra_data() {
559        let encoded = encode_paymaster_data([0xAA; 20], 100, 200, &[]);
560        let (_, _, _, data) = decode_paymaster_data(&encoded).unwrap();
561        assert!(data.is_empty());
562    }
563
564    #[test]
565    fn test_paymaster_data_too_short() {
566        assert!(decode_paymaster_data(&[0u8; 51]).is_err());
567        assert!(decode_paymaster_data(&[]).is_err());
568    }
569
570    #[test]
571    fn test_paymaster_data_minimum_length() {
572        let encoded = encode_paymaster_data([0xBB; 20], 0, 0, &[]);
573        assert_eq!(encoded.len(), 52);
574        assert!(decode_paymaster_data(&encoded).is_ok());
575    }
576
577    // ─── Smart Wallet Execute ─────────────────────────────────
578
579    #[test]
580    fn test_encode_execute_selector() {
581        let calldata = encode_execute([0xBB; 20], 0, &[]);
582        let expected = abi::function_selector("execute(address,uint256,bytes)");
583        assert_eq!(&calldata[..4], &expected);
584    }
585
586    #[test]
587    fn test_encode_execute_with_value() {
588        let calldata = encode_execute([0xBB; 20], 1_000_000, &[0xDE, 0xAD]);
589        assert!(calldata.len() > 4 + 3 * 32);
590    }
591
592    #[test]
593    fn test_encode_execute_batch_selector() {
594        let calls = vec![ExecuteCall {
595            target: [0xAA; 20],
596            value: 0,
597            data: vec![],
598        }];
599        let calldata = encode_execute_batch(&calls);
600        let expected = abi::function_selector("executeBatch(address[],uint256[],bytes[])");
601        assert_eq!(&calldata[..4], &expected);
602    }
603
604    #[test]
605    fn test_encode_execute_batch_multiple() {
606        let calls = vec![
607            ExecuteCall { target: [0xAA; 20], value: 100, data: vec![0x01] },
608            ExecuteCall { target: [0xBB; 20], value: 200, data: vec![0x02] },
609        ];
610        let calldata = encode_execute_batch(&calls);
611        assert!(calldata.len() > 4);
612    }
613
614    #[test]
615    fn test_encode_execute_batch_empty() {
616        let calldata = encode_execute_batch(&[]);
617        let expected = abi::function_selector("executeBatch(address[],uint256[],bytes[])");
618        assert_eq!(&calldata[..4], &expected);
619    }
620
621    // ─── ERC-1271 ─────────────────────────────────────────────
622
623    #[test]
624    fn test_erc1271_magic_value() {
625        assert_eq!(ERC1271_MAGIC, [0x16, 0x26, 0xba, 0x7e]);
626    }
627
628    #[test]
629    fn test_encode_is_valid_signature_selector() {
630        let calldata = encode_is_valid_signature(&[0xAA; 32], &[0xBB; 65]);
631        let expected = abi::function_selector("isValidSignature(bytes32,bytes)");
632        assert_eq!(&calldata[..4], &expected);
633    }
634
635    #[test]
636    fn test_is_valid_signature_magic_true() {
637        // ABI-encoded: left-padded bytes4 0x1626ba7e to 32 bytes
638        let mut result = [0u8; 32];
639        result[28] = 0x16;
640        result[29] = 0x26;
641        result[30] = 0xba;
642        result[31] = 0x7e;
643        assert!(is_valid_signature_magic(&result));
644    }
645
646    #[test]
647    fn test_is_valid_signature_magic_false() {
648        let result = [0u8; 32]; // all zeros = invalid
649        assert!(!is_valid_signature_magic(&result));
650    }
651
652    #[test]
653    fn test_is_valid_signature_magic_too_short() {
654        assert!(!is_valid_signature_magic(&[0u8; 31]));
655    }
656
657    #[test]
658    fn test_is_valid_signature_magic_raw_true() {
659        assert!(is_valid_signature_magic_raw(&[0x16, 0x26, 0xba, 0x7e]));
660    }
661
662    #[test]
663    fn test_is_valid_signature_magic_raw_false() {
664        assert!(!is_valid_signature_magic_raw(&[0x00, 0x00, 0x00, 0x00]));
665    }
666
667    #[test]
668    fn test_is_valid_signature_magic_raw_too_short() {
669        assert!(!is_valid_signature_magic_raw(&[0x16, 0x26, 0xba]));
670    }
671
672    // ─── Account Factory ──────────────────────────────────────
673
674    #[test]
675    fn test_encode_create_account_selector() {
676        let calldata = encode_create_account([0xAA; 20], 0);
677        let expected = abi::function_selector("createAccount(address,uint256)");
678        assert_eq!(&calldata[..4], &expected);
679    }
680
681    #[test]
682    fn test_encode_get_address_selector() {
683        let calldata = encode_get_address([0xAA; 20], 0);
684        let expected = abi::function_selector("getAddress(address,uint256)");
685        assert_eq!(&calldata[..4], &expected);
686    }
687
688    // ─── Nonce Management ─────────────────────────────────────
689
690    #[test]
691    fn test_encode_get_nonce_selector() {
692        let calldata = encode_get_nonce([0xAA; 20], 0);
693        let expected = abi::function_selector("getNonce(address,uint192)");
694        assert_eq!(&calldata[..4], &expected);
695    }
696
697    // ─── Internal Helpers ─────────────────────────────────────
698
699    #[test]
700    fn test_pad_address() {
701        let addr = [0xAA; 20];
702        let padded = pad_address(&addr);
703        assert_eq!(&padded[..12], &[0u8; 12]);
704        assert_eq!(&padded[12..], &[0xAA; 20]);
705    }
706
707    #[test]
708    fn test_pad_u64() {
709        let padded = pad_u64(256);
710        assert_eq!(&padded[..24], &[0u8; 24]);
711        assert_eq!(&padded[24..], &256u64.to_be_bytes());
712    }
713
714    // ─── Init Code ────────────────────────────────────────────
715
716    #[test]
717    fn test_hash_changes_with_init_code() {
718        let mut op1 = sample_op();
719        let mut op2 = sample_op();
720        op2.init_code = vec![0xFF; 20];
721        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
722    }
723
724    #[test]
725    fn test_hash_changes_with_paymaster_data() {
726        let mut op1 = sample_op();
727        let mut op2 = sample_op();
728        op2.paymaster_and_data = vec![0xAA; 52];
729        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
730    }
731
732    #[test]
733    fn test_hash_changes_with_gas_limits() {
734        let mut op1 = sample_op();
735        let mut op2 = sample_op();
736        op2.account_gas_limits = PackedUserOperation::pack_account_gas_limits(999, 888);
737        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
738    }
739
740    #[test]
741    fn test_hash_changes_with_gas_fees() {
742        let mut op1 = sample_op();
743        let mut op2 = sample_op();
744        op2.gas_fees = PackedUserOperation::pack_gas_fees(999, 888);
745        assert_ne!(op1.hash(&[0xFF; 20], 1), op2.hash(&[0xFF; 20], 1));
746    }
747}