Skip to main content

cow_composable/
utils.rs

1//! Utility helpers for composable (conditional) orders.
2//!
3//! Mirrors `utils.ts` from the `@cowprotocol/composable` `TypeScript` SDK:
4//! address checks, EIP-712 hash-to-string reversal, ABI validation, and
5//! conversion from raw on-chain structs to typed orders.
6
7use alloy_primitives::{Address, B256, U256, keccak256};
8use cow_chains::contracts::EXTENSIBLE_FALLBACK_HANDLER;
9use cow_errors::CowError;
10use cow_signing::types::UnsignedOrder;
11use cow_types::{OrderKind, TokenBalance};
12
13use super::types::{
14    BlockInfo, COMPOSABLE_COW_ADDRESS, ConditionalOrderParams, GpV2OrderStruct, IsValidResult,
15};
16
17/// Returns `true` if `address` is the canonical `ComposableCow` factory address.
18///
19/// Mirrors `isComposableCow` from the `TypeScript` SDK.
20///
21/// # Example
22///
23/// ```rust
24/// use cow_composable::{COMPOSABLE_COW_ADDRESS, is_composable_cow};
25///
26/// assert!(is_composable_cow(COMPOSABLE_COW_ADDRESS));
27/// assert!(!is_composable_cow(alloy_primitives::Address::ZERO));
28/// ```
29#[must_use]
30pub fn is_composable_cow(address: Address) -> bool {
31    address == COMPOSABLE_COW_ADDRESS
32}
33
34/// Returns `true` if `address` is the canonical `ExtensibleFallbackHandler` contract.
35///
36/// Used to verify whether a `Safe` wallet has the correct fallback handler installed
37/// to support `ComposableCow`-based conditional orders via EIP-712 domain verifiers.
38///
39/// # Example
40///
41/// ```rust
42/// use cow_chains::contracts::EXTENSIBLE_FALLBACK_HANDLER;
43/// use cow_composable::is_extensible_fallback_handler;
44///
45/// assert!(is_extensible_fallback_handler(EXTENSIBLE_FALLBACK_HANDLER));
46/// assert!(!is_extensible_fallback_handler(alloy_primitives::Address::ZERO));
47/// ```
48#[must_use]
49pub fn is_extensible_fallback_handler(address: Address) -> bool {
50    address == EXTENSIBLE_FALLBACK_HANDLER
51}
52
53/// Reverse-map a `keccak256`-hashed token-balance string back to its name.
54///
55/// Returns `Some("erc20")`, `Some("external")`, or `Some("internal")` if `hash`
56/// matches a known [`TokenBalance`] EIP-712 hash; `None` otherwise.
57///
58/// Mirrors `balanceToString` from the `TypeScript` SDK.
59///
60/// # Example
61///
62/// ```rust
63/// use cow_composable::balance_to_string;
64/// use cow_types::TokenBalance;
65///
66/// let h = TokenBalance::Erc20.eip712_hash();
67/// assert_eq!(balance_to_string(h), Some("erc20"));
68/// assert_eq!(balance_to_string(alloy_primitives::B256::ZERO), None);
69/// ```
70#[must_use]
71pub fn balance_to_string(hash: B256) -> Option<&'static str> {
72    if hash == TokenBalance::Erc20.eip712_hash() {
73        Some("erc20")
74    } else if hash == TokenBalance::External.eip712_hash() {
75        Some("external")
76    } else if hash == TokenBalance::Internal.eip712_hash() {
77        Some("internal")
78    } else {
79        None
80    }
81}
82
83/// Reverse-map a `keccak256`-hashed order-kind string back to its name.
84///
85/// Returns `Some("sell")` or `Some("buy")` if `hash` matches a known
86/// [`OrderKind`] EIP-712 hash; `None` otherwise.
87///
88/// Mirrors `kindToString` from the `TypeScript` SDK.
89///
90/// # Example
91///
92/// ```rust
93/// use cow_composable::kind_to_string;
94///
95/// let sell_hash = alloy_primitives::keccak256(b"sell");
96/// assert_eq!(kind_to_string(sell_hash), Some("sell"));
97///
98/// let buy_hash = alloy_primitives::keccak256(b"buy");
99/// assert_eq!(kind_to_string(buy_hash), Some("buy"));
100///
101/// assert_eq!(kind_to_string(alloy_primitives::B256::ZERO), None);
102/// ```
103#[must_use]
104pub fn kind_to_string(hash: B256) -> Option<&'static str> {
105    if hash == keccak256(b"sell" as &[u8]) {
106        Some("sell")
107    } else if hash == keccak256(b"buy" as &[u8]) {
108        Some("buy")
109    } else {
110        None
111    }
112}
113
114/// Decode a raw on-chain [`GpV2OrderStruct`] into a typed [`UnsignedOrder`].
115///
116/// The `kind`, `sell_token_balance`, and `buy_token_balance` fields in
117/// [`GpV2OrderStruct`] are `keccak256` hashes; this function reverses them
118/// via [`kind_to_string`] and [`balance_to_string`].
119///
120/// Mirrors `fromStructToOrder` from the `@cowprotocol/composable` SDK.
121///
122/// # Errors
123///
124/// Returns [`CowError::AppData`] if any hash cannot be decoded to a known variant.
125///
126/// # Example
127///
128/// ```
129/// use alloy_primitives::{Address, B256, U256, keccak256};
130/// use cow_composable::{GpV2OrderStruct, from_struct_to_order};
131/// use cow_types::OrderKind;
132///
133/// let s = GpV2OrderStruct {
134///     sell_token: Address::ZERO,
135///     buy_token: Address::ZERO,
136///     receiver: Address::ZERO,
137///     sell_amount: U256::ZERO,
138///     buy_amount: U256::ZERO,
139///     valid_to: 0,
140///     app_data: B256::ZERO,
141///     fee_amount: U256::ZERO,
142///     kind: keccak256(b"sell"),
143///     partially_fillable: false,
144///     sell_token_balance: keccak256(b"erc20"),
145///     buy_token_balance: keccak256(b"erc20"),
146/// };
147/// let order = from_struct_to_order(&s).unwrap();
148/// assert_eq!(order.kind, OrderKind::Sell);
149/// ```
150pub fn from_struct_to_order(s: &GpV2OrderStruct) -> Result<UnsignedOrder, CowError> {
151    let kind_str = kind_to_string(s.kind)
152        .ok_or_else(|| CowError::AppData(format!("unknown order kind hash: {}", s.kind)))?;
153    let kind = if kind_str == "sell" { OrderKind::Sell } else { OrderKind::Buy };
154    let sell_token_balance = decode_token_balance(s.sell_token_balance)?;
155    let buy_token_balance = decode_token_balance(s.buy_token_balance)?;
156    Ok(UnsignedOrder {
157        sell_token: s.sell_token,
158        buy_token: s.buy_token,
159        receiver: s.receiver,
160        sell_amount: s.sell_amount,
161        buy_amount: s.buy_amount,
162        valid_to: s.valid_to,
163        app_data: s.app_data,
164        fee_amount: s.fee_amount,
165        kind,
166        partially_fillable: s.partially_fillable,
167        sell_token_balance,
168        buy_token_balance,
169    })
170}
171
172/// Decode a `keccak256`-hashed token-balance string to a [`TokenBalance`] variant.
173fn decode_token_balance(hash: B256) -> Result<TokenBalance, CowError> {
174    match balance_to_string(hash) {
175        Some("erc20") => Ok(TokenBalance::Erc20),
176        Some("external") => Ok(TokenBalance::External),
177        Some("internal") => Ok(TokenBalance::Internal),
178        _ => Err(CowError::AppData(format!("unknown token-balance hash: {hash}"))),
179    }
180}
181
182/// Default token formatter: produces `"{amount}@{address}"`.
183///
184/// Mirrors `DEFAULT_TOKEN_FORMATTER` from the `TypeScript` SDK.
185///
186/// # Example
187///
188/// ```rust
189/// use alloy_primitives::{Address, U256};
190/// use cow_composable::default_token_formatter;
191///
192/// let s = default_token_formatter(Address::ZERO, U256::from(42u64));
193/// assert_eq!(s, "42@0x0000000000000000000000000000000000000000");
194/// ```
195#[must_use]
196pub fn default_token_formatter(address: Address, amount: U256) -> String {
197    format!("{amount}@{address}")
198}
199
200/// Check whether an [`IsValidResult`] represents a valid state.
201///
202/// Returns `true` if the result is `Valid`, `false` if `Invalid`.
203///
204/// This is the Rust equivalent of `getIsValidResult` in the `TypeScript` SDK.
205///
206/// # Example
207///
208/// ```rust
209/// use cow_composable::{IsValidResult, get_is_valid_result};
210///
211/// let valid = IsValidResult::Valid;
212/// assert!(get_is_valid_result(&valid));
213///
214/// let invalid = IsValidResult::Invalid { reason: "expired".to_owned() };
215/// assert!(!get_is_valid_result(&invalid));
216/// ```
217#[must_use]
218pub const fn get_is_valid_result(result: &IsValidResult) -> bool {
219    matches!(result, IsValidResult::Valid)
220}
221
222/// Transform raw contract data bytes into a [`ConditionalOrderParams`] struct.
223///
224/// Decodes the ABI-encoded bytes (handler + salt + staticInput) into the
225/// structured [`ConditionalOrderParams`] type.
226///
227/// This is the Rust equivalent of `transformDataToStruct` in the `TypeScript` SDK.
228///
229/// # Errors
230///
231/// Returns [`CowError::AppData`] if the data is too short or malformed.
232pub fn transform_data_to_struct(data: &[u8]) -> Result<ConditionalOrderParams, CowError> {
233    // ABI layout: handler(32) + salt(32) + offset(32) + length(32) + data...
234    if data.len() < 128 {
235        return Err(CowError::AppData("data too short for ConditionalOrderParams".into()));
236    }
237    let handler = Address::from_slice(&data[12..32]);
238    let salt = B256::from_slice(&data[32..64]);
239    let data_offset = usize::try_from(U256::from_be_slice(&data[64..96]))
240        .map_err(|e| CowError::AppData(format!("invalid offset: {e}")))?;
241    let data_len = usize::try_from(U256::from_be_slice(&data[data_offset..data_offset + 32]))
242        .map_err(|e| CowError::AppData(format!("invalid data length: {e}")))?;
243    let static_input_start = data_offset + 32;
244    let static_input = data[static_input_start..static_input_start + data_len].to_vec();
245    Ok(ConditionalOrderParams { handler, salt, static_input })
246}
247
248/// Transform a [`ConditionalOrderParams`] struct back into ABI-encoded hex.
249///
250/// Produces the same `0x`-prefixed hex encoding that the `ComposableCow` contract expects.
251///
252/// This is the Rust equivalent of `transformStructToData` in the `TypeScript` SDK.
253#[must_use]
254pub fn transform_struct_to_data(params: &ConditionalOrderParams) -> String {
255    super::twap::encode_params(params)
256}
257
258/// Encode a `setDomainVerifier(bytes32 domain, address verifier)` call.
259///
260/// Returns the ABI-encoded calldata for calling `setDomainVerifier` on the
261/// `ExtensibleFallbackHandler` contract.
262///
263/// This is the Rust equivalent of `createSetDomainVerifierTx` in the `TypeScript` SDK.
264///
265/// # Example
266///
267/// ```rust
268/// use alloy_primitives::{Address, B256};
269/// use cow_composable::create_set_domain_verifier_tx;
270///
271/// let domain = B256::ZERO;
272/// let verifier = Address::ZERO;
273/// let calldata = create_set_domain_verifier_tx(domain, verifier);
274/// assert!(!calldata.is_empty());
275/// ```
276#[must_use]
277pub fn create_set_domain_verifier_tx(domain: B256, verifier: Address) -> Vec<u8> {
278    // function setDomainVerifier(bytes32, address)
279    // selector = keccak256("setDomainVerifier(bytes32,address)")[..4]
280    let selector = &keccak256(b"setDomainVerifier(bytes32,address)" as &[u8])[..4];
281    let mut calldata = Vec::with_capacity(4 + 64);
282    calldata.extend_from_slice(selector);
283    calldata.extend_from_slice(domain.as_slice());
284    // Address is left-padded to 32 bytes
285    calldata.extend_from_slice(&[0u8; 12]);
286    calldata.extend_from_slice(verifier.as_slice());
287    calldata
288}
289
290/// Get block information (number and timestamp) for constructing conditional orders.
291///
292/// This is the Rust equivalent of `getBlockInfo` in the `TypeScript` SDK.
293/// In the `TypeScript` SDK this makes an RPC call; here it is a simple constructor.
294///
295/// # Example
296///
297/// ```rust
298/// use cow_composable::get_block_info;
299///
300/// let info = get_block_info(12345, 1_700_000_000);
301/// assert_eq!(info.block_number, 12345);
302/// assert_eq!(info.block_timestamp, 1_700_000_000);
303/// ```
304#[must_use]
305pub const fn get_block_info(block_number: u64, block_timestamp: u64) -> BlockInfo {
306    BlockInfo { block_number, block_timestamp }
307}
308
309/// Get the domain verifier address for a Safe from the `ExtensibleFallbackHandler`.
310///
311/// Returns the ABI-encoded calldata for the `domainVerifiers(address,bytes32)` view call.
312/// In the `TypeScript` SDK this makes an on-chain read; this Rust version returns
313/// the calldata so callers can execute the call via their preferred provider.
314///
315/// # Example
316///
317/// ```rust
318/// use alloy_primitives::{Address, B256};
319/// use cow_composable::get_domain_verifier_calldata;
320///
321/// let safe = Address::ZERO;
322/// let domain = B256::ZERO;
323/// let calldata = get_domain_verifier_calldata(safe, domain);
324/// assert_eq!(calldata.len(), 4 + 64);
325/// ```
326#[must_use]
327pub fn get_domain_verifier_calldata(safe: Address, domain: B256) -> Vec<u8> {
328    // function domainVerifiers(address, bytes32) view returns (address)
329    let selector = &keccak256(b"domainVerifiers(address,bytes32)" as &[u8])[..4];
330    let mut calldata = Vec::with_capacity(4 + 64);
331    calldata.extend_from_slice(selector);
332    // Address is left-padded to 32 bytes
333    calldata.extend_from_slice(&[0u8; 12]);
334    calldata.extend_from_slice(safe.as_slice());
335    calldata.extend_from_slice(domain.as_slice());
336    calldata
337}
338
339/// Alias for [`get_domain_verifier_calldata`].
340///
341/// Matches the `getDomainVerifier` name from the `TypeScript` `composable` package.
342/// Returns ABI-encoded calldata for the `domainVerifiers(address,bytes32)` view call
343/// on the `ExtensibleFallbackHandler` contract.
344///
345/// # Example
346///
347/// ```rust
348/// use alloy_primitives::{Address, B256};
349/// use cow_composable::get_domain_verifier;
350///
351/// let calldata = get_domain_verifier(Address::ZERO, B256::ZERO);
352/// assert_eq!(calldata.len(), 4 + 64);
353/// ```
354#[must_use]
355pub fn get_domain_verifier(safe: Address, domain: B256) -> Vec<u8> {
356    get_domain_verifier_calldata(safe, domain)
357}
358
359/// Returns `true` if `hex` is a plausibly valid ABI-encoded
360/// [`ConditionalOrderParams`].
361///
362/// Checks that the hex decodes to at least 128 bytes (handler word + salt +
363/// offset word + length word) and that the declared `static_input` length fits
364/// within the buffer. This does **not** attempt to decode or validate the
365/// handler address or static input contents.
366///
367/// Mirrors `isValidAbi` from the `TypeScript` SDK.
368///
369/// # Example
370///
371/// ```rust
372/// use alloy_primitives::{Address, B256};
373/// use cow_composable::{ConditionalOrderParams, encode_params, is_valid_abi};
374///
375/// let params =
376///     ConditionalOrderParams { handler: Address::ZERO, salt: B256::ZERO, static_input: vec![] };
377/// assert!(is_valid_abi(&encode_params(&params)));
378/// assert!(!is_valid_abi("0xdeadbeef")); // too short
379/// ```
380#[must_use]
381pub fn is_valid_abi(hex: &str) -> bool {
382    let stripped = hex.trim_start_matches("0x");
383    let Ok(bytes) = alloy_primitives::hex::decode(stripped) else {
384        return false;
385    };
386    // Minimum: handler(32) + salt(32) + offset(32) + length(32) = 128 bytes
387    if bytes.len() < 128 {
388        return false;
389    }
390    let data_len = usize::try_from(U256::from_be_slice(&bytes[96..128]));
391    let data_len_usize = data_len.map_or(usize::MAX, |v| v);
392    let min_total = 128usize.saturating_add(data_len_usize);
393    bytes.len() >= min_total
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    // ── is_composable_cow ────────────────────────────────────────────────
401
402    #[test]
403    fn is_composable_cow_with_correct_address() {
404        assert!(is_composable_cow(COMPOSABLE_COW_ADDRESS));
405    }
406
407    #[test]
408    fn is_composable_cow_with_zero_address() {
409        assert!(!is_composable_cow(Address::ZERO));
410    }
411
412    #[test]
413    fn is_composable_cow_with_random_address() {
414        let addr = Address::new([0xAB; 20]);
415        assert!(!is_composable_cow(addr));
416    }
417
418    // ── is_extensible_fallback_handler ───────────────────────────────────
419
420    #[test]
421    fn is_extensible_fallback_handler_with_correct_address() {
422        assert!(is_extensible_fallback_handler(EXTENSIBLE_FALLBACK_HANDLER));
423    }
424
425    #[test]
426    fn is_extensible_fallback_handler_with_zero_address() {
427        assert!(!is_extensible_fallback_handler(Address::ZERO));
428    }
429
430    #[test]
431    fn is_extensible_fallback_handler_with_random_address() {
432        let addr = Address::new([0x12; 20]);
433        assert!(!is_extensible_fallback_handler(addr));
434    }
435
436    // ── balance_to_string ────────────────────────────────────────────────
437
438    #[test]
439    fn balance_to_string_erc20() {
440        let hash = TokenBalance::Erc20.eip712_hash();
441        assert_eq!(balance_to_string(hash), Some("erc20"));
442    }
443
444    #[test]
445    fn balance_to_string_external() {
446        let hash = TokenBalance::External.eip712_hash();
447        assert_eq!(balance_to_string(hash), Some("external"));
448    }
449
450    #[test]
451    fn balance_to_string_internal() {
452        let hash = TokenBalance::Internal.eip712_hash();
453        assert_eq!(balance_to_string(hash), Some("internal"));
454    }
455
456    #[test]
457    fn balance_to_string_unknown() {
458        assert_eq!(balance_to_string(B256::ZERO), None);
459    }
460
461    // ── kind_to_string ───────────────────────────────────────────────────
462
463    #[test]
464    fn kind_to_string_sell() {
465        let hash = keccak256(b"sell");
466        assert_eq!(kind_to_string(hash), Some("sell"));
467    }
468
469    #[test]
470    fn kind_to_string_buy() {
471        let hash = keccak256(b"buy");
472        assert_eq!(kind_to_string(hash), Some("buy"));
473    }
474
475    #[test]
476    fn kind_to_string_unknown() {
477        assert_eq!(kind_to_string(B256::ZERO), None);
478        assert_eq!(kind_to_string(keccak256(b"limit")), None);
479    }
480
481    // ── from_struct_to_order ─────────────────────────────────────────────
482
483    fn make_gpv2_struct(kind: &[u8], sell_bal: &[u8], buy_bal: &[u8]) -> GpV2OrderStruct {
484        GpV2OrderStruct {
485            sell_token: Address::new([0x11; 20]),
486            buy_token: Address::new([0x22; 20]),
487            receiver: Address::new([0x33; 20]),
488            sell_amount: U256::from(1000u64),
489            buy_amount: U256::from(500u64),
490            valid_to: 1_700_000_000,
491            app_data: B256::ZERO,
492            fee_amount: U256::from(10u64),
493            kind: keccak256(kind),
494            partially_fillable: false,
495            sell_token_balance: keccak256(sell_bal),
496            buy_token_balance: keccak256(buy_bal),
497        }
498    }
499
500    #[test]
501    fn from_struct_to_order_sell_erc20() {
502        let s = make_gpv2_struct(b"sell", b"erc20", b"erc20");
503        let order = from_struct_to_order(&s).unwrap();
504        assert_eq!(order.kind, OrderKind::Sell);
505        assert_eq!(order.sell_token_balance, TokenBalance::Erc20);
506        assert_eq!(order.buy_token_balance, TokenBalance::Erc20);
507        assert_eq!(order.sell_token, s.sell_token);
508        assert_eq!(order.buy_token, s.buy_token);
509        assert_eq!(order.receiver, s.receiver);
510        assert_eq!(order.sell_amount, s.sell_amount);
511        assert_eq!(order.buy_amount, s.buy_amount);
512        assert_eq!(order.valid_to, s.valid_to);
513        assert_eq!(order.fee_amount, s.fee_amount);
514        assert!(!order.partially_fillable);
515    }
516
517    #[test]
518    fn from_struct_to_order_buy_external_internal() {
519        let s = make_gpv2_struct(b"buy", b"external", b"internal");
520        let order = from_struct_to_order(&s).unwrap();
521        assert_eq!(order.kind, OrderKind::Buy);
522        assert_eq!(order.sell_token_balance, TokenBalance::External);
523        assert_eq!(order.buy_token_balance, TokenBalance::Internal);
524    }
525
526    #[test]
527    fn from_struct_to_order_unknown_kind() {
528        let s = make_gpv2_struct(b"limit", b"erc20", b"erc20");
529        let err = from_struct_to_order(&s).unwrap_err();
530        assert!(err.to_string().contains("unknown order kind hash"));
531    }
532
533    #[test]
534    fn from_struct_to_order_unknown_sell_balance() {
535        let mut s = make_gpv2_struct(b"sell", b"erc20", b"erc20");
536        s.sell_token_balance = B256::ZERO;
537        let err = from_struct_to_order(&s).unwrap_err();
538        assert!(err.to_string().contains("unknown token-balance hash"));
539    }
540
541    #[test]
542    fn from_struct_to_order_unknown_buy_balance() {
543        let mut s = make_gpv2_struct(b"sell", b"erc20", b"erc20");
544        s.buy_token_balance = B256::ZERO;
545        let err = from_struct_to_order(&s).unwrap_err();
546        assert!(err.to_string().contains("unknown token-balance hash"));
547    }
548
549    // ── default_token_formatter ──────────────────────────────────────────
550
551    #[test]
552    fn default_token_formatter_basic() {
553        let addr = Address::ZERO;
554        let amount = U256::from(42u64);
555        let result = default_token_formatter(addr, amount);
556        assert_eq!(result, "42@0x0000000000000000000000000000000000000000");
557    }
558
559    #[test]
560    fn default_token_formatter_large_amount() {
561        let addr = Address::new([0xff; 20]);
562        let amount = U256::from(10u64).pow(U256::from(18u64));
563        let result = default_token_formatter(addr, amount);
564        assert!(result.starts_with("1000000000000000000@0x"));
565    }
566
567    // ── get_is_valid_result ──────────────────────────────────────────────
568
569    #[test]
570    fn get_is_valid_result_valid() {
571        assert!(get_is_valid_result(&IsValidResult::Valid));
572    }
573
574    #[test]
575    fn get_is_valid_result_invalid() {
576        let invalid = IsValidResult::Invalid { reason: "order expired".to_owned() };
577        assert!(!get_is_valid_result(&invalid));
578    }
579
580    #[test]
581    fn get_is_valid_result_invalid_empty_reason() {
582        let invalid = IsValidResult::Invalid { reason: String::new() };
583        assert!(!get_is_valid_result(&invalid));
584    }
585
586    // ── get_block_info ───────────────────────────────────────────────────
587
588    #[test]
589    fn get_block_info_basic() {
590        let info = get_block_info(12345, 1_700_000_000);
591        assert_eq!(info.block_number, 12345);
592        assert_eq!(info.block_timestamp, 1_700_000_000);
593    }
594
595    #[test]
596    fn get_block_info_zero() {
597        let info = get_block_info(0, 0);
598        assert_eq!(info.block_number, 0);
599        assert_eq!(info.block_timestamp, 0);
600    }
601
602    // ── transform_data_to_struct ─────────────────────────────────────────
603
604    /// Build a valid ABI-encoded blob for `ConditionalOrderParams`.
605    fn build_abi_encoded_params(handler: Address, salt: B256, static_input: &[u8]) -> Vec<u8> {
606        let mut data = Vec::new();
607        // handler (left-padded to 32 bytes)
608        data.extend_from_slice(&[0u8; 12]);
609        data.extend_from_slice(handler.as_slice());
610        // salt (32 bytes)
611        data.extend_from_slice(salt.as_slice());
612        // offset to dynamic data (points to byte 96)
613        let offset = U256::from(96u64);
614        data.extend_from_slice(&offset.to_be_bytes::<32>());
615        // length of static_input
616        let len = U256::from(static_input.len());
617        data.extend_from_slice(&len.to_be_bytes::<32>());
618        // static_input bytes
619        data.extend_from_slice(static_input);
620        data
621    }
622
623    #[test]
624    fn transform_data_to_struct_roundtrip() {
625        let handler = Address::new([0xAA; 20]);
626        let salt = B256::new([0xBB; 32]);
627        let static_input = vec![1u8, 2, 3, 4, 5];
628        let encoded = build_abi_encoded_params(handler, salt, &static_input);
629
630        let params = transform_data_to_struct(&encoded).unwrap();
631        assert_eq!(params.handler, handler);
632        assert_eq!(params.salt, salt);
633        assert_eq!(params.static_input, static_input);
634    }
635
636    #[test]
637    fn transform_data_to_struct_empty_static_input() {
638        let handler = Address::ZERO;
639        let salt = B256::ZERO;
640        let encoded = build_abi_encoded_params(handler, salt, &[]);
641
642        let params = transform_data_to_struct(&encoded).unwrap();
643        assert_eq!(params.handler, handler);
644        assert_eq!(params.salt, salt);
645        assert!(params.static_input.is_empty());
646    }
647
648    #[test]
649    fn transform_data_to_struct_too_short() {
650        let data = vec![0u8; 64];
651        let err = transform_data_to_struct(&data).unwrap_err();
652        assert!(err.to_string().contains("too short"));
653    }
654
655    // ── transform_struct_to_data ─────────────────────────────────────────
656
657    #[test]
658    fn transform_struct_to_data_produces_hex() {
659        let params = ConditionalOrderParams {
660            handler: Address::ZERO,
661            salt: B256::ZERO,
662            static_input: vec![],
663        };
664        let hex = transform_struct_to_data(&params);
665        assert!(hex.starts_with("0x"));
666        // Should be valid hex after the prefix
667        let stripped = hex.trim_start_matches("0x");
668        assert!(alloy_primitives::hex::decode(stripped).is_ok());
669    }
670
671    // ── is_valid_abi ─────────────────────────────────────────────────────
672
673    #[test]
674    fn is_valid_abi_with_valid_params() {
675        let params = ConditionalOrderParams {
676            handler: Address::ZERO,
677            salt: B256::ZERO,
678            static_input: vec![],
679        };
680        let hex = transform_struct_to_data(&params);
681        assert!(is_valid_abi(&hex));
682    }
683
684    #[test]
685    fn is_valid_abi_with_static_input() {
686        let params = ConditionalOrderParams {
687            handler: Address::new([0xAA; 20]),
688            salt: B256::new([0xBB; 32]),
689            static_input: vec![0xCC; 64],
690        };
691        let hex = transform_struct_to_data(&params);
692        assert!(is_valid_abi(&hex));
693    }
694
695    #[test]
696    fn is_valid_abi_too_short() {
697        assert!(!is_valid_abi("0xdeadbeef"));
698    }
699
700    #[test]
701    fn is_valid_abi_empty() {
702        assert!(!is_valid_abi(""));
703        assert!(!is_valid_abi("0x"));
704    }
705
706    #[test]
707    fn is_valid_abi_invalid_hex() {
708        assert!(!is_valid_abi("0xZZZZ"));
709    }
710
711    #[test]
712    fn is_valid_abi_without_0x_prefix() {
713        let params = ConditionalOrderParams {
714            handler: Address::ZERO,
715            salt: B256::ZERO,
716            static_input: vec![],
717        };
718        let hex = transform_struct_to_data(&params);
719        let stripped = hex.trim_start_matches("0x");
720        assert!(is_valid_abi(stripped));
721    }
722
723    // ── create_set_domain_verifier_tx ────────────────────────────────────
724
725    #[test]
726    fn create_set_domain_verifier_tx_length() {
727        let calldata = create_set_domain_verifier_tx(B256::ZERO, Address::ZERO);
728        // 4-byte selector + 32-byte domain + 32-byte address = 68 bytes
729        assert_eq!(calldata.len(), 68);
730    }
731
732    #[test]
733    fn create_set_domain_verifier_tx_selector() {
734        let calldata = create_set_domain_verifier_tx(B256::ZERO, Address::ZERO);
735        let expected_selector = &keccak256(b"setDomainVerifier(bytes32,address)")[..4];
736        assert_eq!(&calldata[..4], expected_selector);
737    }
738
739    #[test]
740    fn create_set_domain_verifier_tx_encodes_domain() {
741        let domain = B256::new([0xAA; 32]);
742        let calldata = create_set_domain_verifier_tx(domain, Address::ZERO);
743        assert_eq!(&calldata[4..36], domain.as_slice());
744    }
745
746    #[test]
747    fn create_set_domain_verifier_tx_encodes_verifier() {
748        let verifier = Address::new([0xBB; 20]);
749        let calldata = create_set_domain_verifier_tx(B256::ZERO, verifier);
750        // Address is left-padded with 12 zero bytes
751        assert_eq!(&calldata[36..48], &[0u8; 12]);
752        assert_eq!(&calldata[48..68], verifier.as_slice());
753    }
754
755    // ── get_domain_verifier_calldata ─────────────────────────────────────
756
757    #[test]
758    fn get_domain_verifier_calldata_length() {
759        let calldata = get_domain_verifier_calldata(Address::ZERO, B256::ZERO);
760        assert_eq!(calldata.len(), 68);
761    }
762
763    #[test]
764    fn get_domain_verifier_calldata_selector() {
765        let calldata = get_domain_verifier_calldata(Address::ZERO, B256::ZERO);
766        let expected_selector = &keccak256(b"domainVerifiers(address,bytes32)")[..4];
767        assert_eq!(&calldata[..4], expected_selector);
768    }
769
770    #[test]
771    fn get_domain_verifier_calldata_encodes_safe() {
772        let safe = Address::new([0xCC; 20]);
773        let calldata = get_domain_verifier_calldata(safe, B256::ZERO);
774        // Address is left-padded with 12 zero bytes
775        assert_eq!(&calldata[4..16], &[0u8; 12]);
776        assert_eq!(&calldata[16..36], safe.as_slice());
777    }
778
779    #[test]
780    fn get_domain_verifier_calldata_encodes_domain() {
781        let domain = B256::new([0xDD; 32]);
782        let calldata = get_domain_verifier_calldata(Address::ZERO, domain);
783        assert_eq!(&calldata[36..68], domain.as_slice());
784    }
785
786    // ── get_domain_verifier (alias) ──────────────────────────────────────
787
788    #[test]
789    fn get_domain_verifier_is_alias() {
790        let safe = Address::new([0x11; 20]);
791        let domain = B256::new([0x22; 32]);
792        assert_eq!(get_domain_verifier(safe, domain), get_domain_verifier_calldata(safe, domain));
793    }
794}