Skip to main content

o2_tools/
signature_ext.rs

1use fuels::{
2    core::{
3        codec::calldata,
4        traits::Tokenizable,
5    },
6    crypto::Message,
7    types::{
8        Bits256,
9        ChainId,
10        Identity,
11        U256 as FuelU256,
12    },
13};
14
15use crate::{
16    trade_account::{
17        generate_session_signing_payload,
18        generate_signing_payload,
19    },
20    trade_account_deploy::{
21        CallContractArg,
22        CallContractArgs as TradeAccountCallContractArgs,
23        CallParams,
24        Domain,
25        EIP712Domain,
26        Encoding,
27        MultiCallContractArgs,
28        ParallelCallContractArgs,
29        ParallelMultiCallContractArgs,
30        ParallelRevokeArgs,
31        ParallelSessionArgs,
32        ParallelWithdrawArgs,
33        RevokeArgs,
34        SRC16Domain,
35        Secp256k1,
36        SessionArgs,
37        SetProxyTargetArgs,
38        Signature,
39        WithdrawArgs,
40    },
41};
42use ethers_core::{
43    types::{
44        H160,
45        RecoveryMessage,
46        Signature as EthSig,
47        U256,
48        U256 as Ethers256,
49        transaction::eip712::EIP712Domain as EVMDomain,
50    },
51    utils::keccak256,
52};
53
54const EVM_ADDRESS_PADDING_LENGTH: usize = 12;
55
56/// Takes the rightmost 20 BYTES of a 32-byte Fuel contract ID.
57/// Interprets "rightmost" as the last bytes in the array / hex string.
58fn fuel_contract_id_to_evm_address(id: [u8; 32]) -> [u8; 20] {
59    let mut addr = [0u8; 20];
60    // last 20 bytes: indices 12..32
61    addr.copy_from_slice(&id[12..32]);
62    addr
63}
64
65pub trait DomainExt {
66    fn separator(&self) -> [u8; 32];
67    fn encode<T>(&self, struct_type: T) -> [u8; 32]
68    where
69        T: SRC16Encode;
70}
71
72impl DomainExt for Domain {
73    fn separator(&self) -> [u8; 32] {
74        match self {
75            Domain::EIP712Domain(d) => EVMDomain::from(d.clone()).separator(),
76            Domain::SRC16Domain(d) => d.separator(),
77        }
78    }
79
80    fn encode<T>(&self, struct_type: T) -> [u8; 32]
81    where
82        T: SRC16Encode,
83    {
84        let ds = self.separator();
85        let encoding = match self {
86            Domain::SRC16Domain(_) => Encoding::SRC16,
87            Domain::EIP712Domain(_) => Encoding::EIP712,
88        };
89        let sh = struct_type.struct_hash(encoding);
90
91        let mut preimage = Vec::with_capacity(2 + 32 + 32);
92        preimage.extend_from_slice(&[0x19, 0x01]);
93        preimage.extend_from_slice(ds.as_slice());
94        preimage.extend_from_slice(sh.as_slice());
95
96        keccak256(preimage)
97    }
98}
99
100impl SRC16Domain {
101    // Compute the domain separator;
102    pub fn separator(&self) -> [u8; 32] {
103        let mut tokens: Vec<u8> = Vec::new();
104
105        tokens.extend_from_slice(src16_domain_type_hash(self).as_ref());
106
107        if self.name.is_some() {
108            tokens.extend_from_slice(keccak256(self.name.clone().unwrap()).as_slice());
109        }
110        if self.version.is_some() {
111            tokens.extend_from_slice(keccak256(self.version.clone().unwrap()).as_slice());
112        }
113
114        if let Some(chain_id) = self.chain_id {
115            // The Sway contract reads the `u256 chain_id` straight
116            // from VM memory with `mcp` (memory copy). Fuel VM stores
117            // u256 in big-endian byte order — the most-significant
118            // u64 limb sits at the lowest memory address, the
119            // least-significant limb at the highest. `fuels::U256`
120            // (backed by `ethers-core::U256`) stores limbs in the
121            // OPPOSITE order: `0[0]` is the least-significant u64.
122            // The previous `chain_id.0.iter().flat_map(to_be_bytes)`
123            // therefore wrote the LSU at byte offset 0 — invisible
124            // when `chain_id == 0` (all limbs are zero, both
125            // orderings byte-match) but produced a domain separator
126            // that disagreed with the on-chain contract on every
127            // non-zero chain id. The only test environments that
128            // exercise typed signatures use `chain_id = 0`, so the
129            // bug shipped through 0.1.23 → 0.2.2 unnoticed.
130            tokens.extend_from_slice(fuel_u256_to_bytes(&chain_id).as_slice());
131        }
132
133        if let Some(verifying_contract) = self.verifying_contract {
134            tokens.extend_from_slice(verifying_contract.as_slice());
135        }
136
137        if let Some(salt) = self.salt {
138            tokens.extend_from_slice(salt.0.as_slice());
139        }
140
141        keccak256(tokens)
142    }
143}
144
145fn src16_domain_type_hash(domain: &SRC16Domain) -> [u8; 32] {
146    let mut fields = Vec::new();
147
148    // IMPORTANT: fixed order
149    if domain.name.is_some() {
150        fields.push("string name");
151    }
152    if domain.version.is_some() {
153        fields.push("string version");
154    }
155    if domain.chain_id.is_some() {
156        fields.push("u256 chain_id");
157    }
158    if domain.verifying_contract.is_some() {
159        fields.push("contractId verifying_contract");
160    }
161    if domain.salt.is_some() {
162        fields.push("b256 salt");
163    }
164
165    let type_string = format!("SRC16Domain({})", fields.join(","));
166
167    keccak256(type_string.as_bytes())
168}
169
170fn fuel_u256_to_bytes(value: &FuelU256) -> [u8; 32] {
171    let mut bytes = [0u8; 32];
172    value.to_big_endian(&mut bytes);
173    bytes
174}
175
176impl From<EIP712Domain> for EVMDomain {
177    fn from(domain: EIP712Domain) -> Self {
178        let chain_id = domain.chain_id.map(|id| Ethers256(id.0));
179        let verifying_contract = domain
180            .verifying_contract
181            .map(|contract| H160(fuel_contract_id_to_evm_address(contract.0)));
182        let salt = domain.salt.map(|s| s.0);
183
184        EVMDomain {
185            name: domain.name,
186            version: domain.version,
187            chain_id,
188            verifying_contract,
189            salt,
190        }
191    }
192}
193
194pub trait SRC16Encode {
195    #[allow(dead_code)]
196    fn type_hash(encoding: Encoding) -> [u8; 32];
197    fn struct_hash(self, encoding: Encoding) -> [u8; 32];
198}
199
200fn from_compact_or_standard(bytes: &[u8]) -> anyhow::Result<EthSig> {
201    match bytes.len() {
202        65 => Ok(EthSig::try_from(bytes)?),
203        64 => {
204            let r = U256::from_big_endian(&bytes[..32]);
205            let mut s_bytes = [0u8; 32];
206            s_bytes.copy_from_slice(&bytes[32..64]);
207
208            let y_parity = (s_bytes[0] >> 7) & 1;
209            // In compact ECDSA signatures, the y-parity is encoded in the most significant bit
210            // of the first byte of the s value. This bit must be cleared to obtain the actual s value.
211            s_bytes[0] &= 0x7F;
212
213            let s = U256::from_big_endian(&s_bytes);
214            // Ethereum uses v = 27 or 28 for legacy signatures; here, we add 27 to the parity bit.
215            let v = 27 + y_parity as u64;
216
217            Ok(EthSig { r, s, v })
218        }
219        n => anyhow::bail!("invalid length {n}, expected 64 or 65"),
220    }
221}
222
223pub trait SignatureCurve {
224    type Address;
225
226    /// Verify that the owner signed the payload
227    fn verify_owner<T>(
228        &self,
229        nonce: u64,
230        chain_id: ChainId,
231        f_name: String,
232        args: Option<T>,
233        address: &Self::Address,
234    ) -> anyhow::Result<()>
235    where
236        T: Tokenizable;
237
238    /// Verify that the session key signed the payload
239    fn verify_session<T>(
240        &self,
241        nonce: u64,
242        args: T,
243        address: &Self::Address,
244    ) -> anyhow::Result<()>
245    where
246        T: Tokenizable;
247
248    fn verify_typed<T>(
249        &self,
250        args: T,
251        domain: &Domain,
252        address: &Self::Address,
253    ) -> anyhow::Result<()>
254    where
255        T: SRC16Encode;
256}
257
258pub struct Secp256k1Address(pub fuels::types::Address);
259
260impl SignatureCurve for Secp256k1 {
261    type Address = Secp256k1Address;
262
263    fn verify_owner<T>(
264        &self,
265        nonce: u64,
266        chain_id: ChainId,
267        f_name: String,
268        args: Option<T>,
269        address: &Self::Address,
270    ) -> anyhow::Result<()>
271    where
272        T: Tokenizable,
273    {
274        let address = &address.0;
275
276        // if the first 12 bytes of the address are all zeros, then we assume it
277        // is an EVM (Ethereum) address and signature
278        if address.starts_with([0u8; EVM_ADDRESS_PADDING_LENGTH].as_ref()) {
279            let evm_address = &address[EVM_ADDRESS_PADDING_LENGTH..];
280            let evm_message = if let Some(args_some) = args {
281                calldata!((nonce, *chain_id, f_name, args_some))?
282            } else {
283                calldata!((nonce, *chain_id, f_name))?
284            };
285            let evm_sig = from_compact_or_standard(&self.bits)?;
286            let recovered_address = evm_sig.recover(&evm_message[..])?;
287
288            if recovered_address.as_bytes() != evm_address {
289                anyhow::bail!("invalid evm address provided");
290            }
291
292            evm_sig.verify(evm_message, recovered_address)?;
293        } else {
294            let fuel_message = generate_signing_payload(nonce, *chain_id, f_name, args);
295            let fuels_sig = fuels::crypto::Signature::from_bytes(self.bits);
296            let recovered_pub_key = fuels_sig.recover(&fuel_message)?;
297
298            if *recovered_pub_key.hash() != address.as_ref() {
299                anyhow::bail!("invalid fuel address provided");
300            }
301
302            fuels_sig.verify(&recovered_pub_key, &fuel_message)?;
303        }
304
305        Ok(())
306    }
307
308    fn verify_session<T>(
309        &self,
310        nonce: u64,
311        args: T,
312        address: &Self::Address,
313    ) -> anyhow::Result<()>
314    where
315        T: Tokenizable,
316    {
317        let address = &address.0;
318        let fuel_message = generate_session_signing_payload(nonce, args);
319
320        let fuels_sig = fuels::crypto::Signature::from_bytes(self.bits);
321        let recovered_pub_key = fuels_sig.recover(&fuel_message)?;
322
323        if *recovered_pub_key.hash() != address.as_ref() {
324            anyhow::bail!("invalid fuel address provided");
325        }
326
327        fuels_sig.verify(&recovered_pub_key, &fuel_message)?;
328
329        Ok(())
330    }
331
332    fn verify_typed<T>(
333        &self,
334        args: T,
335        domain: &Domain,
336        address: &Self::Address,
337    ) -> anyhow::Result<()>
338    where
339        T: SRC16Encode,
340    {
341        let address = &address.0;
342
343        // Encode the typed data
344        let message: Message = Message::from_bytes(domain.encode(args));
345
346        // Check if this is an EVM address or Fuel address based on padding
347        if address.starts_with([0u8; EVM_ADDRESS_PADDING_LENGTH].as_ref()) {
348            // EVM (Ethereum) address verification
349            let evm_address = &address[EVM_ADDRESS_PADDING_LENGTH..];
350            let evm_sig = from_compact_or_standard(&self.bits)?;
351            let recovered_address =
352                evm_sig.recover(RecoveryMessage::Hash((*message).into()))?;
353
354            if recovered_address.as_bytes() != evm_address {
355                anyhow::bail!("invalid evm address provided");
356            }
357
358            evm_sig
359                .verify(RecoveryMessage::Hash((*message).into()), recovered_address)?;
360        } else {
361            // Fuel address verification
362            let fuels_sig = fuels::crypto::Signature::from_bytes(self.bits);
363            let recovered_pub_key = fuels_sig.recover(&message)?;
364
365            if *recovered_pub_key.hash() != address.as_ref() {
366                anyhow::bail!("invalid fuel address provided");
367            }
368
369            fuels_sig.verify(&recovered_pub_key, &message)?;
370        }
371
372        Ok(())
373    }
374}
375
376pub enum Address {
377    Secp256k1(Secp256k1Address),
378    Secp256r1,
379    Ed25519,
380}
381
382impl From<fuels::types::Address> for Address {
383    fn from(addr: fuels::types::Address) -> Self {
384        Address::Secp256k1(Secp256k1Address(addr))
385    }
386}
387
388impl Signature {
389    pub fn verify_owner<T>(
390        &self,
391        nonce: u64,
392        chain_id: ChainId,
393        f_name: String,
394        args: Option<T>,
395        address: &Address,
396    ) -> anyhow::Result<()>
397    where
398        T: Tokenizable,
399    {
400        match (self, address) {
401            (Signature::Secp256k1(sig), Address::Secp256k1(addr)) => {
402                sig.verify_owner(nonce, chain_id, f_name, args, addr)
403            }
404            (Signature::Secp256r1(_), Address::Secp256r1) => {
405                anyhow::bail!(
406                    "owner signature verification not supported for p256 signatures"
407                )
408            }
409            (Signature::Ed25519(_), Address::Ed25519) => {
410                anyhow::bail!(
411                    "owner signature verification not supported for ed25519 signatures"
412                )
413            }
414            _ => anyhow::bail!("signature and address type mismatch"),
415        }
416    }
417
418    pub fn verify_session<T>(
419        &self,
420        nonce: u64,
421        args: T,
422        address: &Address,
423    ) -> anyhow::Result<()>
424    where
425        T: Tokenizable,
426    {
427        match (self, address) {
428            (Signature::Secp256k1(sig), Address::Secp256k1(addr)) => {
429                sig.verify_session(nonce, args, addr)
430            }
431            (Signature::Secp256r1(_), Address::Secp256r1) => {
432                anyhow::bail!(
433                    "session signature verification not supported for p256 signatures"
434                )
435            }
436            (Signature::Ed25519(_), Address::Ed25519) => {
437                anyhow::bail!(
438                    "session signature verification not supported for ed25519 signatures"
439                )
440            }
441            _ => anyhow::bail!("signature and address type mismatch"),
442        }
443    }
444
445    pub fn verify_typed<T>(
446        &self,
447        args: T,
448        domain: &Domain,
449        address: &Address,
450    ) -> anyhow::Result<()>
451    where
452        T: SRC16Encode,
453    {
454        match (self, address) {
455            (Signature::Secp256k1(sig), Address::Secp256k1(addr)) => {
456                sig.verify_typed(args, domain, addr)
457            }
458            (Signature::Secp256r1(_), Address::Secp256r1) => {
459                anyhow::bail!(
460                    "typed signature verification not supported for p256 signatures"
461                )
462            }
463            (Signature::Ed25519(_), Address::Ed25519) => {
464                anyhow::bail!(
465                    "typed signature verification not supported for ed25519 signatures"
466                )
467            }
468            _ => anyhow::bail!("signature and address type mismatch"),
469        }
470    }
471}
472
473/// The Keccak256 hash of the type SessionArgs as UTF8 encoded bytes.
474///
475/// "SessionArgs(u64 nonce,b256 session_id,u64 expiry,contractId[] contract_ids)"
476pub const SRC_16_SIGNED_SESSION_TYPE_HASH: &str =
477    "0x2eb25d47a049c7084035785d680582656395f354f9484e7e3bbb2993368e46b7";
478
479/// The Keccak256 hash of the type SessionArgs as UTF8 encoded bytes.
480///
481/// "SessionArgs(uint64 nonce,bytes32 session_id,uint64 expiry,bytes32[] contract_ids)"
482pub const EIP_712_SIGNED_SESSION_TYPE_HASH: &str =
483    "0xf91528e233badfd402ad99c4d1dd9d109ec218093c9dcf7432699f5bb94721e2";
484
485impl SRC16Encode for SessionArgs {
486    fn type_hash(encoding: Encoding) -> [u8; 32] {
487        match encoding {
488            Encoding::SRC16 => {
489                Bits256::from_hex_str(SRC_16_SIGNED_SESSION_TYPE_HASH)
490                    .unwrap()
491                    .0
492            }
493            Encoding::EIP712 => {
494                Bits256::from_hex_str(EIP_712_SIGNED_SESSION_TYPE_HASH)
495                    .unwrap()
496                    .0
497            }
498        }
499    }
500    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
501        let mut tokens: Vec<u8> = Vec::new();
502        match encoding {
503            Encoding::SRC16 => tokens.extend_from_slice(
504                Bits256::from_hex_str(SRC_16_SIGNED_SESSION_TYPE_HASH)
505                    .unwrap()
506                    .0
507                    .as_slice(),
508            ),
509            Encoding::EIP712 => tokens.extend_from_slice(
510                Bits256::from_hex_str(EIP_712_SIGNED_SESSION_TYPE_HASH)
511                    .unwrap()
512                    .0
513                    .as_slice(),
514            ),
515        }
516
517        // nonce u64
518        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
519        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
520        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
521        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
522        // session id b256
523        match self.session_id {
524            Identity::Address(bits) => tokens.extend_from_slice(bits.as_slice()),
525            Identity::ContractId(bits) => tokens.extend_from_slice(bits.as_slice()),
526        }
527        // expiry u64
528        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
529        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
530        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
531        tokens.extend_from_slice(self.expiry.unix.to_be_bytes().as_slice());
532
533        // contract ids b256[]
534        let contract_id_bytes: Vec<u8> = self
535            .contract_ids
536            .iter()
537            .flat_map(|id| id.as_ref())
538            .copied()
539            .collect();
540
541        tokens.extend_from_slice(keccak256(contract_id_bytes).as_slice());
542
543        keccak256(tokens)
544    }
545}
546
547/// The Keccak256 hash of the type WithdrawArgs as UTF8 encoded bytes.
548///
549/// "WithdrawArgs(u64 nonce,identity to,u64 amount,assetId asset_id)"
550pub const SRC_16_SIGNED_WITHDRAW_TYPE_HASH: &str =
551    "0x9732cd3b83bcf964465644c9c8546dff5d9f2e5c16c077dba9a2a2bc2b9f7188";
552
553/// The Keccak256 hash of the type WithdrawArgs as UTF8 encoded bytes.
554///
555/// "WithdrawArgs(uint64 nonce,bytes32 to,uint64 amount,bytes32 asset_id)"
556pub const EIP_712_SIGNED_WITHDRAW_TYPE_HASH: &str =
557    "0x138ab031e51a15ab8c1d21d9a203f6cc5c134d7dbb44a9fd3f6acc3231af1c32";
558
559impl SRC16Encode for &WithdrawArgs {
560    fn type_hash(encoding: Encoding) -> [u8; 32] {
561        match encoding {
562            Encoding::SRC16 => {
563                Bits256::from_hex_str(SRC_16_SIGNED_WITHDRAW_TYPE_HASH)
564                    .unwrap()
565                    .0
566            }
567            Encoding::EIP712 => {
568                Bits256::from_hex_str(EIP_712_SIGNED_WITHDRAW_TYPE_HASH)
569                    .unwrap()
570                    .0
571            }
572        }
573    }
574    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
575        let mut tokens: Vec<u8> = Vec::new();
576        match encoding {
577            Encoding::SRC16 => tokens.extend_from_slice(
578                Bits256::from_hex_str(SRC_16_SIGNED_WITHDRAW_TYPE_HASH)
579                    .unwrap()
580                    .0
581                    .as_slice(),
582            ),
583            Encoding::EIP712 => tokens.extend_from_slice(
584                Bits256::from_hex_str(EIP_712_SIGNED_WITHDRAW_TYPE_HASH)
585                    .unwrap()
586                    .0
587                    .as_slice(),
588            ),
589        }
590
591        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
592        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
593        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
594        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
595        match self.to {
596            Identity::Address(bits) => tokens.extend_from_slice(bits.as_slice()),
597            Identity::ContractId(bits) => tokens.extend_from_slice(bits.as_slice()),
598        }
599        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
600        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
601        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
602        tokens.extend_from_slice(self.amount.to_be_bytes().as_slice());
603        tokens.extend_from_slice(self.asset_id.as_slice());
604
605        keccak256(tokens)
606    }
607}
608
609/// The Keccak256 hash of the type CallParams as UTF8 encoded bytes.
610///
611/// "CallParams(u64 coins,assetId asset_id,u64 gas)"
612pub const SRC_16_SIGNED_CALL_PARAMS_TYPE_HASH: &str =
613    "0x05816efb1220d5dd49f0abf9882712729285e7080da3dc2ba051a6ab701cf3b8";
614
615/// The Keccak256 hash of the type CallParams as UTF8 encoded bytes.
616///
617/// "CallParams(uint64 coins,bytes32 asset_id,uint64 gas)"
618pub const EIP_712_SIGNED_CALL_PARAMS_TYPE_HASH: &str =
619    "0x322b030cfd61eddd3d0acc5c37358539477197bfc2599dc09a9c75ad47f6e5b8";
620
621impl SRC16Encode for CallParams {
622    fn type_hash(encoding: Encoding) -> [u8; 32] {
623        match encoding {
624            Encoding::SRC16 => {
625                Bits256::from_hex_str(SRC_16_SIGNED_CALL_PARAMS_TYPE_HASH)
626                    .unwrap()
627                    .0
628            }
629            Encoding::EIP712 => {
630                Bits256::from_hex_str(EIP_712_SIGNED_CALL_PARAMS_TYPE_HASH)
631                    .unwrap()
632                    .0
633            }
634        }
635    }
636    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
637        let mut tokens: Vec<u8> = Vec::new();
638        match encoding {
639            Encoding::SRC16 => tokens.extend_from_slice(
640                Bits256::from_hex_str(SRC_16_SIGNED_CALL_PARAMS_TYPE_HASH)
641                    .unwrap()
642                    .0
643                    .as_slice(),
644            ),
645            Encoding::EIP712 => tokens.extend_from_slice(
646                Bits256::from_hex_str(EIP_712_SIGNED_CALL_PARAMS_TYPE_HASH)
647                    .unwrap()
648                    .0
649                    .as_slice(),
650            ),
651        }
652
653        // Call Params
654        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
655        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
656        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
657        tokens.extend_from_slice(self.coins.to_be_bytes().as_slice());
658        tokens.extend_from_slice(self.asset_id.as_slice());
659        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
660        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
661        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
662        tokens.extend_from_slice(self.gas.to_be_bytes().as_slice());
663
664        keccak256(tokens)
665    }
666}
667
668/// The Keccak256 hash of the type CallContractArg as UTF8 encoded bytes.
669///
670/// "CallContractArg(contractId contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(u64 coins,assetId asset_id,u64 gas)"
671pub const SRC_16_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH: &str =
672    "0xc8ac163d1f87886f48788901b8e5eb4eebb3edad4ae111859b2a051059920048";
673
674/// The Keccak256 hash of the type CallContractArg as UTF8 encoded bytes.
675///
676/// "CallContractArg(bytes32 contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(uint64 coins,bytes32 asset_id,uint64 gas)"
677pub const EIP_712_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH: &str =
678    "0x325b44fd679189ccd8f708a440a27448cbfd6f47f90205e8ace3f1efc27c92d6";
679
680impl SRC16Encode for CallContractArg {
681    fn type_hash(encoding: Encoding) -> [u8; 32] {
682        match encoding {
683            Encoding::SRC16 => {
684                Bits256::from_hex_str(SRC_16_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH)
685                    .unwrap()
686                    .0
687            }
688            Encoding::EIP712 => {
689                Bits256::from_hex_str(EIP_712_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH)
690                    .unwrap()
691                    .0
692            }
693        }
694    }
695    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
696        let mut tokens: Vec<u8> = Vec::new();
697        match encoding {
698            Encoding::SRC16 => tokens.extend_from_slice(
699                Bits256::from_hex_str(SRC_16_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH)
700                    .unwrap()
701                    .0
702                    .as_slice(),
703            ),
704            Encoding::EIP712 => tokens.extend_from_slice(
705                Bits256::from_hex_str(EIP_712_SIGNED_CALL_CONTRACT_ARG_TYPE_HASH)
706                    .unwrap()
707                    .0
708                    .as_slice(),
709            ),
710        }
711
712        // Contract Id
713        tokens.extend_from_slice(self.contract_id.as_slice());
714
715        // Function selector
716        tokens.extend_from_slice(keccak256(self.function_selector.0).as_slice());
717
718        // Call Params
719        tokens.extend_from_slice(self.call_params.struct_hash(encoding).as_slice());
720
721        // Call data
722        match self.call_data {
723            Some(data) => tokens.extend_from_slice(keccak256(data.0).as_slice()),
724            None => tokens.extend_from_slice(keccak256(Vec::new()).as_slice()),
725        }
726
727        keccak256(tokens)
728    }
729}
730
731/// The Keccak256 hash of the type CallContractArgs as UTF8 encoded bytes.
732///
733/// "CallContractArgs(u64 nonce,CallContractArg call_contract_args)CallContractArg(contractId contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(u64 coins,assetId asset_id,u64 gas)"
734pub const SRC_16_SIGNED_CALL_CONTRACT_TYPE_HASH: &str =
735    "0x3f9265dacb0b84ea31e56285f5757a2200e743f27f0b27c3d58db09c9f62cb46";
736
737/// The Keccak256 hash of the type CallContractArgs as UTF8 encoded bytes.
738///
739/// "CallContractArgs(uint64 nonce,CallContractArg call_contract_args)CallContractArg(bytes32 contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(uint64 coins,bytes32 asset_id,uint64 gas)"
740pub const EIP_712_SIGNED_CALL_CONTRACT_TYPE_HASH: &str =
741    "0x26ade4c76b3df03eed7d7a2d1387896e8717f8a862414199904b8a4455b25699";
742
743impl SRC16Encode for TradeAccountCallContractArgs {
744    fn type_hash(encoding: Encoding) -> [u8; 32] {
745        match encoding {
746            Encoding::SRC16 => {
747                Bits256::from_hex_str(SRC_16_SIGNED_CALL_CONTRACT_TYPE_HASH)
748                    .unwrap()
749                    .0
750            }
751            Encoding::EIP712 => {
752                Bits256::from_hex_str(EIP_712_SIGNED_CALL_CONTRACT_TYPE_HASH)
753                    .unwrap()
754                    .0
755            }
756        }
757    }
758    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
759        let mut tokens: Vec<u8> = Vec::new();
760        match encoding {
761            Encoding::SRC16 => tokens.extend_from_slice(
762                Bits256::from_hex_str(SRC_16_SIGNED_CALL_CONTRACT_TYPE_HASH)
763                    .unwrap()
764                    .0
765                    .as_slice(),
766            ),
767            Encoding::EIP712 => tokens.extend_from_slice(
768                Bits256::from_hex_str(EIP_712_SIGNED_CALL_CONTRACT_TYPE_HASH)
769                    .unwrap()
770                    .0
771                    .as_slice(),
772            ),
773        }
774
775        // Nonce
776        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
777        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
778        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
779        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
780
781        // Call Contract args
782        tokens
783            .extend_from_slice(self.call_contract_args.struct_hash(encoding).as_slice());
784
785        keccak256(tokens)
786    }
787}
788
789/// The Keccak256 hash of the type MultiCallContractArgs as UTF8 encoded bytes.
790///
791/// "MultiCallContractArgs(u64 nonce,CallContractArg[] call_contract_args)CallContractArg(contractId contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(u64 coins,assetId asset_id,u64 gas)"
792pub const SRC_16_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH: &str =
793    "0x44c30cb1e12dbc44e06a72b087359f9af5caae554d43dada454d5f6619eabf5f";
794
795/// The Keccak256 hash of the type MultiCallContractArgs as UTF8 encoded bytes.
796///
797/// "MultiCallContractArgs(uint64 nonce,CallContractArg[] call_contract_args)CallContractArg(bytes32 contract_id,bytes function_selector,CallParams call_params,bytes call_data)CallParams(uint64 coins,bytes32 asset_id,uint64 gas)"
798pub const EIP_712_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH: &str =
799    "0x292b2913cef817f041ea01e3b02c4dd2b7184e6490f21a84ece8443e406640a1";
800
801impl SRC16Encode for MultiCallContractArgs {
802    fn type_hash(encoding: Encoding) -> [u8; 32] {
803        match encoding {
804            Encoding::SRC16 => {
805                Bits256::from_hex_str(SRC_16_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH)
806                    .unwrap()
807                    .0
808            }
809            Encoding::EIP712 => {
810                Bits256::from_hex_str(EIP_712_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH)
811                    .unwrap()
812                    .0
813            }
814        }
815    }
816    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
817        let mut tokens: Vec<u8> = Vec::new();
818        match encoding {
819            Encoding::SRC16 => tokens.extend_from_slice(
820                Bits256::from_hex_str(SRC_16_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH)
821                    .unwrap()
822                    .0
823                    .as_slice(),
824            ),
825            Encoding::EIP712 => tokens.extend_from_slice(
826                Bits256::from_hex_str(EIP_712_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH)
827                    .unwrap()
828                    .0
829                    .as_slice(),
830            ),
831        }
832
833        // Nonce
834        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
835        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
836        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
837        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
838
839        // Contract call params
840        let mut call_buffer: Vec<u8> = Vec::new();
841        for call in self.call_contract_args {
842            let h = call.struct_hash(encoding.clone());
843            call_buffer.extend_from_slice(h.as_slice());
844        }
845        tokens.extend_from_slice(keccak256(call_buffer.as_slice()).as_slice());
846
847        keccak256(tokens)
848    }
849}
850
851/// The Keccak256 hash of the type RevokeArgs as UTF8 encoded bytes.
852///
853/// "RevokeArgs(u64 nonce)"
854pub const SRC_16_REVOKE_TYPE_HASH: &str =
855    "0x97c9c74c84314e9fcc4ba97ad28e9cde1eda9b79b0229b54219de0e79560ac86";
856
857/// The Keccak256 hash of the type RevokeArgs as UTF8 encoded bytes.
858///
859/// "RevokeArgs(uint64 nonce)"
860pub const EIP_712_REVOKE_TYPE_HASH: &str =
861    "0xf863f4b733d291f6bd744df3ba070a394dd83fced6f038a22195c1b34009dabb";
862
863impl SRC16Encode for RevokeArgs {
864    fn type_hash(encoding: Encoding) -> [u8; 32] {
865        match encoding {
866            Encoding::SRC16 => Bits256::from_hex_str(SRC_16_REVOKE_TYPE_HASH).unwrap().0,
867            Encoding::EIP712 => {
868                Bits256::from_hex_str(EIP_712_REVOKE_TYPE_HASH).unwrap().0
869            }
870        }
871    }
872    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
873        let mut tokens: Vec<u8> = Vec::new();
874        match encoding {
875            Encoding::SRC16 => tokens.extend_from_slice(
876                Bits256::from_hex_str(SRC_16_REVOKE_TYPE_HASH)
877                    .unwrap()
878                    .0
879                    .as_slice(),
880            ),
881            Encoding::EIP712 => tokens.extend_from_slice(
882                Bits256::from_hex_str(EIP_712_REVOKE_TYPE_HASH)
883                    .unwrap()
884                    .0
885                    .as_slice(),
886            ),
887        }
888
889        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
890        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
891        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
892        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
893
894        keccak256(tokens)
895    }
896}
897
898/// The Keccak256 hash of the type SetProxyTargetArgs as UTF8 encoded bytes.
899///
900/// "SetProxyTargetArgs(u64 nonce)"
901pub const SRC_16_PROXY_TARGET_TYPE_HASH: &str =
902    "0x523db1f70cbdfc0982627ed4e52dc429384c32e94e16ddc5fa5ab921035d8d6d";
903
904/// The Keccak256 hash of the type RevokeArgs as UTF8 encoded bytes.
905///
906/// "SetProxyTargetArgs(uint64 nonce)"
907pub const EIP_712_PROXY_TARGET_TYPE_HASH: &str =
908    "0xf12c7a18e931f36d11f09bd52f37183c4993a3a509ac0c2c6cec459cee7a004d";
909
910impl SRC16Encode for SetProxyTargetArgs {
911    fn type_hash(encoding: Encoding) -> [u8; 32] {
912        match encoding {
913            Encoding::SRC16 => {
914                Bits256::from_hex_str(SRC_16_PROXY_TARGET_TYPE_HASH)
915                    .unwrap()
916                    .0
917            }
918            Encoding::EIP712 => {
919                Bits256::from_hex_str(EIP_712_PROXY_TARGET_TYPE_HASH)
920                    .unwrap()
921                    .0
922            }
923        }
924    }
925    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
926        let mut tokens: Vec<u8> = Vec::new();
927        match encoding {
928            Encoding::SRC16 => tokens.extend_from_slice(
929                Bits256::from_hex_str(SRC_16_PROXY_TARGET_TYPE_HASH)
930                    .unwrap()
931                    .0
932                    .as_slice(),
933            ),
934            Encoding::EIP712 => tokens.extend_from_slice(
935                Bits256::from_hex_str(EIP_712_PROXY_TARGET_TYPE_HASH)
936                    .unwrap()
937                    .0
938                    .as_slice(),
939            ),
940        }
941
942        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
943        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
944        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
945        tokens.extend_from_slice(self.nonce.to_be_bytes().as_slice());
946
947        keccak256(tokens)
948    }
949}
950
951pub const SRC_16_PARALLEL_SIGNED_SESSION_TYPE_HASH: &str =
952    "0x7bcb65ebf5b9c6a7eb6a5d92462eeb2b3eba824e9c8f22c2b01533c2e3364019";
953
954pub const EIP_712_PARALLEL_SIGNED_SESSION_TYPE_HASH: &str =
955    "0x67a8adc215ea9fc6e3a50e038c7c123758629a96e6f33c3a5b6bd8534842d9a7";
956
957impl SRC16Encode for &ParallelSessionArgs {
958    fn type_hash(encoding: Encoding) -> [u8; 32] {
959        match encoding {
960            Encoding::SRC16 => {
961                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_SESSION_TYPE_HASH)
962                    .unwrap()
963                    .0
964            }
965            Encoding::EIP712 => {
966                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_SESSION_TYPE_HASH)
967                    .unwrap()
968                    .0
969            }
970        }
971    }
972
973    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
974        let mut tokens: Vec<u8> = Vec::new();
975        match encoding {
976            Encoding::SRC16 => tokens.extend_from_slice(
977                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_SESSION_TYPE_HASH)
978                    .unwrap()
979                    .0
980                    .as_slice(),
981            ),
982            Encoding::EIP712 => tokens.extend_from_slice(
983                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_SESSION_TYPE_HASH)
984                    .unwrap()
985                    .0
986                    .as_slice(),
987            ),
988        }
989
990        tokens.extend_from_slice(fuel_u256_to_bytes(&self.nonce).as_slice());
991        match self.session_id {
992            Identity::Address(bits) => tokens.extend_from_slice(bits.as_slice()),
993            Identity::ContractId(bits) => tokens.extend_from_slice(bits.as_slice()),
994        }
995        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
996        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
997        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
998        tokens.extend_from_slice(self.expiry.unix.to_be_bytes().as_slice());
999
1000        let contract_id_bytes: Vec<u8> = self
1001            .contract_ids
1002            .iter()
1003            .flat_map(|id| id.as_ref())
1004            .copied()
1005            .collect();
1006        tokens.extend_from_slice(keccak256(contract_id_bytes).as_slice());
1007
1008        keccak256(tokens)
1009    }
1010}
1011
1012pub const SRC_16_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH: &str =
1013    "0x95a9c4c23dd3607b14ecdc67fcc42946f61a4f1d153dc7dfbd77ca6c2af1c6f6";
1014
1015pub const EIP_712_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH: &str =
1016    "0x7d2e70c357f073fd00c187b1871bf74c440ec8303b435f001d884d6ffd77ca1c";
1017
1018impl SRC16Encode for &ParallelWithdrawArgs {
1019    fn type_hash(encoding: Encoding) -> [u8; 32] {
1020        match encoding {
1021            Encoding::SRC16 => {
1022                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH)
1023                    .unwrap()
1024                    .0
1025            }
1026            Encoding::EIP712 => {
1027                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH)
1028                    .unwrap()
1029                    .0
1030            }
1031        }
1032    }
1033
1034    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
1035        let mut tokens: Vec<u8> = Vec::new();
1036        match encoding {
1037            Encoding::SRC16 => tokens.extend_from_slice(
1038                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH)
1039                    .unwrap()
1040                    .0
1041                    .as_slice(),
1042            ),
1043            Encoding::EIP712 => tokens.extend_from_slice(
1044                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_WITHDRAW_TYPE_HASH)
1045                    .unwrap()
1046                    .0
1047                    .as_slice(),
1048            ),
1049        }
1050
1051        tokens.extend_from_slice(fuel_u256_to_bytes(&self.nonce).as_slice());
1052        match self.to {
1053            Identity::Address(bits) => tokens.extend_from_slice(bits.as_slice()),
1054            Identity::ContractId(bits) => tokens.extend_from_slice(bits.as_slice()),
1055        }
1056        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
1057        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
1058        tokens.extend_from_slice(0u64.to_be_bytes().as_slice());
1059        tokens.extend_from_slice(self.amount.to_be_bytes().as_slice());
1060        tokens.extend_from_slice(self.asset_id.as_slice());
1061
1062        keccak256(tokens)
1063    }
1064}
1065
1066pub const SRC_16_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH: &str =
1067    "0xfed97554285faeaf02128aa961f1b12bc144175e0bc3095deee2999f9c0563c1";
1068
1069pub const EIP_712_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH: &str =
1070    "0x936febfd2859fb63ad8d03cb75f762221befa5f66b3ed7a2716039b602fdc944";
1071
1072impl SRC16Encode for ParallelCallContractArgs {
1073    fn type_hash(encoding: Encoding) -> [u8; 32] {
1074        match encoding {
1075            Encoding::SRC16 => {
1076                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH)
1077                    .unwrap()
1078                    .0
1079            }
1080            Encoding::EIP712 => {
1081                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH)
1082                    .unwrap()
1083                    .0
1084            }
1085        }
1086    }
1087
1088    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
1089        let mut tokens: Vec<u8> = Vec::new();
1090        match encoding {
1091            Encoding::SRC16 => tokens.extend_from_slice(
1092                Bits256::from_hex_str(SRC_16_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH)
1093                    .unwrap()
1094                    .0
1095                    .as_slice(),
1096            ),
1097            Encoding::EIP712 => tokens.extend_from_slice(
1098                Bits256::from_hex_str(EIP_712_PARALLEL_SIGNED_CALL_CONTRACT_TYPE_HASH)
1099                    .unwrap()
1100                    .0
1101                    .as_slice(),
1102            ),
1103        }
1104
1105        tokens.extend_from_slice(fuel_u256_to_bytes(&self.nonce).as_slice());
1106        tokens
1107            .extend_from_slice(self.call_contract_args.struct_hash(encoding).as_slice());
1108
1109        keccak256(tokens)
1110    }
1111}
1112
1113pub const SRC_16_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH: &str =
1114    "0xb913eb232c21ef9250e65abc3c1cb9f75af53ed044455ea484d4f3fa3b4b0ee5";
1115
1116pub const EIP_712_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH: &str =
1117    "0x3c5f4ecf0e01c024666fd99c1a6b05d93d7ca2ca31879f4d66d008356c8dcbf2";
1118
1119impl SRC16Encode for ParallelMultiCallContractArgs {
1120    fn type_hash(encoding: Encoding) -> [u8; 32] {
1121        match encoding {
1122            Encoding::SRC16 => {
1123                Bits256::from_hex_str(
1124                    SRC_16_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH,
1125                )
1126                .unwrap()
1127                .0
1128            }
1129            Encoding::EIP712 => {
1130                Bits256::from_hex_str(
1131                    EIP_712_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH,
1132                )
1133                .unwrap()
1134                .0
1135            }
1136        }
1137    }
1138
1139    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
1140        let mut tokens: Vec<u8> = Vec::new();
1141        match encoding {
1142            Encoding::SRC16 => tokens.extend_from_slice(
1143                Bits256::from_hex_str(
1144                    SRC_16_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH,
1145                )
1146                .unwrap()
1147                .0
1148                .as_slice(),
1149            ),
1150            Encoding::EIP712 => tokens.extend_from_slice(
1151                Bits256::from_hex_str(
1152                    EIP_712_PARALLEL_SIGNED_MULTI_CALL_CONTRACT_TYPE_HASH,
1153                )
1154                .unwrap()
1155                .0
1156                .as_slice(),
1157            ),
1158        }
1159
1160        tokens.extend_from_slice(fuel_u256_to_bytes(&self.nonce).as_slice());
1161
1162        let mut call_buffer: Vec<u8> = Vec::new();
1163        for call in self.call_contract_args {
1164            call_buffer.extend_from_slice(call.struct_hash(encoding.clone()).as_slice());
1165        }
1166        tokens.extend_from_slice(keccak256(call_buffer).as_slice());
1167
1168        keccak256(tokens)
1169    }
1170}
1171
1172pub const SRC_16_PARALLEL_REVOKE_TYPE_HASH: &str =
1173    "0xac7c930989abf28d71694ae7f327a130cdc1ef80040d94c6873cf0686024382d";
1174
1175pub const EIP_712_PARALLEL_REVOKE_TYPE_HASH: &str =
1176    "0x51c1376821126b4be9647fdbbf51877dd5a72c6a76e58fb957f37ac985962ea8";
1177
1178impl SRC16Encode for ParallelRevokeArgs {
1179    fn type_hash(encoding: Encoding) -> [u8; 32] {
1180        match encoding {
1181            Encoding::SRC16 => {
1182                Bits256::from_hex_str(SRC_16_PARALLEL_REVOKE_TYPE_HASH)
1183                    .unwrap()
1184                    .0
1185            }
1186            Encoding::EIP712 => {
1187                Bits256::from_hex_str(EIP_712_PARALLEL_REVOKE_TYPE_HASH)
1188                    .unwrap()
1189                    .0
1190            }
1191        }
1192    }
1193
1194    fn struct_hash(self, encoding: Encoding) -> [u8; 32] {
1195        let mut tokens: Vec<u8> = Vec::new();
1196        match encoding {
1197            Encoding::SRC16 => tokens.extend_from_slice(
1198                Bits256::from_hex_str(SRC_16_PARALLEL_REVOKE_TYPE_HASH)
1199                    .unwrap()
1200                    .0
1201                    .as_slice(),
1202            ),
1203            Encoding::EIP712 => tokens.extend_from_slice(
1204                Bits256::from_hex_str(EIP_712_PARALLEL_REVOKE_TYPE_HASH)
1205                    .unwrap()
1206                    .0
1207                    .as_slice(),
1208            ),
1209        }
1210
1211        tokens.extend_from_slice(fuel_u256_to_bytes(&self.nonce).as_slice());
1212
1213        keccak256(tokens)
1214    }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219    use super::*;
1220    use fuels::types::ContractId;
1221
1222    /// Regression for the SRC16Domain `chain_id` encoding mismatch
1223    /// against the Sway contract.
1224    ///
1225    /// The Sway contract's `_get_domain_separator` constructs a
1226    /// `SRC16Domain` with `chain_id().as_u256()` and copies that
1227    /// u256 straight from VM memory into the domain preimage via
1228    /// `mcp` (32 raw bytes). Fuel VM stores u256 in big-endian word
1229    /// order — the MOST-significant u64 limb at the lowest memory
1230    /// address, the LEAST-significant at the highest. So a u256
1231    /// value `N` lives in memory as `[0×24 || N.to_be_bytes(8)]`
1232    /// for any `N` that fits in u64.
1233    ///
1234    /// `fuels::U256` (re-exporting `ethers-core::U256`) stores its
1235    /// 4 u64 limbs in the OPPOSITE order: `0[0]` is the
1236    /// least-significant. The previous implementation iterated
1237    /// `chain_id.0.iter().flat_map(|x| x.to_be_bytes())`, writing
1238    /// the LSU at byte offset 0 — disagreeing with the contract on
1239    /// every non-zero chain id.
1240    ///
1241    /// The bug was invisible for two reasons:
1242    ///   1. Every test environment (this crate's tests, the
1243    ///      `trade-account` integration tests, and `fake-o2`) uses
1244    ///      `chain_id = 0`. All four limbs are zero, so
1245    ///      MSU-first and LSU-first byte encodings are identical.
1246    ///   2. EIP712Domain encodes chain_id either in the salt
1247    ///      (`[0×24 || chain_id.to_be_bytes()]`) or via
1248    ///      `ethers-core`'s domain logic — both unambiguous and
1249    ///      already matched the contract.
1250    ///
1251    /// This regression pins the corrected MSU-first encoding by
1252    /// computing the separator two independent ways and asserting
1253    /// equality for a real mainnet chain_id (9889).
1254    #[test]
1255    fn src16_domain_separator_encodes_chain_id_msu_first_big_endian() {
1256        let chain_id: u64 = 9889;
1257        let name = "TradeAccount";
1258        let version = "2";
1259        let mut vc_bytes = [0u8; 32];
1260        for (i, chunk) in
1261            "9423658cc5c65c586208bedd70f1cc11af5120858df6c6ba128b6ac4e1e6a9d6"
1262                .as_bytes()
1263                .chunks(2)
1264                .enumerate()
1265        {
1266            vc_bytes[i] =
1267                u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap();
1268        }
1269        let verifying_contract = ContractId::from(vc_bytes);
1270
1271        let domain = SRC16Domain {
1272            name: Some(name.to_string()),
1273            version: Some(version.to_string()),
1274            chain_id: Some(FuelU256::from(chain_id)),
1275            verifying_contract: Some(verifying_contract),
1276            salt: None,
1277        };
1278        let actual = domain.separator();
1279
1280        // Manual computation with the canonical big-endian
1281        // chain_id placement — matches what the Sway contract
1282        // reads from VM memory via `mcp`.
1283        let type_hash_str = "SRC16Domain(string name,string version,u256 chain_id,contractId verifying_contract)";
1284        let type_hash = keccak256(type_hash_str.as_bytes());
1285
1286        let mut expected_preimage = Vec::new();
1287        expected_preimage.extend_from_slice(&type_hash);
1288        expected_preimage.extend_from_slice(&keccak256(name.as_bytes()));
1289        expected_preimage.extend_from_slice(&keccak256(version.as_bytes()));
1290        // chain_id as 32-byte big-endian — high zero limbs first,
1291        // little limb last.
1292        let mut chain_id_be = [0u8; 32];
1293        chain_id_be[24..].copy_from_slice(&chain_id.to_be_bytes());
1294        expected_preimage.extend_from_slice(&chain_id_be);
1295        expected_preimage.extend_from_slice(verifying_contract.as_slice());
1296
1297        let expected = keccak256(&expected_preimage);
1298
1299        assert_eq!(
1300            actual,
1301            expected,
1302            "SRC16Domain::separator() must place chain_id at byte offset 24 \
1303             (big-endian) to match the Sway contract's VM-memory mcp read; \
1304             got {} expected {}",
1305            hex_of(&actual),
1306            hex_of(&expected),
1307        );
1308    }
1309
1310    /// Cross-check: the zero-chain-id case (which is what every
1311    /// pre-existing test exercises) still works after the fix.
1312    /// LSU-first and MSU-first encodings produce the same 32 zero
1313    /// bytes here, so any contract that signed against this domain
1314    /// before the fix is byte-compatible with the corrected encoding.
1315    #[test]
1316    fn src16_domain_separator_handles_zero_chain_id_unchanged() {
1317        let domain = SRC16Domain {
1318            name: Some("TradeAccount".to_string()),
1319            version: Some("2".to_string()),
1320            chain_id: Some(FuelU256::from(0u64)),
1321            verifying_contract: Some(ContractId::from([0u8; 32])),
1322            salt: None,
1323        };
1324        // Just check it runs; the actual hash depends on every
1325        // field so a hardcoded value would be brittle. The point
1326        // is: no regression for the zero-chain-id baseline.
1327        let _ = domain.separator();
1328    }
1329
1330    fn hex_of(bytes: &[u8]) -> String {
1331        bytes.iter().map(|b| format!("{:02x}", b)).collect()
1332    }
1333}