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;
15use sha3::{Digest, Keccak256};
16
17fn keccak256(data: &[u8]) -> [u8; 32] {
18    let mut out = [0u8; 32];
19    out.copy_from_slice(&Keccak256::digest(data));
20    out
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: u64) -> [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        let mut chain_buf = [0u8; 32];
186        chain_buf[24..].copy_from_slice(&chain_id.to_be_bytes());
187        data.extend_from_slice(&chain_buf);
188
189        keccak256(&data)
190    }
191
192    /// Sign this UserOperation.
193    pub fn sign(
194        &self,
195        signer: &super::EthereumSigner,
196        entry_point: &[u8; 20],
197        chain_id: u64,
198    ) -> Result<super::EthereumSignature, SignerError> {
199        let hash = self.hash(entry_point, chain_id);
200        // EIP-191 personal_sign style hashing for the final signature
201        let eth_hash = eth_signed_message_hash(&hash);
202        signer.sign_digest(&eth_hash)
203    }
204}
205
206fn eth_signed_message_hash(hash: &[u8; 32]) -> [u8; 32] {
207    let mut buf = Vec::with_capacity(28 + 32);
208    buf.extend_from_slice(b"\x19Ethereum Signed Message:\n32");
209    buf.extend_from_slice(hash);
210    keccak256(&buf)
211}
212
213// ═══════════════════════════════════════════════════════════════════
214// EIP-7702: Set EOA Account Code
215// ═══════════════════════════════════════════════════════════════════
216
217/// EIP-7702 authorization for setting EOA account code.
218///
219/// An EOA signs an authorization allowing its account to be temporarily
220/// delegated to a contract implementation.
221#[derive(Debug, Clone)]
222pub struct Eip7702Authorization {
223    /// Chain ID this authorization is valid for (0 = any chain).
224    pub chain_id: u64,
225    /// Contract address to delegate to.
226    pub address: [u8; 20],
227    /// Authorization nonce.
228    pub nonce: u64,
229}
230
231impl Eip7702Authorization {
232    /// The EIP-7702 authorization magic.
233    pub const MAGIC: u8 = 0x05;
234
235    /// Compute the signing hash for this authorization.
236    ///
237    /// `keccak256(MAGIC || RLP([chain_id, address, nonce]))`
238    #[must_use]
239    pub fn signing_hash(&self) -> [u8; 32] {
240        use super::rlp;
241        let mut items = Vec::new();
242        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
243        items.extend_from_slice(&rlp::encode_bytes(&self.address));
244        items.extend_from_slice(&rlp::encode_u64(self.nonce));
245        let rlp_data = rlp::encode_list(&items);
246
247        let mut payload = vec![Self::MAGIC];
248        payload.extend_from_slice(&rlp_data);
249        keccak256(&payload)
250    }
251
252    /// Sign this authorization.
253    pub fn sign(
254        &self,
255        signer: &super::EthereumSigner,
256    ) -> Result<super::EthereumSignature, SignerError> {
257        let hash = self.signing_hash();
258        signer.sign_digest(&hash)
259    }
260}
261
262// ═══════════════════════════════════════════════════════════════════
263// EIP-3074: AUTH Message Hash
264// ═══════════════════════════════════════════════════════════════════
265
266/// EIP-3074 AUTH message for authorizing an invoker contract.
267///
268/// The AUTH opcode verifies this signature to authorize the invoker
269/// to act on behalf of the signer.
270#[derive(Debug, Clone)]
271pub struct AuthMessage {
272    /// The invoker contract address.
273    pub invoker: [u8; 20],
274    /// Commit hash (application-specific commitment).
275    pub commit: [u8; 32],
276}
277
278impl AuthMessage {
279    /// The EIP-3074 AUTH magic byte.
280    pub const MAGIC: u8 = 0x04;
281
282    /// Compute the AUTH signing hash.
283    ///
284    /// `keccak256(MAGIC || pad32(chainId) || pad32(nonce) || pad32(invoker) || commit)`
285    ///
286    /// Note: In production, `chain_id` and `nonce` come from the EVM context.
287    /// This method accepts them as parameters for flexibility.
288    #[must_use]
289    pub fn signing_hash(&self, chain_id: u64, nonce: u64) -> [u8; 32] {
290        let mut buf = Vec::with_capacity(1 + 4 * 32);
291        buf.push(Self::MAGIC);
292
293        // chain_id padded to 32 bytes
294        let mut chain_buf = [0u8; 32];
295        chain_buf[24..].copy_from_slice(&chain_id.to_be_bytes());
296        buf.extend_from_slice(&chain_buf);
297
298        // nonce padded to 32 bytes
299        let mut nonce_buf = [0u8; 32];
300        nonce_buf[24..].copy_from_slice(&nonce.to_be_bytes());
301        buf.extend_from_slice(&nonce_buf);
302
303        // invoker padded to 32 bytes
304        let mut invoker_buf = [0u8; 32];
305        invoker_buf[12..].copy_from_slice(&self.invoker);
306        buf.extend_from_slice(&invoker_buf);
307
308        // commit (32 bytes)
309        buf.extend_from_slice(&self.commit);
310
311        keccak256(&buf)
312    }
313
314    /// Sign the AUTH message.
315    pub fn sign(
316        &self,
317        signer: &super::EthereumSigner,
318        chain_id: u64,
319        nonce: u64,
320    ) -> Result<super::EthereumSignature, SignerError> {
321        let hash = self.signing_hash(chain_id, nonce);
322        signer.sign_digest(&hash)
323    }
324}
325
326// ═══════════════════════════════════════════════════════════════════
327// EIP-6492: Pre-deploy Contract Signatures
328// ═══════════════════════════════════════════════════════════════════
329
330/// EIP-6492 magic suffix bytes appended to wrapped signatures.
331pub const EIP6492_MAGIC: [u8; 32] = [
332    0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92,
333    0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92, 0x64, 0x92,
334];
335
336/// Wrap a signature with EIP-6492 format for pre-deploy contract wallets.
337///
338/// Format: `abi.encode(create2Factory, factoryCalldata, originalSig) ++ magicBytes`
339///
340/// This allows verification of signatures from smart contract wallets
341/// that haven't been deployed yet (counterfactual wallets).
342#[must_use]
343pub fn wrap_eip6492_signature(
344    create2_factory: &[u8; 20],
345    factory_calldata: &[u8],
346    original_signature: &[u8],
347) -> Vec<u8> {
348    use super::abi::{self, AbiValue};
349    let mut encoded = abi::encode(&[
350        AbiValue::Address(*create2_factory),
351        AbiValue::Bytes(factory_calldata.to_vec()),
352        AbiValue::Bytes(original_signature.to_vec()),
353    ]);
354    encoded.extend_from_slice(&EIP6492_MAGIC);
355    encoded
356}
357
358/// Check if a signature is EIP-6492 wrapped.
359#[must_use]
360pub fn is_eip6492_signature(signature: &[u8]) -> bool {
361    signature.len() > 32 && signature[signature.len() - 32..] == EIP6492_MAGIC
362}
363
364/// Unwrap an EIP-6492 signature, returning the inner data without the magic suffix.
365///
366/// The caller is responsible for ABI-decoding the result to extract
367/// `(address factory, bytes factoryCalldata, bytes originalSig)`.
368pub fn unwrap_eip6492_signature(signature: &[u8]) -> Result<&[u8], SignerError> {
369    if !is_eip6492_signature(signature) {
370        return Err(SignerError::ParseError("not an EIP-6492 signature".into()));
371    }
372    Ok(&signature[..signature.len() - 32])
373}
374
375// ═══════════════════════════════════════════════════════════════════
376// EIP-5267: EIP-712 Domain Retrieval
377// ═══════════════════════════════════════════════════════════════════
378
379/// Encode the `eip712Domain()` function call for EIP-5267.
380///
381/// Returns the ABI-encoded calldata to query a contract's EIP-712 domain.
382/// The response contains: `(bytes1 fields, string name, string version,
383/// uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)`.
384#[must_use]
385pub fn encode_eip712_domain_call() -> Vec<u8> {
386    // Function selector: keccak256("eip712Domain()")[..4] = 0x84b0196e
387    let selector = &keccak256(b"eip712Domain()")[..4];
388    selector.to_vec()
389}
390
391/// The function selector for `eip712Domain()` (EIP-5267).
392pub const EIP5267_SELECTOR: [u8; 4] = [0x84, 0xb0, 0x19, 0x6e];
393
394// ═══════════════════════════════════════════════════════════════════
395// EIP-2335: BLS12-381 Keystore Constants
396// ═══════════════════════════════════════════════════════════════════
397
398/// Standard BLS12-381 key derivation paths (EIP-2334).
399pub mod bls_paths {
400    /// Withdrawal key path: `m/12381/3600/{validator_index}/0`
401    pub fn withdrawal(validator_index: u32) -> Vec<u32> {
402        vec![12381, 3600, validator_index, 0]
403    }
404
405    /// Signing key path: `m/12381/3600/{validator_index}/0/0`
406    pub fn signing(validator_index: u32) -> Vec<u32> {
407        vec![12381, 3600, validator_index, 0, 0]
408    }
409}
410
411/// EIP-2335 keystore version constant.
412pub const EIP2335_VERSION: u32 = 4;
413
414/// EIP-2335 keystore description type.
415pub const EIP2335_KDF: &str = "scrypt";
416/// EIP-2335 keystore cipher algorithm.
417pub const EIP2335_CIPHER: &str = "aes-128-ctr";
418/// EIP-2335 keystore checksum algorithm.
419pub const EIP2335_CHECKSUM: &str = "sha256";
420
421// ═══════════════════════════════════════════════════════════════════
422// EIP-3009: Transfer With Authorization (USDC-style meta-tx)
423// ═══════════════════════════════════════════════════════════════════
424
425/// EIP-3009 TransferWithAuthorization for gasless token transfers.
426///
427/// Used by USDC and other compliant tokens. The token holder signs a
428/// typed message authorizing a relayer to execute the transfer on their behalf.
429#[derive(Debug, Clone)]
430pub struct TransferWithAuthorization {
431    /// Token holder (sender).
432    pub from: [u8; 20],
433    /// Recipient address.
434    pub to: [u8; 20],
435    /// Transfer amount (32-byte big-endian uint256).
436    pub value: [u8; 32],
437    /// Earliest valid timestamp.
438    pub valid_after: u64,
439    /// Latest valid timestamp.
440    pub valid_before: u64,
441    /// Unique nonce (32 bytes, chosen by the authorizer).
442    pub nonce: [u8; 32],
443}
444
445impl TransferWithAuthorization {
446    /// The EIP-712 typehash for `TransferWithAuthorization`.
447    ///
448    /// `keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")`
449    #[must_use]
450    pub fn type_hash() -> [u8; 32] {
451        keccak256(b"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
452    }
453
454    /// Compute the struct hash.
455    #[must_use]
456    pub fn struct_hash(&self) -> [u8; 32] {
457        let mut data = Vec::with_capacity(7 * 32);
458        data.extend_from_slice(&Self::type_hash());
459
460        let mut from_padded = [0u8; 32];
461        from_padded[12..].copy_from_slice(&self.from);
462        data.extend_from_slice(&from_padded);
463
464        let mut to_padded = [0u8; 32];
465        to_padded[12..].copy_from_slice(&self.to);
466        data.extend_from_slice(&to_padded);
467
468        data.extend_from_slice(&self.value);
469
470        let mut va = [0u8; 32];
471        va[24..].copy_from_slice(&self.valid_after.to_be_bytes());
472        data.extend_from_slice(&va);
473
474        let mut vb = [0u8; 32];
475        vb[24..].copy_from_slice(&self.valid_before.to_be_bytes());
476        data.extend_from_slice(&vb);
477
478        data.extend_from_slice(&self.nonce);
479
480        keccak256(&data)
481    }
482
483    /// Compute the EIP-712 signing hash.
484    #[must_use]
485    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
486        eip712_signing_hash(domain_separator, &self.struct_hash())
487    }
488
489    /// Sign this authorization.
490    pub fn sign(
491        &self,
492        signer: &super::EthereumSigner,
493        domain_separator: &[u8; 32],
494    ) -> Result<super::EthereumSignature, SignerError> {
495        let hash = self.signing_hash(domain_separator);
496        signer.sign_digest(&hash)
497    }
498}
499
500/// EIP-3009 ReceiveWithAuthorization for pull-based token transfers.
501#[derive(Debug, Clone)]
502pub struct ReceiveWithAuthorization {
503    /// Token holder (sender).
504    pub from: [u8; 20],
505    /// Recipient (must be msg.sender that calls the contract).
506    pub to: [u8; 20],
507    /// Transfer amount (32-byte big-endian uint256).
508    pub value: [u8; 32],
509    /// Earliest valid timestamp.
510    pub valid_after: u64,
511    /// Latest valid timestamp.
512    pub valid_before: u64,
513    /// Unique nonce (32 bytes).
514    pub nonce: [u8; 32],
515}
516
517impl ReceiveWithAuthorization {
518    /// The EIP-712 typehash for `ReceiveWithAuthorization`.
519    #[must_use]
520    pub fn type_hash() -> [u8; 32] {
521        keccak256(b"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
522    }
523
524    /// Compute the struct hash.
525    #[must_use]
526    pub fn struct_hash(&self) -> [u8; 32] {
527        let mut data = Vec::with_capacity(7 * 32);
528        data.extend_from_slice(&Self::type_hash());
529
530        let mut from_padded = [0u8; 32];
531        from_padded[12..].copy_from_slice(&self.from);
532        data.extend_from_slice(&from_padded);
533
534        let mut to_padded = [0u8; 32];
535        to_padded[12..].copy_from_slice(&self.to);
536        data.extend_from_slice(&to_padded);
537
538        data.extend_from_slice(&self.value);
539
540        let mut va = [0u8; 32];
541        va[24..].copy_from_slice(&self.valid_after.to_be_bytes());
542        data.extend_from_slice(&va);
543
544        let mut vb = [0u8; 32];
545        vb[24..].copy_from_slice(&self.valid_before.to_be_bytes());
546        data.extend_from_slice(&vb);
547
548        data.extend_from_slice(&self.nonce);
549
550        keccak256(&data)
551    }
552
553    /// Sign this authorization.
554    pub fn sign(
555        &self,
556        signer: &super::EthereumSigner,
557        domain_separator: &[u8; 32],
558    ) -> Result<super::EthereumSignature, SignerError> {
559        let hash = eip712_signing_hash(domain_separator, &self.struct_hash());
560        signer.sign_digest(&hash)
561    }
562}
563
564/// EIP-3009 CancelAuthorization.
565#[derive(Debug, Clone)]
566pub struct CancelAuthorization {
567    /// The authorizer address.
568    pub authorizer: [u8; 20],
569    /// Nonce to cancel.
570    pub nonce: [u8; 32],
571}
572
573impl CancelAuthorization {
574    /// The EIP-712 typehash for `CancelAuthorization`.
575    #[must_use]
576    pub fn type_hash() -> [u8; 32] {
577        keccak256(b"CancelAuthorization(address authorizer,bytes32 nonce)")
578    }
579
580    /// Compute the struct hash.
581    #[must_use]
582    pub fn struct_hash(&self) -> [u8; 32] {
583        let mut data = Vec::with_capacity(3 * 32);
584        data.extend_from_slice(&Self::type_hash());
585
586        let mut auth_padded = [0u8; 32];
587        auth_padded[12..].copy_from_slice(&self.authorizer);
588        data.extend_from_slice(&auth_padded);
589
590        data.extend_from_slice(&self.nonce);
591
592        keccak256(&data)
593    }
594
595    /// Sign this cancellation.
596    pub fn sign(
597        &self,
598        signer: &super::EthereumSigner,
599        domain_separator: &[u8; 32],
600    ) -> Result<super::EthereumSignature, SignerError> {
601        let hash = eip712_signing_hash(domain_separator, &self.struct_hash());
602        signer.sign_digest(&hash)
603    }
604}
605
606// ═══════════════════════════════════════════════════════════════════
607// EIP-4494: ERC-721 Permit (NFT Gasless Approve)
608// ═══════════════════════════════════════════════════════════════════
609
610/// EIP-4494 ERC-721 Permit for gasless NFT approvals.
611///
612/// Similar to EIP-2612 but for NFTs. The owner signs a permit allowing
613/// a spender to transfer a specific token ID.
614#[derive(Debug, Clone)]
615pub struct Erc721Permit {
616    /// Address being approved.
617    pub spender: [u8; 20],
618    /// Token ID to approve (as 32-byte big-endian uint256).
619    pub token_id: [u8; 32],
620    /// Current nonce for this token on the contract.
621    pub nonce: u64,
622    /// Unix timestamp deadline.
623    pub deadline: u64,
624}
625
626impl Erc721Permit {
627    /// The EIP-712 typehash for ERC-721 Permit.
628    ///
629    /// `keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")`
630    #[must_use]
631    pub fn type_hash() -> [u8; 32] {
632        keccak256(b"Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")
633    }
634
635    /// Compute the struct hash.
636    #[must_use]
637    pub fn struct_hash(&self) -> [u8; 32] {
638        let mut data = Vec::with_capacity(5 * 32);
639        data.extend_from_slice(&Self::type_hash());
640
641        let mut spender_padded = [0u8; 32];
642        spender_padded[12..].copy_from_slice(&self.spender);
643        data.extend_from_slice(&spender_padded);
644
645        data.extend_from_slice(&self.token_id);
646
647        let mut nonce_buf = [0u8; 32];
648        nonce_buf[24..].copy_from_slice(&self.nonce.to_be_bytes());
649        data.extend_from_slice(&nonce_buf);
650
651        let mut deadline_buf = [0u8; 32];
652        deadline_buf[24..].copy_from_slice(&self.deadline.to_be_bytes());
653        data.extend_from_slice(&deadline_buf);
654
655        keccak256(&data)
656    }
657
658    /// Compute the EIP-712 signing hash.
659    #[must_use]
660    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
661        eip712_signing_hash(domain_separator, &self.struct_hash())
662    }
663
664    /// Sign this permit.
665    pub fn sign(
666        &self,
667        signer: &super::EthereumSigner,
668        domain_separator: &[u8; 32],
669    ) -> Result<super::EthereumSignature, SignerError> {
670        let hash = self.signing_hash(domain_separator);
671        signer.sign_digest(&hash)
672    }
673}
674
675// ═══════════════════════════════════════════════════════════════════
676// Multicall Encoding
677// ═══════════════════════════════════════════════════════════════════
678
679/// Encode a batch of calls for Multicall3 (`aggregate3`).
680///
681/// Multicall3 (0xcA11bde05977b3631167028862bE2a173976CA11) is deployed
682/// on virtually every EVM chain.
683///
684/// # Arguments
685/// - `calls` — list of `(target_address, allow_failure, calldata)`
686///
687/// # Returns
688/// ABI-encoded calldata for `aggregate3((address,bool,bytes)[])`
689#[must_use]
690pub fn encode_multicall3(calls: &[([u8; 20], bool, Vec<u8>)]) -> Vec<u8> {
691    use super::abi::{self, AbiValue};
692
693    // aggregate3 selector: keccak256("aggregate3((address,bool,bytes)[])")
694    let selector = &keccak256(b"aggregate3((address,bool,bytes)[])")[..4];
695
696    let call_tuples: Vec<AbiValue> = calls
697        .iter()
698        .map(|(target, allow, cd)| {
699            AbiValue::Tuple(vec![
700                AbiValue::Address(*target),
701                AbiValue::Bool(*allow),
702                AbiValue::Bytes(cd.clone()),
703            ])
704        })
705        .collect();
706
707    let mut calldata = Vec::new();
708    calldata.extend_from_slice(selector);
709    calldata.extend_from_slice(&abi::encode(&[AbiValue::Array(call_tuples)]));
710    calldata
711}
712
713/// The canonical Multicall3 contract address (same on all chains).
714pub const MULTICALL3_ADDRESS: [u8; 20] = [
715    0xCA, 0x11, 0xBD, 0xE0, 0x59, 0x77, 0xB3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xBE, 0x2A, 0x17,
716    0x39, 0x76, 0xCA, 0x11,
717];
718
719/// Encode a simple Multicall (`tryAggregate`) for read-only batched calls.
720///
721/// # Arguments  
722/// - `require_success` — if true, reverts on any failed call
723/// - `calls` — list of `(target_address, calldata)`
724///
725/// # Returns
726/// ABI-encoded calldata for `tryAggregate(bool,(address,bytes)[])`
727#[must_use]
728pub fn encode_try_aggregate(require_success: bool, calls: &[([u8; 20], Vec<u8>)]) -> Vec<u8> {
729    use super::abi::{self, AbiValue};
730
731    let selector = &keccak256(b"tryAggregate(bool,(address,bytes)[])")[..4];
732
733    let call_tuples: Vec<AbiValue> = calls
734        .iter()
735        .map(|(target, cd)| {
736            AbiValue::Tuple(vec![
737                AbiValue::Address(*target),
738                AbiValue::Bytes(cd.clone()),
739            ])
740        })
741        .collect();
742
743    let mut calldata = Vec::new();
744    calldata.extend_from_slice(selector);
745    calldata.extend_from_slice(&abi::encode(&[
746        AbiValue::Bool(require_success),
747        AbiValue::Array(call_tuples),
748    ]));
749    calldata
750}
751
752// ─── Internal Helper ───────────────────────────────────────────────
753
754fn eip712_signing_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
755    let mut buf = Vec::with_capacity(2 + 32 + 32);
756    buf.push(0x19);
757    buf.push(0x01);
758    buf.extend_from_slice(domain_separator);
759    buf.extend_from_slice(struct_hash);
760    keccak256(&buf)
761}
762
763// ═══════════════════════════════════════════════════════════════════
764// Tests
765// ═══════════════════════════════════════════════════════════════════
766
767#[cfg(test)]
768#[allow(clippy::unwrap_used, clippy::expect_used)]
769mod tests {
770    use super::*;
771    use crate::traits::KeyPair;
772
773    // ─── EIP-2612 Permit ───────────────────────────────────────────
774
775    #[test]
776    fn test_permit_type_hash() {
777        let hash = Permit::type_hash();
778        assert_eq!(
779            hex::encode(hash),
780            "6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"
781        );
782    }
783
784    #[test]
785    fn test_permit_struct_hash_deterministic() {
786        let permit = Permit {
787            owner: [0xAA; 20],
788            spender: [0xBB; 20],
789            value: [0; 32],
790            nonce: 0,
791            deadline: u64::MAX,
792        };
793        assert_eq!(permit.struct_hash(), permit.struct_hash());
794    }
795
796    #[test]
797    fn test_permit_sign_roundtrip() {
798        let signer = super::super::EthereumSigner::generate().unwrap();
799        let permit = Permit {
800            owner: signer.address(),
801            spender: [0xBB; 20],
802            value: {
803                let mut v = [0u8; 32];
804                v[31] = 100;
805                v
806            },
807            nonce: 0,
808            deadline: u64::MAX,
809        };
810        let domain = [0xCC; 32]; // mock domain separator
811        let sig = permit.sign(&signer, &domain).unwrap();
812        assert_eq!(sig.r.len(), 32);
813        assert_eq!(sig.s.len(), 32);
814    }
815
816    // ─── EIP-4337 UserOperation ────────────────────────────────────
817
818    #[test]
819    fn test_user_op_hash_deterministic() {
820        let op = UserOperation {
821            sender: [0xAA; 20],
822            nonce: [0; 32],
823            init_code: vec![],
824            call_data: vec![0x01, 0x02],
825            call_gas_limit: [0; 32],
826            verification_gas_limit: [0; 32],
827            pre_verification_gas: [0; 32],
828            max_fee_per_gas: [0; 32],
829            max_priority_fee_per_gas: [0; 32],
830            paymaster_and_data: vec![],
831        };
832        let entry_point = [0xBB; 20];
833        let h1 = op.hash(&entry_point, 1);
834        let h2 = op.hash(&entry_point, 1);
835        assert_eq!(h1, h2);
836    }
837
838    #[test]
839    fn test_user_op_different_chain_different_hash() {
840        let op = UserOperation {
841            sender: [0xAA; 20],
842            nonce: [0; 32],
843            init_code: vec![],
844            call_data: vec![],
845            call_gas_limit: [0; 32],
846            verification_gas_limit: [0; 32],
847            pre_verification_gas: [0; 32],
848            max_fee_per_gas: [0; 32],
849            max_priority_fee_per_gas: [0; 32],
850            paymaster_and_data: vec![],
851        };
852        let ep = [0xBB; 20];
853        assert_ne!(op.hash(&ep, 1), op.hash(&ep, 5));
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], 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}