Skip to main content

chains_sdk/ethereum/
eips.rs

1//! Additional Ethereum EIP helpers for signing-related standards.
2//!
3//! Provides encoding helpers for EIPs that involve message construction
4//! or data formatting at the signing layer:
5//!
6//! - **EIP-2612**: ERC-20 Permit (gasless approve via EIP-712)
7//! - **EIP-4337**: Account Abstraction UserOperation
8//! - **EIP-7702**: Set EOA Account Code authorization
9//! - **EIP-3074**: AUTH/AUTHCALL message digest
10//! - **EIP-6492**: Pre-deploy contract signature wrapping
11//! - **EIP-5267**: EIP-712 domain query calldata
12//! - **EIP-2335**: BLS12-381 keystore path constants
13
14use crate::error::SignerError;
15
16/// Keccak-256 hash — delegates to the canonical implementation in `super::keccak256`.
17///
18/// Re-exported here because this file is used outside the ethereum module too.
19fn keccak256(data: &[u8]) -> [u8; 32] {
20    super::keccak256(data)
21}
22
23// ═══════════════════════════════════════════════════════════════════
24// EIP-2612: Permit (ERC-20 Gasless Approve)
25// ═══════════════════════════════════════════════════════════════════
26
27/// EIP-2612 Permit message for gasless ERC-20 token approvals.
28///
29/// This constructs the EIP-712 typed data that the token holder signs,
30/// allowing a third party to call `permit()` on the ERC-20 contract.
31#[derive(Debug, Clone)]
32pub struct Permit {
33    /// Token holder granting approval.
34    pub owner: [u8; 20],
35    /// Address being approved to spend tokens.
36    pub spender: [u8; 20],
37    /// Amount of tokens to approve (as 32-byte big-endian uint256).
38    pub value: [u8; 32],
39    /// Current nonce of the owner on the token contract.
40    pub nonce: u64,
41    /// Unix timestamp deadline for the permit signature.
42    pub deadline: u64,
43}
44
45impl Permit {
46    /// Compute the EIP-712 `PERMIT_TYPEHASH`.
47    ///
48    /// `keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")`
49    #[must_use]
50    pub fn type_hash() -> [u8; 32] {
51        keccak256(
52            b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)",
53        )
54    }
55
56    /// Compute the struct hash for this permit.
57    ///
58    /// `keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline))`
59    #[must_use]
60    pub fn struct_hash(&self) -> [u8; 32] {
61        let mut data = Vec::with_capacity(6 * 32);
62        data.extend_from_slice(&Self::type_hash());
63
64        // owner (left-padded to 32)
65        let mut owner_padded = [0u8; 32];
66        owner_padded[12..].copy_from_slice(&self.owner);
67        data.extend_from_slice(&owner_padded);
68
69        // spender (left-padded to 32)
70        let mut spender_padded = [0u8; 32];
71        spender_padded[12..].copy_from_slice(&self.spender);
72        data.extend_from_slice(&spender_padded);
73
74        // value (already 32 bytes)
75        data.extend_from_slice(&self.value);
76
77        // nonce
78        let mut nonce_buf = [0u8; 32];
79        nonce_buf[24..].copy_from_slice(&self.nonce.to_be_bytes());
80        data.extend_from_slice(&nonce_buf);
81
82        // deadline
83        let mut deadline_buf = [0u8; 32];
84        deadline_buf[24..].copy_from_slice(&self.deadline.to_be_bytes());
85        data.extend_from_slice(&deadline_buf);
86
87        keccak256(&data)
88    }
89
90    /// Compute the EIP-712 signing hash for this permit.
91    ///
92    /// `keccak256("\x19\x01" || domain_separator || struct_hash)`
93    #[must_use]
94    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
95        let mut buf = Vec::with_capacity(2 + 32 + 32);
96        buf.push(0x19);
97        buf.push(0x01);
98        buf.extend_from_slice(domain_separator);
99        buf.extend_from_slice(&self.struct_hash());
100        keccak256(&buf)
101    }
102
103    /// Sign this permit with the given signer.
104    pub fn sign(
105        &self,
106        signer: &super::EthereumSigner,
107        domain_separator: &[u8; 32],
108    ) -> Result<super::EthereumSignature, SignerError> {
109        let hash = self.signing_hash(domain_separator);
110        signer.sign_digest(&hash)
111    }
112}
113
114// ═══════════════════════════════════════════════════════════════════
115// EIP-4337: Account Abstraction UserOperation
116// ═══════════════════════════════════════════════════════════════════
117
118/// EIP-4337 UserOperation for account abstraction wallets.
119///
120/// This struct represents a user operation that gets submitted to a bundler
121/// instead of a regular transaction.
122#[derive(Debug, Clone)]
123pub struct UserOperation {
124    /// The account making the operation.
125    pub sender: [u8; 20],
126    /// Anti-replay nonce.
127    pub nonce: [u8; 32],
128    /// Contract creation code + calldata (for new accounts).
129    pub init_code: Vec<u8>,
130    /// The calldata to execute on the sender account.
131    pub call_data: Vec<u8>,
132    /// Gas limit for the execution phase.
133    pub call_gas_limit: [u8; 32],
134    /// Gas limit for verification.
135    pub verification_gas_limit: [u8; 32],
136    /// Gas for pre-verification (bundler overhead).
137    pub pre_verification_gas: [u8; 32],
138    /// Maximum fee per gas.
139    pub max_fee_per_gas: [u8; 32],
140    /// Maximum priority fee per gas.
141    pub max_priority_fee_per_gas: [u8; 32],
142    /// Paymaster address + data (empty if self-paying).
143    pub paymaster_and_data: Vec<u8>,
144}
145
146impl UserOperation {
147    /// Pack the UserOperation for hashing (without signature).
148    ///
149    /// Returns the ABI-encoded hash input as specified in EIP-4337.
150    #[must_use]
151    pub fn pack(&self) -> Vec<u8> {
152        let mut data = Vec::with_capacity(320);
153
154        // sender (left-padded to 32)
155        let mut sender_buf = [0u8; 32];
156        sender_buf[12..].copy_from_slice(&self.sender);
157        data.extend_from_slice(&sender_buf);
158
159        data.extend_from_slice(&self.nonce);
160        data.extend_from_slice(&keccak256(&self.init_code));
161        data.extend_from_slice(&keccak256(&self.call_data));
162        data.extend_from_slice(&self.call_gas_limit);
163        data.extend_from_slice(&self.verification_gas_limit);
164        data.extend_from_slice(&self.pre_verification_gas);
165        data.extend_from_slice(&self.max_fee_per_gas);
166        data.extend_from_slice(&self.max_priority_fee_per_gas);
167        data.extend_from_slice(&keccak256(&self.paymaster_and_data));
168
169        data
170    }
171
172    /// Compute the UserOperation hash.
173    ///
174    /// `keccak256(abi.encode(pack(userOp), entryPoint, chainId))`
175    #[must_use]
176    pub fn hash(&self, entry_point: &[u8; 20], chain_id: [u8; 32]) -> [u8; 32] {
177        let packed_hash = keccak256(&self.pack());
178        let mut data = Vec::with_capacity(3 * 32);
179        data.extend_from_slice(&packed_hash);
180
181        let mut ep_buf = [0u8; 32];
182        ep_buf[12..].copy_from_slice(entry_point);
183        data.extend_from_slice(&ep_buf);
184
185        data.extend_from_slice(&chain_id);
186
187        keccak256(&data)
188    }
189
190    /// Sign this UserOperation.
191    pub fn sign(
192        &self,
193        signer: &super::EthereumSigner,
194        entry_point: &[u8; 20],
195        chain_id: [u8; 32],
196    ) -> Result<super::EthereumSignature, SignerError> {
197        let hash = self.hash(entry_point, chain_id);
198        signer.sign_digest(&hash)
199    }
200}
201
202/// Convert a `u64` into canonical `uint256` encoding.
203#[must_use]
204pub fn uint256_from_u64(value: u64) -> [u8; 32] {
205    let mut out = [0u8; 32];
206    out[24..].copy_from_slice(&value.to_be_bytes());
207    out
208}
209
210// ═══════════════════════════════════════════════════════════════════
211// EIP-7702: Set EOA Account Code
212// ═══════════════════════════════════════════════════════════════════
213
214/// EIP-7702 authorization for setting EOA account code.
215///
216/// An EOA signs an authorization allowing its account to be temporarily
217/// delegated to a contract implementation.
218#[derive(Debug, Clone)]
219pub struct Eip7702Authorization {
220    /// Chain ID this authorization is valid for (0 = any chain).
221    pub chain_id: u64,
222    /// Contract address to delegate to.
223    pub address: [u8; 20],
224    /// Authorization nonce.
225    pub nonce: u64,
226}
227
228impl Eip7702Authorization {
229    /// The EIP-7702 authorization magic.
230    pub const MAGIC: u8 = 0x05;
231
232    /// Compute the signing hash for this authorization.
233    ///
234    /// `keccak256(MAGIC || RLP([chain_id, address, nonce]))`
235    #[must_use]
236    pub fn signing_hash(&self) -> [u8; 32] {
237        use super::rlp;
238        let mut items = Vec::new();
239        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
240        items.extend_from_slice(&rlp::encode_bytes(&self.address));
241        items.extend_from_slice(&rlp::encode_u64(self.nonce));
242        let rlp_data = rlp::encode_list(&items);
243
244        let mut payload = vec![Self::MAGIC];
245        payload.extend_from_slice(&rlp_data);
246        keccak256(&payload)
247    }
248
249    /// Sign this authorization.
250    pub fn sign(
251        &self,
252        signer: &super::EthereumSigner,
253    ) -> Result<super::EthereumSignature, SignerError> {
254        let hash = self.signing_hash();
255        signer.sign_digest(&hash)
256    }
257}
258
259// ═══════════════════════════════════════════════════════════════════
260// EIP-3074: AUTH Message Hash
261// ═══════════════════════════════════════════════════════════════════
262
263/// EIP-3074 AUTH message for authorizing an invoker contract.
264///
265/// The AUTH opcode verifies this signature to authorize the invoker
266/// to act on behalf of the signer.
267#[derive(Debug, Clone)]
268pub struct AuthMessage {
269    /// The invoker contract address.
270    pub invoker: [u8; 20],
271    /// Commit hash (application-specific commitment).
272    pub commit: [u8; 32],
273}
274
275impl AuthMessage {
276    /// The EIP-3074 AUTH magic byte.
277    pub const MAGIC: u8 = 0x04;
278
279    /// Compute the AUTH signing hash.
280    ///
281    /// `keccak256(MAGIC || pad32(chainId) || pad32(nonce) || pad32(invoker) || commit)`
282    ///
283    /// Note: In production, `chain_id` and `nonce` come from the EVM context.
284    /// This method accepts them as parameters for flexibility.
285    #[must_use]
286    pub fn signing_hash(&self, chain_id: u64, nonce: u64) -> [u8; 32] {
287        let mut buf = Vec::with_capacity(1 + 4 * 32);
288        buf.push(Self::MAGIC);
289
290        // chain_id padded to 32 bytes
291        let mut chain_buf = [0u8; 32];
292        chain_buf[24..].copy_from_slice(&chain_id.to_be_bytes());
293        buf.extend_from_slice(&chain_buf);
294
295        // nonce padded to 32 bytes
296        let mut nonce_buf = [0u8; 32];
297        nonce_buf[24..].copy_from_slice(&nonce.to_be_bytes());
298        buf.extend_from_slice(&nonce_buf);
299
300        // invoker padded to 32 bytes
301        let mut invoker_buf = [0u8; 32];
302        invoker_buf[12..].copy_from_slice(&self.invoker);
303        buf.extend_from_slice(&invoker_buf);
304
305        // commit (32 bytes)
306        buf.extend_from_slice(&self.commit);
307
308        keccak256(&buf)
309    }
310
311    /// Sign the AUTH message.
312    pub fn sign(
313        &self,
314        signer: &super::EthereumSigner,
315        chain_id: u64,
316        nonce: u64,
317    ) -> Result<super::EthereumSignature, SignerError> {
318        let hash = self.signing_hash(chain_id, nonce);
319        signer.sign_digest(&hash)
320    }
321}
322
323// ═══════════════════════════════════════════════════════════════════
324// EIP-6492: Pre-deploy Contract Signatures
325// ═══════════════════════════════════════════════════════════════════
326
327/// EIP-6492 magic suffix bytes appended to wrapped signatures.
328pub const EIP6492_MAGIC: [u8; 32] = [
329    0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92,
330    0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92,
331];
332
333/// Wrap a signature with EIP-6492 format for pre-deploy contract wallets.
334///
335/// Format: `abi.encode(create2Factory, factoryCalldata, originalSig) ++ magicBytes`
336///
337/// This allows verification of signatures from smart contract wallets
338/// that haven't been deployed yet (counterfactual wallets).
339#[must_use]
340pub fn wrap_eip6492_signature(
341    create2_factory: &[u8; 20],
342    factory_calldata: &[u8],
343    original_signature: &[u8],
344) -> Vec<u8> {
345    use super::abi::{self, AbiValue};
346    let mut encoded = abi::encode(&[
347        AbiValue::Address(*create2_factory),
348        AbiValue::Bytes(factory_calldata.to_vec()),
349        AbiValue::Bytes(original_signature.to_vec()),
350    ]);
351    encoded.extend_from_slice(&EIP6492_MAGIC);
352    encoded
353}
354
355/// Check if a signature is EIP-6492 wrapped.
356#[must_use]
357pub fn is_eip6492_signature(signature: &[u8]) -> bool {
358    signature.len() > 32 && signature[signature.len() - 32..] == EIP6492_MAGIC
359}
360
361/// Unwrap an EIP-6492 signature, returning the inner data without the magic suffix.
362///
363/// The caller is responsible for ABI-decoding the result to extract
364/// `(address factory, bytes factoryCalldata, bytes originalSig)`.
365pub fn unwrap_eip6492_signature(signature: &[u8]) -> Result<&[u8], SignerError> {
366    if !is_eip6492_signature(signature) {
367        return Err(SignerError::ParseError("not an EIP-6492 signature".into()));
368    }
369    Ok(&signature[..signature.len() - 32])
370}
371
372// ═══════════════════════════════════════════════════════════════════
373// EIP-5267: EIP-712 Domain Retrieval
374// ═══════════════════════════════════════════════════════════════════
375
376/// Encode the `eip712Domain()` function call for EIP-5267.
377///
378/// Returns the ABI-encoded calldata to query a contract's EIP-712 domain.
379/// The response contains: `(bytes1 fields, string name, string version,
380/// uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)`.
381#[must_use]
382pub fn encode_eip712_domain_call() -> Vec<u8> {
383    // Function selector: keccak256("eip712Domain()")[..4] = 0x84b0196e
384    let selector = &keccak256(b"eip712Domain()")[..4];
385    selector.to_vec()
386}
387
388/// The function selector for `eip712Domain()` (EIP-5267).
389pub const EIP5267_SELECTOR: [u8; 4] = [0x84, 0xb0, 0x19, 0x6e];
390
391// ═══════════════════════════════════════════════════════════════════
392// EIP-2335: BLS12-381 Keystore Constants
393// ═══════════════════════════════════════════════════════════════════
394
395/// Standard BLS12-381 key derivation paths (EIP-2334).
396pub mod bls_paths {
397    /// Withdrawal key path: `m/12381/3600/{validator_index}/0`
398    pub fn withdrawal(validator_index: u32) -> Vec<u32> {
399        vec![12381, 3600, validator_index, 0]
400    }
401
402    /// Signing key path: `m/12381/3600/{validator_index}/0/0`
403    pub fn signing(validator_index: u32) -> Vec<u32> {
404        vec![12381, 3600, validator_index, 0, 0]
405    }
406}
407
408/// EIP-2335 keystore version constant.
409pub const EIP2335_VERSION: u32 = 4;
410
411/// EIP-2335 keystore description type.
412pub const EIP2335_KDF: &str = "scrypt";
413/// EIP-2335 keystore cipher algorithm.
414pub const EIP2335_CIPHER: &str = "aes-128-ctr";
415/// EIP-2335 keystore checksum algorithm.
416pub const EIP2335_CHECKSUM: &str = "sha256";
417
418// ═══════════════════════════════════════════════════════════════════
419// EIP-3009: Transfer With Authorization (USDC-style meta-tx)
420// ═══════════════════════════════════════════════════════════════════
421
422/// EIP-3009 TransferWithAuthorization for gasless token transfers.
423///
424/// Used by USDC and other compliant tokens. The token holder signs a
425/// typed message authorizing a relayer to execute the transfer on their behalf.
426#[derive(Debug, Clone)]
427pub struct TransferWithAuthorization {
428    /// Token holder (sender).
429    pub from: [u8; 20],
430    /// Recipient address.
431    pub to: [u8; 20],
432    /// Transfer amount (32-byte big-endian uint256).
433    pub value: [u8; 32],
434    /// Earliest valid timestamp.
435    pub valid_after: u64,
436    /// Latest valid timestamp.
437    pub valid_before: u64,
438    /// Unique nonce (32 bytes, chosen by the authorizer).
439    pub nonce: [u8; 32],
440}
441
442impl TransferWithAuthorization {
443    /// The EIP-712 typehash for `TransferWithAuthorization`.
444    ///
445    /// `keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")`
446    #[must_use]
447    pub fn type_hash() -> [u8; 32] {
448        keccak256(b"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
449    }
450
451    /// Compute the struct hash.
452    #[must_use]
453    pub fn struct_hash(&self) -> [u8; 32] {
454        let mut data = Vec::with_capacity(7 * 32);
455        data.extend_from_slice(&Self::type_hash());
456
457        let mut from_padded = [0u8; 32];
458        from_padded[12..].copy_from_slice(&self.from);
459        data.extend_from_slice(&from_padded);
460
461        let mut to_padded = [0u8; 32];
462        to_padded[12..].copy_from_slice(&self.to);
463        data.extend_from_slice(&to_padded);
464
465        data.extend_from_slice(&self.value);
466
467        let mut va = [0u8; 32];
468        va[24..].copy_from_slice(&self.valid_after.to_be_bytes());
469        data.extend_from_slice(&va);
470
471        let mut vb = [0u8; 32];
472        vb[24..].copy_from_slice(&self.valid_before.to_be_bytes());
473        data.extend_from_slice(&vb);
474
475        data.extend_from_slice(&self.nonce);
476
477        keccak256(&data)
478    }
479
480    /// Compute the EIP-712 signing hash.
481    #[must_use]
482    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
483        eip712_signing_hash(domain_separator, &self.struct_hash())
484    }
485
486    /// Sign this authorization.
487    pub fn sign(
488        &self,
489        signer: &super::EthereumSigner,
490        domain_separator: &[u8; 32],
491    ) -> Result<super::EthereumSignature, SignerError> {
492        let hash = self.signing_hash(domain_separator);
493        signer.sign_digest(&hash)
494    }
495}
496
497/// EIP-3009 ReceiveWithAuthorization for pull-based token transfers.
498#[derive(Debug, Clone)]
499pub struct ReceiveWithAuthorization {
500    /// Token holder (sender).
501    pub from: [u8; 20],
502    /// Recipient (must be msg.sender that calls the contract).
503    pub to: [u8; 20],
504    /// Transfer amount (32-byte big-endian uint256).
505    pub value: [u8; 32],
506    /// Earliest valid timestamp.
507    pub valid_after: u64,
508    /// Latest valid timestamp.
509    pub valid_before: u64,
510    /// Unique nonce (32 bytes).
511    pub nonce: [u8; 32],
512}
513
514impl ReceiveWithAuthorization {
515    /// The EIP-712 typehash for `ReceiveWithAuthorization`.
516    #[must_use]
517    pub fn type_hash() -> [u8; 32] {
518        keccak256(b"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
519    }
520
521    /// Compute the struct hash.
522    #[must_use]
523    pub fn struct_hash(&self) -> [u8; 32] {
524        let mut data = Vec::with_capacity(7 * 32);
525        data.extend_from_slice(&Self::type_hash());
526
527        let mut from_padded = [0u8; 32];
528        from_padded[12..].copy_from_slice(&self.from);
529        data.extend_from_slice(&from_padded);
530
531        let mut to_padded = [0u8; 32];
532        to_padded[12..].copy_from_slice(&self.to);
533        data.extend_from_slice(&to_padded);
534
535        data.extend_from_slice(&self.value);
536
537        let mut va = [0u8; 32];
538        va[24..].copy_from_slice(&self.valid_after.to_be_bytes());
539        data.extend_from_slice(&va);
540
541        let mut vb = [0u8; 32];
542        vb[24..].copy_from_slice(&self.valid_before.to_be_bytes());
543        data.extend_from_slice(&vb);
544
545        data.extend_from_slice(&self.nonce);
546
547        keccak256(&data)
548    }
549
550    /// Sign this authorization.
551    pub fn sign(
552        &self,
553        signer: &super::EthereumSigner,
554        domain_separator: &[u8; 32],
555    ) -> Result<super::EthereumSignature, SignerError> {
556        let hash = eip712_signing_hash(domain_separator, &self.struct_hash());
557        signer.sign_digest(&hash)
558    }
559}
560
561/// EIP-3009 CancelAuthorization.
562#[derive(Debug, Clone)]
563pub struct CancelAuthorization {
564    /// The authorizer address.
565    pub authorizer: [u8; 20],
566    /// Nonce to cancel.
567    pub nonce: [u8; 32],
568}
569
570impl CancelAuthorization {
571    /// The EIP-712 typehash for `CancelAuthorization`.
572    #[must_use]
573    pub fn type_hash() -> [u8; 32] {
574        keccak256(b"CancelAuthorization(address authorizer,bytes32 nonce)")
575    }
576
577    /// Compute the struct hash.
578    #[must_use]
579    pub fn struct_hash(&self) -> [u8; 32] {
580        let mut data = Vec::with_capacity(3 * 32);
581        data.extend_from_slice(&Self::type_hash());
582
583        let mut auth_padded = [0u8; 32];
584        auth_padded[12..].copy_from_slice(&self.authorizer);
585        data.extend_from_slice(&auth_padded);
586
587        data.extend_from_slice(&self.nonce);
588
589        keccak256(&data)
590    }
591
592    /// Sign this cancellation.
593    pub fn sign(
594        &self,
595        signer: &super::EthereumSigner,
596        domain_separator: &[u8; 32],
597    ) -> Result<super::EthereumSignature, SignerError> {
598        let hash = eip712_signing_hash(domain_separator, &self.struct_hash());
599        signer.sign_digest(&hash)
600    }
601}
602
603// ═══════════════════════════════════════════════════════════════════
604// EIP-4494: ERC-721 Permit (NFT Gasless Approve)
605// ═══════════════════════════════════════════════════════════════════
606
607/// EIP-4494 ERC-721 Permit for gasless NFT approvals.
608///
609/// Similar to EIP-2612 but for NFTs. The owner signs a permit allowing
610/// a spender to transfer a specific token ID.
611#[derive(Debug, Clone)]
612pub struct Erc721Permit {
613    /// Address being approved.
614    pub spender: [u8; 20],
615    /// Token ID to approve (as 32-byte big-endian uint256).
616    pub token_id: [u8; 32],
617    /// Current nonce for this token on the contract.
618    pub nonce: u64,
619    /// Unix timestamp deadline.
620    pub deadline: u64,
621}
622
623impl Erc721Permit {
624    /// The EIP-712 typehash for ERC-721 Permit.
625    ///
626    /// `keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")`
627    #[must_use]
628    pub fn type_hash() -> [u8; 32] {
629        keccak256(b"Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")
630    }
631
632    /// Compute the struct hash.
633    #[must_use]
634    pub fn struct_hash(&self) -> [u8; 32] {
635        let mut data = Vec::with_capacity(5 * 32);
636        data.extend_from_slice(&Self::type_hash());
637
638        let mut spender_padded = [0u8; 32];
639        spender_padded[12..].copy_from_slice(&self.spender);
640        data.extend_from_slice(&spender_padded);
641
642        data.extend_from_slice(&self.token_id);
643
644        let mut nonce_buf = [0u8; 32];
645        nonce_buf[24..].copy_from_slice(&self.nonce.to_be_bytes());
646        data.extend_from_slice(&nonce_buf);
647
648        let mut deadline_buf = [0u8; 32];
649        deadline_buf[24..].copy_from_slice(&self.deadline.to_be_bytes());
650        data.extend_from_slice(&deadline_buf);
651
652        keccak256(&data)
653    }
654
655    /// Compute the EIP-712 signing hash.
656    #[must_use]
657    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
658        eip712_signing_hash(domain_separator, &self.struct_hash())
659    }
660
661    /// Sign this permit.
662    pub fn sign(
663        &self,
664        signer: &super::EthereumSigner,
665        domain_separator: &[u8; 32],
666    ) -> Result<super::EthereumSignature, SignerError> {
667        let hash = self.signing_hash(domain_separator);
668        signer.sign_digest(&hash)
669    }
670}
671
672// ═══════════════════════════════════════════════════════════════════
673// Multicall Encoding
674// ═══════════════════════════════════════════════════════════════════
675
676/// Encode a batch of calls for Multicall3 (`aggregate3`).
677///
678/// Multicall3 (0xcA11bde05977b3631167028862bE2a173976CA11) is deployed
679/// on virtually every EVM chain.
680///
681/// # Arguments
682/// - `calls` — list of `(target_address, allow_failure, calldata)`
683///
684/// # Returns
685/// ABI-encoded calldata for `aggregate3((address,bool,bytes)[])`
686#[must_use]
687pub fn encode_multicall3(calls: &[([u8; 20], bool, Vec<u8>)]) -> Vec<u8> {
688    use super::abi::{self, AbiValue};
689
690    // aggregate3 selector: keccak256("aggregate3((address,bool,bytes)[])")
691    let selector = &keccak256(b"aggregate3((address,bool,bytes)[])")[..4];
692
693    let call_tuples: Vec<AbiValue> = calls
694        .iter()
695        .map(|(target, allow, cd)| {
696            AbiValue::Tuple(vec![
697                AbiValue::Address(*target),
698                AbiValue::Bool(*allow),
699                AbiValue::Bytes(cd.clone()),
700            ])
701        })
702        .collect();
703
704    let mut calldata = Vec::new();
705    calldata.extend_from_slice(selector);
706    calldata.extend_from_slice(&abi::encode(&[AbiValue::Array(call_tuples)]));
707    calldata
708}
709
710/// The canonical Multicall3 contract address (same on all chains).
711pub const MULTICALL3_ADDRESS: [u8; 20] = [
712    0xCA, 0x11, 0xBD, 0xE0, 0x59, 0x77, 0xB3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xBE, 0x2A, 0x17,
713    0x39, 0x76, 0xCA, 0x11,
714];
715
716/// Encode a simple Multicall (`tryAggregate`) for read-only batched calls.
717///
718/// # Arguments  
719/// - `require_success` — if true, reverts on any failed call
720/// - `calls` — list of `(target_address, calldata)`
721///
722/// # Returns
723/// ABI-encoded calldata for `tryAggregate(bool,(address,bytes)[])`
724#[must_use]
725pub fn encode_try_aggregate(require_success: bool, calls: &[([u8; 20], Vec<u8>)]) -> Vec<u8> {
726    use super::abi::{self, AbiValue};
727
728    let selector = &keccak256(b"tryAggregate(bool,(address,bytes)[])")[..4];
729
730    let call_tuples: Vec<AbiValue> = calls
731        .iter()
732        .map(|(target, cd)| {
733            AbiValue::Tuple(vec![
734                AbiValue::Address(*target),
735                AbiValue::Bytes(cd.clone()),
736            ])
737        })
738        .collect();
739
740    let mut calldata = Vec::new();
741    calldata.extend_from_slice(selector);
742    calldata.extend_from_slice(&abi::encode(&[
743        AbiValue::Bool(require_success),
744        AbiValue::Array(call_tuples),
745    ]));
746    calldata
747}
748
749// ─── Internal Helper ───────────────────────────────────────────────
750
751fn eip712_signing_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
752    let mut buf = Vec::with_capacity(2 + 32 + 32);
753    buf.push(0x19);
754    buf.push(0x01);
755    buf.extend_from_slice(domain_separator);
756    buf.extend_from_slice(struct_hash);
757    keccak256(&buf)
758}
759
760// ═══════════════════════════════════════════════════════════════════
761// Tests
762// ═══════════════════════════════════════════════════════════════════
763
764#[cfg(test)]
765#[allow(clippy::unwrap_used, clippy::expect_used)]
766mod tests {
767    use super::*;
768    use crate::traits::KeyPair;
769
770    // ─── EIP-2612 Permit ───────────────────────────────────────────
771
772    #[test]
773    fn test_permit_type_hash() {
774        let hash = Permit::type_hash();
775        assert_eq!(
776            hex::encode(hash),
777            "6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"
778        );
779    }
780
781    #[test]
782    fn test_permit_struct_hash_deterministic() {
783        let permit = Permit {
784            owner: [0xAA; 20],
785            spender: [0xBB; 20],
786            value: [0; 32],
787            nonce: 0,
788            deadline: u64::MAX,
789        };
790        assert_eq!(permit.struct_hash(), permit.struct_hash());
791    }
792
793    #[test]
794    fn test_permit_sign_roundtrip() {
795        let signer = super::super::EthereumSigner::generate().unwrap();
796        let permit = Permit {
797            owner: signer.address(),
798            spender: [0xBB; 20],
799            value: {
800                let mut v = [0u8; 32];
801                v[31] = 100;
802                v
803            },
804            nonce: 0,
805            deadline: u64::MAX,
806        };
807        let domain = [0xCC; 32]; // mock domain separator
808        let sig = permit.sign(&signer, &domain).unwrap();
809        assert_eq!(sig.r.len(), 32);
810        assert_eq!(sig.s.len(), 32);
811    }
812
813    // ─── EIP-4337 UserOperation ────────────────────────────────────
814
815    #[test]
816    fn test_user_op_hash_deterministic() {
817        let op = UserOperation {
818            sender: [0xAA; 20],
819            nonce: [0; 32],
820            init_code: vec![],
821            call_data: vec![0x01, 0x02],
822            call_gas_limit: [0; 32],
823            verification_gas_limit: [0; 32],
824            pre_verification_gas: [0; 32],
825            max_fee_per_gas: [0; 32],
826            max_priority_fee_per_gas: [0; 32],
827            paymaster_and_data: vec![],
828        };
829        let entry_point = [0xBB; 20];
830        let h1 = op.hash(&entry_point, uint256_from_u64(1));
831        let h2 = op.hash(&entry_point, uint256_from_u64(1));
832        assert_eq!(h1, h2);
833    }
834
835    #[test]
836    fn test_user_op_different_chain_different_hash() {
837        let op = UserOperation {
838            sender: [0xAA; 20],
839            nonce: [0; 32],
840            init_code: vec![],
841            call_data: vec![],
842            call_gas_limit: [0; 32],
843            verification_gas_limit: [0; 32],
844            pre_verification_gas: [0; 32],
845            max_fee_per_gas: [0; 32],
846            max_priority_fee_per_gas: [0; 32],
847            paymaster_and_data: vec![],
848        };
849        let ep = [0xBB; 20];
850        assert_ne!(
851            op.hash(&ep, uint256_from_u64(1)),
852            op.hash(&ep, uint256_from_u64(5))
853        );
854    }
855
856    #[test]
857    fn test_user_op_sign() {
858        let signer = super::super::EthereumSigner::generate().unwrap();
859        let op = UserOperation {
860            sender: signer.address(),
861            nonce: [0; 32],
862            init_code: vec![],
863            call_data: vec![],
864            call_gas_limit: [0; 32],
865            verification_gas_limit: [0; 32],
866            pre_verification_gas: [0; 32],
867            max_fee_per_gas: [0; 32],
868            max_priority_fee_per_gas: [0; 32],
869            paymaster_and_data: vec![],
870        };
871        let sig = op.sign(&signer, &[0x55; 20], uint256_from_u64(1)).unwrap();
872        assert_eq!(sig.r.len(), 32);
873    }
874
875    // ─── EIP-7702 Authorization ────────────────────────────────────
876
877    #[test]
878    fn test_eip7702_signing_hash_deterministic() {
879        let auth = Eip7702Authorization {
880            chain_id: 1,
881            address: [0xCC; 20],
882            nonce: 0,
883        };
884        assert_eq!(auth.signing_hash(), auth.signing_hash());
885    }
886
887    #[test]
888    fn test_eip7702_different_chain_different_hash() {
889        let auth1 = Eip7702Authorization {
890            chain_id: 1,
891            address: [0xCC; 20],
892            nonce: 0,
893        };
894        let auth2 = Eip7702Authorization {
895            chain_id: 5,
896            address: [0xCC; 20],
897            nonce: 0,
898        };
899        assert_ne!(auth1.signing_hash(), auth2.signing_hash());
900    }
901
902    #[test]
903    fn test_eip7702_sign() {
904        let signer = super::super::EthereumSigner::generate().unwrap();
905        let auth = Eip7702Authorization {
906            chain_id: 1,
907            address: [0xDD; 20],
908            nonce: 42,
909        };
910        let sig = auth.sign(&signer).unwrap();
911        assert!(sig.v == 27 || sig.v == 28);
912    }
913
914    // ─── EIP-3074 AUTH ─────────────────────────────────────────────
915
916    #[test]
917    fn test_auth_message_hash_deterministic() {
918        let msg = AuthMessage {
919            invoker: [0xEE; 20],
920            commit: [0xFF; 32],
921        };
922        assert_eq!(msg.signing_hash(1, 0), msg.signing_hash(1, 0));
923    }
924
925    #[test]
926    fn test_auth_message_different_nonce() {
927        let msg = AuthMessage {
928            invoker: [0xEE; 20],
929            commit: [0xFF; 32],
930        };
931        assert_ne!(msg.signing_hash(1, 0), msg.signing_hash(1, 1));
932    }
933
934    #[test]
935    fn test_auth_message_sign() {
936        let signer = super::super::EthereumSigner::generate().unwrap();
937        let msg = AuthMessage {
938            invoker: [0xAA; 20],
939            commit: [0xBB; 32],
940        };
941        let sig = msg.sign(&signer, 1, 0).unwrap();
942        assert!(sig.v == 27 || sig.v == 28);
943    }
944
945    // ─── EIP-6492 ──────────────────────────────────────────────────
946
947    #[test]
948    fn test_eip6492_wrap_unwrap() {
949        let factory = [0xAA; 20];
950        let calldata = vec![0xBB; 64];
951        let sig = vec![0xCC; 65];
952        let wrapped = wrap_eip6492_signature(&factory, &calldata, &sig);
953        assert!(is_eip6492_signature(&wrapped));
954        let inner = unwrap_eip6492_signature(&wrapped).unwrap();
955        assert!(!inner.is_empty());
956    }
957
958    #[test]
959    fn test_eip6492_not_wrapped() {
960        let plain_sig = vec![0x00; 65];
961        assert!(!is_eip6492_signature(&plain_sig));
962    }
963
964    // ─── EIP-5267 ──────────────────────────────────────────────────
965
966    #[test]
967    fn test_eip5267_selector() {
968        let calldata = encode_eip712_domain_call();
969        assert_eq!(calldata, EIP5267_SELECTOR);
970    }
971
972    // ─── EIP-2335 BLS Paths ────────────────────────────────────────
973
974    #[test]
975    fn test_bls_signing_path() {
976        let path = bls_paths::signing(0);
977        assert_eq!(path, vec![12381, 3600, 0, 0, 0]);
978    }
979
980    #[test]
981    fn test_bls_withdrawal_path() {
982        let path = bls_paths::withdrawal(5);
983        assert_eq!(path, vec![12381, 3600, 5, 0]);
984    }
985
986    // ─── EIP-3009 TransferWithAuthorization ────────────────────────
987
988    #[test]
989    fn test_transfer_auth_type_hash() {
990        // Known type hash from USDC
991        let hash = TransferWithAuthorization::type_hash();
992        assert_eq!(
993            hex::encode(hash),
994            "7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267"
995        );
996    }
997
998    #[test]
999    fn test_receive_auth_type_hash() {
1000        let hash = ReceiveWithAuthorization::type_hash();
1001        assert_eq!(
1002            hex::encode(hash),
1003            "d099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8"
1004        );
1005    }
1006
1007    #[test]
1008    fn test_cancel_auth_type_hash() {
1009        let hash = CancelAuthorization::type_hash();
1010        assert_eq!(
1011            hex::encode(hash),
1012            "158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429"
1013        );
1014    }
1015
1016    #[test]
1017    fn test_transfer_auth_sign() {
1018        let signer = super::super::EthereumSigner::generate().unwrap();
1019        let auth = TransferWithAuthorization {
1020            from: signer.address(),
1021            to: [0xBB; 20],
1022            value: {
1023                let mut v = [0u8; 32];
1024                v[31] = 100;
1025                v
1026            },
1027            valid_after: 0,
1028            valid_before: u64::MAX,
1029            nonce: [0x42; 32],
1030        };
1031        let domain = [0xCC; 32];
1032        let sig = auth.sign(&signer, &domain).unwrap();
1033        assert!(sig.v == 27 || sig.v == 28);
1034    }
1035
1036    #[test]
1037    fn test_cancel_auth_sign() {
1038        let signer = super::super::EthereumSigner::generate().unwrap();
1039        let cancel = CancelAuthorization {
1040            authorizer: signer.address(),
1041            nonce: [0xFF; 32],
1042        };
1043        let domain = [0xCC; 32];
1044        let sig = cancel.sign(&signer, &domain).unwrap();
1045        assert!(sig.v == 27 || sig.v == 28);
1046    }
1047
1048    #[test]
1049    fn test_transfer_auth_different_nonce_different_hash() {
1050        let auth1 = TransferWithAuthorization {
1051            from: [0xAA; 20],
1052            to: [0xBB; 20],
1053            value: [0; 32],
1054            valid_after: 0,
1055            valid_before: u64::MAX,
1056            nonce: [0x01; 32],
1057        };
1058        let auth2 = TransferWithAuthorization {
1059            nonce: [0x02; 32],
1060            ..auth1.clone()
1061        };
1062        assert_ne!(auth1.struct_hash(), auth2.struct_hash());
1063    }
1064
1065    // ─── EIP-4494 ERC-721 Permit ───────────────────────────────────
1066
1067    #[test]
1068    fn test_erc721_permit_type_hash() {
1069        let hash = Erc721Permit::type_hash();
1070        // Known: keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")
1071        assert_eq!(
1072            hex::encode(hash),
1073            "49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad"
1074        );
1075    }
1076
1077    #[test]
1078    fn test_erc721_permit_sign() {
1079        let signer = super::super::EthereumSigner::generate().unwrap();
1080        let permit = Erc721Permit {
1081            spender: [0xBB; 20],
1082            token_id: {
1083                let mut t = [0u8; 32];
1084                t[31] = 1;
1085                t
1086            }, // tokenId = 1
1087            nonce: 0,
1088            deadline: u64::MAX,
1089        };
1090        let domain = [0xCC; 32];
1091        let sig = permit.sign(&signer, &domain).unwrap();
1092        assert_eq!(sig.r.len(), 32);
1093    }
1094
1095    #[test]
1096    fn test_erc721_permit_different_token_id() {
1097        let perm1 = Erc721Permit {
1098            spender: [0xBB; 20],
1099            token_id: {
1100                let mut t = [0u8; 32];
1101                t[31] = 1;
1102                t
1103            },
1104            nonce: 0,
1105            deadline: u64::MAX,
1106        };
1107        let perm2 = Erc721Permit {
1108            token_id: {
1109                let mut t = [0u8; 32];
1110                t[31] = 2;
1111                t
1112            },
1113            ..perm1.clone()
1114        };
1115        assert_ne!(perm1.struct_hash(), perm2.struct_hash());
1116    }
1117
1118    // ─── Multicall ─────────────────────────────────────────────────
1119
1120    #[test]
1121    fn test_multicall3_encode() {
1122        let calls = vec![
1123            ([0xAA; 20], false, vec![0x01, 0x02, 0x03]),
1124            ([0xBB; 20], true, vec![0x04, 0x05]),
1125        ];
1126        let calldata = encode_multicall3(&calls);
1127        // First 4 bytes: aggregate3 selector
1128        let expected_selector = &keccak256(b"aggregate3((address,bool,bytes)[])")[..4];
1129        assert_eq!(&calldata[..4], expected_selector);
1130        assert!(calldata.len() > 4);
1131    }
1132
1133    #[test]
1134    fn test_multicall3_empty() {
1135        let calldata = encode_multicall3(&[]);
1136        let expected_selector = &keccak256(b"aggregate3((address,bool,bytes)[])")[..4];
1137        assert_eq!(&calldata[..4], expected_selector);
1138    }
1139
1140    #[test]
1141    fn test_try_aggregate_encode() {
1142        let calls = vec![([0xAA; 20], vec![0x01, 0x02])];
1143        let calldata = encode_try_aggregate(true, &calls);
1144        let expected_selector = &keccak256(b"tryAggregate(bool,(address,bytes)[])")[..4];
1145        assert_eq!(&calldata[..4], expected_selector);
1146    }
1147
1148    #[test]
1149    fn test_multicall3_address() {
1150        // Canonical Multicall3 address
1151        assert_eq!(
1152            hex::encode(MULTICALL3_ADDRESS).to_lowercase(),
1153            "ca11bde05977b3631167028862be2a173976ca11"
1154        );
1155    }
1156}