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