Skip to main content

cow_weiroll/
types.rs

1//! Weiroll script types, command encoding, and contract reference factories.
2//!
3//! This module defines the low-level types for Weiroll scripts:
4//!
5//! | Type | Purpose |
6//! |---|---|
7//! | [`WeirollCommand`] | A single 32-byte packed instruction |
8//! | [`WeirollScript`] | Finalised script (commands + state slots) |
9//! | [`WeirollCommandFlags`] | Call-type flags (`CALL`, `DELEGATECALL`, `STATICCALL`) |
10//! | [`WeirollContractRef`] | Contract address + ABI + default call flags |
11//!
12//! Factory functions:
13//!
14//! | Function | Creates |
15//! |---|---|
16//! | [`create_weiroll_contract`] | `CALL`-mode contract ref |
17//! | [`create_weiroll_library`] | `DELEGATECALL`-mode library ref |
18//! | [`create_weiroll_delegate_call`] | Full `execute(...)` [`EvmCall`] from a planner callback |
19
20use alloy_primitives::{Address, U256, address};
21
22use cow_chains::chains::EvmCall;
23
24/// Canonical Weiroll VM contract address.
25pub const WEIROLL_CONTRACT_ADDRESS: Address = address!("9585c3062Df1C247d5E373Cfca9167F7dC2b5963");
26
27/// A single command in a Weiroll script (32-byte packed encoding).
28///
29/// Each command encodes a contract call instruction that the Weiroll VM
30/// executes sequentially. The 32-byte packed layout is:
31///
32/// ```text
33/// ┌──────────┬───────┬────────┬────────────────┬──────────┬─────────┐
34/// │ flags(1) │ val(1)│ gas(2) │  target (20)   │ sel (4)  │ i/o (4) │
35/// └──────────┴───────┴────────┴────────────────┴──────────┴─────────┘
36/// ```
37///
38/// Use [`pack`](Self::pack) to serialise into the 32-byte wire format.
39#[derive(Debug, Clone)]
40pub struct WeirollCommand {
41    /// Command flags byte — see [`WeirollCommandFlags`] for the call-type
42    /// bits and modifier constants.
43    pub flags: u8,
44    /// Value byte — used with [`WeirollCommandFlags::CallWithValue`] to
45    /// index the state slot containing the ETH value to send.
46    pub value: u8,
47    /// Gas limit for this call (big-endian, 2 bytes). `0` means unlimited.
48    pub gas: u16,
49    /// Target contract address (20 bytes).
50    pub target: Address,
51    /// Solidity function selector (first 4 bytes of `keccak256(signature)`).
52    pub selector: [u8; 4],
53    /// Input/output slot mapping (4 bytes) — each nibble or byte indexes a
54    /// state slot that provides an argument or receives the return value.
55    pub in_out: [u8; 4],
56}
57
58impl WeirollCommand {
59    /// Pack this command into a 32-byte word for on-chain execution.
60    ///
61    /// Layout: `[flags(1)] [value(1)] [gas(2)] [target(20)] [selector(4)] [inout(4)]`
62    ///
63    /// # Returns
64    ///
65    /// A `[u8; 32]` containing the packed command.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use alloy_primitives::Address;
71    /// use cow_weiroll::WeirollCommand;
72    ///
73    /// let cmd = WeirollCommand {
74    ///     flags: 0x01,
75    ///     value: 0x00,
76    ///     gas: 21_000,
77    ///     target: Address::ZERO,
78    ///     selector: [0xde, 0xad, 0xbe, 0xef],
79    ///     in_out: [0x00; 4],
80    /// };
81    /// let packed = cmd.pack();
82    /// assert_eq!(packed.len(), 32);
83    /// assert_eq!(packed[0], 0x01); // flags
84    /// ```
85    #[must_use]
86    pub fn pack(&self) -> [u8; 32] {
87        let mut word = [0u8; 32];
88        word[0] = self.flags;
89        word[1] = self.value;
90        word[2..4].copy_from_slice(&self.gas.to_be_bytes());
91        word[4..24].copy_from_slice(self.target.as_slice());
92        word[24..28].copy_from_slice(&self.selector);
93        word[28..32].copy_from_slice(&self.in_out);
94        word
95    }
96
97    /// Return the flags byte.
98    ///
99    /// # Returns
100    ///
101    /// The raw `u8` flags value for this command.
102    #[must_use]
103    pub const fn flags_ref(&self) -> u8 {
104        self.flags
105    }
106
107    /// Return a reference to the target address.
108    ///
109    /// # Returns
110    ///
111    /// A reference to the target contract [`Address`].
112    #[must_use]
113    pub const fn target_ref(&self) -> &Address {
114        &self.target
115    }
116
117    /// Return a reference to the function selector.
118    ///
119    /// # Returns
120    ///
121    /// A reference to the 4-byte function selector.
122    #[must_use]
123    pub const fn selector_ref(&self) -> &[u8; 4] {
124        &self.selector
125    }
126}
127
128/// A complete Weiroll script ready for on-chain execution.
129///
130/// Produced by [`WeirollPlanner::plan`](super::WeirollPlanner::plan). The
131/// `commands` and `state` fields map directly to the two arguments of the
132/// Weiroll executor's `execute(bytes32[],bytes[])` function.
133///
134/// # Example
135///
136/// ```
137/// use cow_weiroll::WeirollPlanner;
138///
139/// let planner = WeirollPlanner::new();
140/// let script = planner.plan();
141/// assert!(script.is_empty());
142/// assert_eq!(script.command_count(), 0);
143/// assert_eq!(script.state_slot_count(), 0);
144/// ```
145#[derive(Debug, Clone)]
146pub struct WeirollScript {
147    /// Packed 32-byte command words (one per instruction).
148    pub commands: Vec<[u8; 32]>,
149    /// ABI-encoded state slots (arguments and return-value buffers).
150    pub state: Vec<Vec<u8>>,
151}
152
153impl WeirollScript {
154    /// Number of commands in this script.
155    ///
156    /// # Returns
157    ///
158    /// The length of the [`commands`](Self::commands) vector.
159    #[must_use]
160    pub const fn command_count(&self) -> usize {
161        self.commands.len()
162    }
163
164    /// Number of state slots in this script.
165    ///
166    /// # Returns
167    ///
168    /// The length of the [`state`](Self::state) vector.
169    #[must_use]
170    pub const fn state_slot_count(&self) -> usize {
171        self.state.len()
172    }
173
174    /// Returns `true` if the script contains no commands.
175    ///
176    /// An empty script is a no-op when executed on-chain.
177    ///
178    /// # Returns
179    ///
180    /// `true` when the [`commands`](Self::commands) vector is empty.
181    #[must_use]
182    pub const fn is_empty(&self) -> bool {
183        self.commands.is_empty()
184    }
185}
186
187/// Flags that modify the execution mode of a Weiroll command.
188///
189/// These correspond to the EVM opcodes used when the Weiroll executor
190/// invokes each command's target contract.  The call-type variants live in
191/// the lower 2 bits; combine them with the associated `CALLTYPE_MASK`,
192/// `EXTENDED_COMMAND`, and `TUPLE_RETURN` constants using bitwise
193/// operations.
194///
195/// # Example
196///
197/// ```
198/// use cow_weiroll::WeirollCommandFlags;
199///
200/// let flags = WeirollCommandFlags::Call;
201/// assert_eq!(flags as u8, 0x01);
202/// assert_eq!(flags as u8 & WeirollCommandFlags::CALLTYPE_MASK, WeirollCommandFlags::Call as u8,);
203/// ```
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
205#[repr(u8)]
206pub enum WeirollCommandFlags {
207    /// Execute via `DELEGATECALL` opcode (library calls).
208    DelegateCall = 0x00,
209    /// Execute via `CALL` opcode (standard external calls).
210    Call = 0x01,
211    /// Execute via `STATICCALL` opcode (read-only calls).
212    StaticCall = 0x02,
213    /// Execute via `CALL` with an explicit value transfer; the first
214    /// argument is interpreted as the ETH value to send.
215    CallWithValue = 0x03,
216}
217
218impl WeirollCommandFlags {
219    /// Bitmask that isolates the call-type bits from other flag bits.
220    pub const CALLTYPE_MASK: u8 = 0x03;
221    /// Marks an extended command that uses an additional 32-byte word
222    /// for argument slot indices (internal use).
223    pub const EXTENDED_COMMAND: u8 = 0x40;
224    /// Signals that the return value should be ABI-wrapped as `bytes`
225    /// so that multi-return functions can be captured (internal use).
226    pub const TUPLE_RETURN: u8 = 0x80;
227}
228
229/// The default Weiroll executor contract address deployed across supported
230/// chains.
231pub const WEIROLL_ADDRESS: &str = "0x9585c3062Df1C247d5E373Cfca9167F7dC2b5963";
232
233/// A Weiroll-compatible contract reference with a default call mode.
234///
235/// Pairs a contract address and its ABI with the [`WeirollCommandFlags`]
236/// that should be used when the Weiroll executor invokes this contract.
237/// Create via [`create_weiroll_contract`] (for `CALL`) or
238/// [`create_weiroll_library`] (for `DELEGATECALL`).
239///
240/// Mirrors `WeirollContract` from the `TypeScript` SDK.
241///
242/// # Example
243///
244/// ```
245/// use alloy_primitives::Address;
246/// use cow_weiroll::{WeirollCommandFlags, WeirollContractRef};
247///
248/// let contract = WeirollContractRef {
249///     address: Address::ZERO,
250///     abi: vec![],
251///     command_flags: WeirollCommandFlags::Call,
252/// };
253/// assert_eq!(contract.command_flags, WeirollCommandFlags::Call);
254/// ```
255#[derive(Debug, Clone)]
256pub struct WeirollContractRef {
257    /// The on-chain address of the contract.
258    pub address: Address,
259    /// The JSON-ABI of the contract (raw bytes or string representation).
260    pub abi: Vec<u8>,
261    /// The default call flags to apply when executing this contract's
262    /// functions through Weiroll.
263    pub command_flags: WeirollCommandFlags,
264}
265
266/// Create a [`WeirollContractRef`] for a standard `CALL` contract.
267///
268/// All function invocations through the returned reference will default to
269/// [`WeirollCommandFlags::Call`] unless overridden via the optional
270/// `command_flags` argument.
271///
272/// Mirrors `createWeirollContract` from the `TypeScript` SDK.
273///
274/// # Parameters
275///
276/// * `address` — the on-chain contract [`Address`].
277/// * `abi` — the contract's JSON-ABI as raw bytes (pass `vec![]` if not needed).
278/// * `command_flags` — optional override for the default call mode. When `None`, defaults to
279///   [`WeirollCommandFlags::Call`].
280///
281/// # Returns
282///
283/// A [`WeirollContractRef`] with the specified (or default) call flags.
284///
285/// # Example
286///
287/// ```
288/// use alloy_primitives::Address;
289/// use cow_weiroll::{WeirollCommandFlags, create_weiroll_contract};
290///
291/// let contract = create_weiroll_contract(Address::ZERO, vec![], None);
292/// assert_eq!(contract.command_flags, WeirollCommandFlags::Call);
293///
294/// let static_contract =
295///     create_weiroll_contract(Address::ZERO, vec![], Some(WeirollCommandFlags::StaticCall));
296/// assert_eq!(static_contract.command_flags, WeirollCommandFlags::StaticCall);
297/// ```
298#[must_use]
299pub fn create_weiroll_contract(
300    address: Address,
301    abi: Vec<u8>,
302    command_flags: Option<WeirollCommandFlags>,
303) -> WeirollContractRef {
304    WeirollContractRef {
305        address,
306        abi,
307        command_flags: command_flags.map_or(WeirollCommandFlags::Call, |v| v),
308    }
309}
310
311/// Create a [`WeirollContractRef`] for a Weiroll library
312/// (`DELEGATECALL`).
313///
314/// Library contracts are executed in the context of the Weiroll executor,
315/// so their storage writes affect the executor's state. This is the
316/// expected mode for helper libraries specifically written for Weiroll.
317///
318/// Mirrors `createWeirollLibrary` from the `TypeScript` SDK.
319///
320/// # Parameters
321///
322/// * `address` — the on-chain library [`Address`].
323/// * `abi` — the library's JSON-ABI as raw bytes.
324///
325/// # Returns
326///
327/// A [`WeirollContractRef`] with
328/// [`WeirollCommandFlags::DelegateCall`].
329///
330/// # Example
331///
332/// ```
333/// use alloy_primitives::Address;
334/// use cow_weiroll::{WeirollCommandFlags, create_weiroll_library};
335///
336/// let library = create_weiroll_library(Address::ZERO, vec![]);
337/// assert_eq!(library.command_flags, WeirollCommandFlags::DelegateCall);
338/// ```
339#[must_use]
340pub const fn create_weiroll_library(address: Address, abi: Vec<u8>) -> WeirollContractRef {
341    WeirollContractRef { address, abi, command_flags: WeirollCommandFlags::DelegateCall }
342}
343
344/// Build a Weiroll delegate-call [`EvmCall`] by running a planner
345/// callback.
346///
347/// Creates a fresh [`WeirollPlanner`](super::WeirollPlanner), passes it to
348/// the caller-supplied closure so commands and state slots can be added,
349/// then finalises the plan and ABI-encodes the resulting
350/// `execute(bytes32[],bytes[])` calldata targeting the canonical
351/// [`WEIROLL_CONTRACT_ADDRESS`].
352///
353/// Mirrors `createWeirollDelegateCall` from the `TypeScript` SDK.
354///
355/// # Parameters
356///
357/// * `add_to_planner` — a closure that receives `&mut WeirollPlanner` and populates it with
358///   commands and state slots.
359///
360/// # Returns
361///
362/// An [`EvmCall`] with `to` set to the Weiroll executor, `data` set to
363/// the ABI-encoded `execute(...)` calldata, and `value` set to zero.
364///
365/// # Example
366///
367/// ```
368/// use alloy_primitives::Address;
369/// use cow_weiroll::{WEIROLL_ADDRESS, WeirollCommand, create_weiroll_delegate_call};
370///
371/// let evm_call = create_weiroll_delegate_call(|planner| {
372///     planner.add_command(WeirollCommand {
373///         flags: 0,
374///         value: 0,
375///         gas: 0,
376///         target: Address::ZERO,
377///         selector: [0; 4],
378///         in_out: [0; 4],
379///     });
380/// });
381/// assert_eq!(evm_call.to, WEIROLL_ADDRESS.parse::<Address>().unwrap());
382/// assert_eq!(evm_call.value, alloy_primitives::U256::ZERO);
383/// ```
384///
385/// [`WEIROLL_ADDRESS`]: crate::types::WEIROLL_ADDRESS
386#[must_use]
387pub fn create_weiroll_delegate_call(
388    add_to_planner: impl FnOnce(&mut super::WeirollPlanner),
389) -> EvmCall {
390    let mut planner = super::WeirollPlanner::new();
391    add_to_planner(&mut planner);
392    let script = planner.plan();
393
394    let calldata = abi_encode_execute(&script.commands, &script.state);
395
396    EvmCall { to: WEIROLL_CONTRACT_ADDRESS, data: calldata, value: U256::ZERO }
397}
398
399/// ABI-encode an `execute(bytes32[], bytes[])` call.
400///
401/// Performs manual Solidity ABI encoding without requiring `alloy-sol-types`.
402fn abi_encode_execute(commands: &[[u8; 32]], state: &[Vec<u8>]) -> Vec<u8> {
403    // Function selector: keccak256("execute(bytes32[],bytes[])") = 0xde792be1
404    let selector: [u8; 4] = [0xde, 0x79, 0x2b, 0xe1];
405
406    // Head: selector + offset(commands) + offset(state)
407    // commands array starts at offset 64 (2 * 32)
408    // state array starts after commands: 64 + 32 + commands.len() * 32
409    let commands_offset: usize = 64;
410    let commands_data_len = 32 + commands.len() * 32; // length + elements
411    let state_offset: usize = commands_offset + commands_data_len;
412
413    let mut buf = Vec::with_capacity(4 + state_offset + 256);
414    buf.extend_from_slice(&selector);
415
416    // Offset to commands array
417    buf.extend_from_slice(&pad_u256(commands_offset));
418    // Offset to state array
419    buf.extend_from_slice(&pad_u256(state_offset));
420
421    // Commands array: length + elements
422    buf.extend_from_slice(&pad_u256(commands.len()));
423    for cmd in commands {
424        buf.extend_from_slice(cmd);
425    }
426
427    // State array (dynamic): length + offsets + data
428    buf.extend_from_slice(&pad_u256(state.len()));
429
430    // Calculate offsets for each bytes element (relative to start of data area)
431    let data_area_start = state.len() * 32;
432    let mut current_offset = data_area_start;
433    for slot in state {
434        buf.extend_from_slice(&pad_u256(current_offset));
435        // Each element: 32 bytes length + ceil(len/32)*32 bytes data
436        current_offset += 32 + slot.len().div_ceil(32) * 32;
437    }
438
439    // Encode each bytes element
440    for slot in state {
441        buf.extend_from_slice(&pad_u256(slot.len()));
442        buf.extend_from_slice(slot);
443        // Pad to 32-byte boundary
444        let padding = (32 - (slot.len() % 32)) % 32;
445        buf.extend(std::iter::repeat_n(0u8, padding));
446    }
447
448    buf
449}
450
451/// Left-pad a `usize` into a 32-byte big-endian word.
452fn pad_u256(value: usize) -> [u8; 32] {
453    let mut word = [0u8; 32];
454    word[24..32].copy_from_slice(&(value as u64).to_be_bytes());
455    word
456}
457
458/// Apply a mutation to a value and return it — a porting convenience for
459/// builder-style field assignments.
460///
461/// In the `TypeScript` SDK, `defineReadOnly` uses `Object.defineProperty`
462/// to freeze a property as non-writable. Rust achieves immutability by
463/// default through its ownership system. This function exists as a porting
464/// convenience — it takes ownership of `object`, applies `setter`, and
465/// returns the modified value.
466///
467/// # Parameters
468///
469/// * `object` — the value to mutate (consumed by move).
470/// * `setter` — a closure that receives `&mut T` and applies the desired field assignment.
471///
472/// # Returns
473///
474/// The modified `object`.
475///
476/// # Example
477///
478/// ```
479/// use cow_weiroll::define_read_only;
480///
481/// #[derive(Debug, PartialEq)]
482/// struct Config {
483///     name: String,
484///     value: u32,
485/// }
486///
487/// let cfg = Config { name: String::new(), value: 0 };
488/// let cfg = define_read_only(cfg, |c| c.name = "example".into());
489/// let cfg = define_read_only(cfg, |c| c.value = 42);
490/// assert_eq!(cfg.name, "example");
491/// assert_eq!(cfg.value, 42);
492/// ```
493#[must_use]
494pub fn define_read_only<T>(mut object: T, setter: impl FnOnce(&mut T)) -> T {
495    setter(&mut object);
496    object
497}
498
499/// Look up a value by key in a static registry of `(key, value)` pairs.
500///
501/// In the `TypeScript` SDK, `getStatic` walks up the prototype chain
502/// (up to 32 levels) looking for a property on the constructor. In Rust
503/// there is no prototype chain; this function linearly searches a slice
504/// and returns a clone of the first matching value.
505///
506/// # Parameters
507///
508/// * `entries` — a slice of `(&str, T)` pairs to search.
509/// * `key` — the key to look up.
510///
511/// # Returns
512///
513/// `Some(value.clone())` if found, `None` otherwise.
514///
515/// # Example
516///
517/// ```
518/// use cow_weiroll::get_static;
519///
520/// let registry: &[(&str, i32)] = &[("version", 1), ("max_depth", 32)];
521///
522/// assert_eq!(get_static(registry, "version"), Some(1));
523/// assert_eq!(get_static(registry, "missing"), None);
524/// ```
525#[must_use]
526pub fn get_static<T: Clone>(entries: &[(&str, T)], key: &str) -> Option<T> {
527    entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone())
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    // ── WeirollCommand::pack ─────────────────────────────────────────────────
535
536    #[test]
537    fn pack_encodes_all_fields() {
538        let cmd = WeirollCommand {
539            flags: 0x01,
540            value: 0xFF,
541            gas: 21_000,
542            target: Address::ZERO,
543            selector: [0xde, 0xad, 0xbe, 0xef],
544            in_out: [0x01, 0x02, 0x03, 0x04],
545        };
546        let packed = cmd.pack();
547        assert_eq!(packed.len(), 32);
548        assert_eq!(packed[0], 0x01);
549        assert_eq!(packed[1], 0xFF);
550        assert_eq!(&packed[2..4], &21_000u16.to_be_bytes());
551        assert_eq!(&packed[4..24], Address::ZERO.as_slice());
552        assert_eq!(&packed[24..28], &[0xde, 0xad, 0xbe, 0xef]);
553        assert_eq!(&packed[28..32], &[0x01, 0x02, 0x03, 0x04]);
554    }
555
556    // ── WeirollCommand accessors ─────────────────────────────────────────────
557
558    #[test]
559    fn flags_ref_returns_flags() {
560        let cmd = WeirollCommand {
561            flags: 0x42,
562            value: 0,
563            gas: 0,
564            target: Address::ZERO,
565            selector: [0; 4],
566            in_out: [0; 4],
567        };
568        assert_eq!(cmd.flags_ref(), 0x42);
569    }
570
571    #[test]
572    fn target_ref_returns_address() {
573        let addr: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
574        let cmd = WeirollCommand {
575            flags: 0,
576            value: 0,
577            gas: 0,
578            target: addr,
579            selector: [0; 4],
580            in_out: [0; 4],
581        };
582        assert_eq!(*cmd.target_ref(), addr);
583    }
584
585    #[test]
586    fn selector_ref_returns_selector() {
587        let cmd = WeirollCommand {
588            flags: 0,
589            value: 0,
590            gas: 0,
591            target: Address::ZERO,
592            selector: [0xaa, 0xbb, 0xcc, 0xdd],
593            in_out: [0; 4],
594        };
595        assert_eq!(*cmd.selector_ref(), [0xaa, 0xbb, 0xcc, 0xdd]);
596    }
597
598    // ── WeirollScript ────────────────────────────────────────────────────────
599
600    #[test]
601    fn empty_script() {
602        let script = WeirollScript { commands: vec![], state: vec![] };
603        assert!(script.is_empty());
604        assert_eq!(script.command_count(), 0);
605        assert_eq!(script.state_slot_count(), 0);
606    }
607
608    #[test]
609    fn non_empty_script() {
610        let script =
611            WeirollScript { commands: vec![[0u8; 32], [1u8; 32]], state: vec![vec![0xab]] };
612        assert!(!script.is_empty());
613        assert_eq!(script.command_count(), 2);
614        assert_eq!(script.state_slot_count(), 1);
615    }
616
617    // ── WeirollCommandFlags constants ────────────────────────────────────────
618
619    #[test]
620    fn command_flags_values() {
621        assert_eq!(WeirollCommandFlags::DelegateCall as u8, 0x00);
622        assert_eq!(WeirollCommandFlags::Call as u8, 0x01);
623        assert_eq!(WeirollCommandFlags::StaticCall as u8, 0x02);
624        assert_eq!(WeirollCommandFlags::CallWithValue as u8, 0x03);
625    }
626
627    #[test]
628    fn calltype_mask_isolates_call_type() {
629        assert_eq!(
630            WeirollCommandFlags::Call as u8 & WeirollCommandFlags::CALLTYPE_MASK,
631            WeirollCommandFlags::Call as u8
632        );
633    }
634
635    #[test]
636    fn extended_command_and_tuple_return_bits() {
637        assert_eq!(WeirollCommandFlags::EXTENDED_COMMAND, 0x40);
638        assert_eq!(WeirollCommandFlags::TUPLE_RETURN, 0x80);
639    }
640
641    // ── create_weiroll_contract ──────────────────────────────────────────────
642
643    #[test]
644    fn create_contract_default_flags() {
645        let c = create_weiroll_contract(Address::ZERO, vec![], None);
646        assert_eq!(c.command_flags, WeirollCommandFlags::Call);
647    }
648
649    #[test]
650    fn create_contract_custom_flags() {
651        let c =
652            create_weiroll_contract(Address::ZERO, vec![], Some(WeirollCommandFlags::StaticCall));
653        assert_eq!(c.command_flags, WeirollCommandFlags::StaticCall);
654    }
655
656    // ── create_weiroll_library ───────────────────────────────────────────────
657
658    #[test]
659    fn create_library_uses_delegatecall() {
660        let lib = create_weiroll_library(Address::ZERO, vec![]);
661        assert_eq!(lib.command_flags, WeirollCommandFlags::DelegateCall);
662    }
663
664    // ── create_weiroll_delegate_call ─────────────────────────────────────────
665
666    #[test]
667    fn delegate_call_produces_valid_evm_call() {
668        let evm_call = create_weiroll_delegate_call(|planner| {
669            planner.add_command(WeirollCommand {
670                flags: 0,
671                value: 0,
672                gas: 0,
673                target: Address::ZERO,
674                selector: [0; 4],
675                in_out: [0; 4],
676            });
677        });
678        assert_eq!(evm_call.to, WEIROLL_CONTRACT_ADDRESS);
679        assert_eq!(evm_call.value, U256::ZERO);
680        // Selector is 0xde792be1
681        assert_eq!(&evm_call.data[..4], &[0xde, 0x79, 0x2b, 0xe1]);
682    }
683
684    #[test]
685    fn delegate_call_empty_planner() {
686        let evm_call = create_weiroll_delegate_call(|_| {});
687        assert_eq!(evm_call.to, WEIROLL_CONTRACT_ADDRESS);
688        assert!(!evm_call.data.is_empty());
689    }
690
691    // ── define_read_only ─────────────────────────────────────────────────────
692
693    #[test]
694    fn define_read_only_applies_mutation() {
695        let val = define_read_only(42u32, |v| *v = 100);
696        assert_eq!(val, 100);
697    }
698
699    #[test]
700    fn define_read_only_with_struct() {
701        #[derive(Default)]
702        struct S {
703            x: i32,
704        }
705        let s = define_read_only(S::default(), |s| s.x = 7);
706        assert_eq!(s.x, 7);
707    }
708
709    // ── get_static ───────────────────────────────────────────────────────────
710
711    #[test]
712    fn get_static_found() {
713        let entries: &[(&str, i32)] = &[("a", 1), ("b", 2)];
714        assert_eq!(get_static(entries, "b"), Some(2));
715    }
716
717    #[test]
718    fn get_static_not_found() {
719        let entries: &[(&str, i32)] = &[("a", 1)];
720        assert_eq!(get_static(entries, "z"), None);
721    }
722
723    #[test]
724    fn get_static_empty_entries() {
725        let entries: &[(&str, i32)] = &[];
726        assert_eq!(get_static(entries, "a"), None);
727    }
728
729    // ── WEIROLL_ADDRESS constant ─────────────────────────────────────────────
730
731    #[test]
732    fn weiroll_address_matches_constant() {
733        let parsed: Address = WEIROLL_ADDRESS.parse().unwrap();
734        assert_eq!(parsed, WEIROLL_CONTRACT_ADDRESS);
735    }
736}