Skip to main content

cast/
call_spec.rs

1//! Call specification parsing for batch transactions.
2//!
3//! Parses call specs in the format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xrawdata>]`
4//!
5//! Examples:
6//! - `0x123` - Just an address (empty call)
7//! - `0x123:0.1ether` - ETH transfer
8//! - `0x123::transfer(address,uint256):0x789,1000` - Contract call with signature
9//! - `0x123::0xabcdef` - Contract call with raw calldata
10
11use alloy_primitives::{Address, Bytes, U256, hex};
12use eyre::{Result, WrapErr, eyre};
13use std::str::FromStr;
14
15/// A parsed call specification for batch transactions.
16#[derive(Debug, Clone)]
17pub struct CallSpec {
18    /// Target address (required)
19    pub to: Address,
20    /// ETH value to send (optional, defaults to 0)
21    pub value: U256,
22    /// Function signature, e.g., "transfer(address,uint256)" (optional)
23    pub sig: Option<String>,
24    /// Function arguments (optional)
25    pub args: Vec<String>,
26    /// Raw calldata if provided instead of sig+args (optional)
27    pub data: Option<Bytes>,
28}
29
30impl CallSpec {
31    /// Parse a call spec string.
32    ///
33    /// Format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xrawdata>]`
34    ///
35    /// The delimiter is `:` but we need to be careful about:
36    /// - Colons in function signatures (none expected)
37    /// - Colons in hex addresses (none expected)
38    /// - We use double-colon `::` to separate value from sig/data when value is empty
39    pub fn parse(s: &str) -> Result<Self> {
40        let s = s.trim();
41        if s.is_empty() {
42            return Err(eyre!("Empty call specification"));
43        }
44
45        // Split by `:` but handle `::` for empty value
46        let parts: Vec<&str> = s.split(':').collect();
47
48        if parts.is_empty() {
49            return Err(eyre!("Invalid call specification: {}", s));
50        }
51
52        // First part is always the address
53        let to = Address::from_str(parts[0])
54            .map_err(|e| eyre!("Invalid address '{}': {}", parts[0], e))?;
55
56        let mut value = U256::ZERO;
57        let mut sig = None;
58        let mut args = Vec::new();
59        let mut data = None;
60
61        // Parse remaining parts
62        // Pattern: to:value:sig:args or to::sig:args (empty value) or to:value:0xdata
63        let mut idx = 1;
64
65        // Check for value (non-empty, not starting with 0x unless it's a number)
66        if idx < parts.len() {
67            let part = parts[idx];
68            if !part.is_empty() && !part.starts_with("0x") && !part.contains('(') {
69                // This looks like a value
70                value = parse_ether_or_wei(part)?;
71                idx += 1;
72            } else if part.is_empty() {
73                // Empty value (::), skip
74                idx += 1;
75            }
76        }
77
78        // Check for sig/data
79        if idx < parts.len() {
80            let part = parts[idx];
81            if part.starts_with("0x") {
82                // Raw calldata
83                data = Some(Bytes::from(
84                    hex::decode(part).map_err(|e| eyre!("Invalid hex data '{}': {}", part, e))?,
85                ));
86            } else if !part.is_empty() {
87                // Function signature
88                sig = Some(part.to_string());
89                idx += 1;
90
91                // Collect remaining parts as args (comma-separated in the last part)
92                if idx < parts.len() {
93                    let args_str = parts[idx..].join(":");
94                    args = args_str.split(',').map(|s| s.trim().to_string()).collect();
95                }
96            }
97        }
98
99        Ok(Self { to, value, sig, args, data })
100    }
101}
102
103impl FromStr for CallSpec {
104    type Err = eyre::Error;
105
106    fn from_str(s: &str) -> Result<Self> {
107        Self::parse(s)
108    }
109}
110
111/// Parse a value string that can be in ether notation (e.g., "0.1ether") or raw wei.
112fn parse_ether_or_wei(s: &str) -> Result<U256> {
113    // Use alloy's DynSolType coercion which handles "1ether", "1gwei", "1000" etc.
114    if s.starts_with("0x") {
115        U256::from_str_radix(s, 16).map_err(|e| eyre!("Invalid hex value '{}': {}", s, e))
116    } else {
117        alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), s)
118            .wrap_err_with(|| format!("Invalid value '{s}'"))?
119            .as_uint()
120            .map(|(v, _)| v)
121            .ok_or_else(|| eyre!("Could not parse value '{}'", s))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_parse_address_only() {
131        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890").unwrap();
132        assert_eq!(
133            spec.to,
134            "0x1234567890123456789012345678901234567890".parse::<Address>().unwrap()
135        );
136        assert_eq!(spec.value, U256::ZERO);
137        assert!(spec.sig.is_none());
138        assert!(spec.args.is_empty());
139        assert!(spec.data.is_none());
140    }
141
142    #[test]
143    fn test_parse_with_value() {
144        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890:1ether").unwrap();
145        assert_eq!(spec.value, parse_ether_or_wei("1ether").unwrap());
146        assert!(spec.sig.is_none());
147    }
148
149    #[test]
150    fn test_parse_with_sig() {
151        let spec = CallSpec::parse(
152            "0x1234567890123456789012345678901234567890::transfer(address,uint256):0xabc,1000",
153        )
154        .unwrap();
155        assert_eq!(spec.value, U256::ZERO);
156        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
157        assert_eq!(spec.args, vec!["0xabc", "1000"]);
158    }
159
160    #[test]
161    fn test_parse_with_value_and_sig() {
162        let spec = CallSpec::parse(
163            "0x1234567890123456789012345678901234567890:0.5ether:transfer(address,uint256):0xabc,1000",
164        )
165        .unwrap();
166        assert_eq!(spec.value, parse_ether_or_wei("0.5ether").unwrap());
167        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
168    }
169
170    #[test]
171    fn test_parse_with_raw_data() {
172        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890::0xabcdef").unwrap();
173        assert_eq!(spec.value, U256::ZERO);
174        assert!(spec.sig.is_none());
175        assert_eq!(spec.data, Some(Bytes::from(hex::decode("abcdef").unwrap())));
176    }
177}