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}