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_network::Network;
12use alloy_primitives::{Address, Bytes, U256, hex};
13use alloy_provider::Provider;
14use eyre::{Result, WrapErr, eyre};
15use foundry_cli::utils::parse_function_args;
16use foundry_config::Chain;
17use std::str::FromStr;
18use tempo_primitives::transaction::Call;
19
20/// A parsed call specification for batch transactions.
21#[derive(Debug, Clone)]
22pub struct CallSpec {
23    /// Target address (required)
24    pub to: Address,
25    /// ETH value to send (optional, defaults to 0)
26    pub value: U256,
27    /// Function signature, e.g., "transfer(address,uint256)" (optional)
28    pub sig: Option<String>,
29    /// Function arguments (optional)
30    pub args: Vec<String>,
31    /// Raw calldata if provided instead of sig+args (optional)
32    pub data: Option<Bytes>,
33}
34
35impl CallSpec {
36    /// Parse a call spec string.
37    ///
38    /// Format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xrawdata>]`
39    ///
40    /// The delimiter is `:` but we need to be careful about:
41    /// - Colons in function signatures (none expected)
42    /// - Colons in hex addresses (none expected)
43    /// - We use double-colon `::` to separate value from sig/data when value is empty
44    pub fn parse(s: &str) -> Result<Self> {
45        let s = s.trim();
46        if s.is_empty() {
47            return Err(eyre!("Empty call specification"));
48        }
49
50        // Split by `:` but handle `::` for empty value
51        let parts: Vec<&str> = s.split(':').collect();
52
53        if parts.is_empty() {
54            return Err(eyre!("Invalid call specification: {}", s));
55        }
56
57        // First part is always the address
58        let to = Address::from_str(parts[0])
59            .map_err(|e| eyre!("Invalid address '{}': {}", parts[0], e))?;
60
61        let mut value = U256::ZERO;
62        let mut sig = None;
63        let mut args = Vec::new();
64        let mut data = None;
65
66        // Parse remaining parts
67        // Pattern: to:value:sig:args or to::sig:args (empty value) or to:value:0xdata
68        let mut idx = 1;
69
70        // Check for value (non-empty, not starting with 0x unless it's a number)
71        if idx < parts.len() {
72            let part = parts[idx];
73            if !part.is_empty() && !part.starts_with("0x") && !part.contains('(') {
74                // This looks like a value
75                value = parse_ether_or_wei(part)?;
76                idx += 1;
77            } else if part.is_empty() {
78                // Empty value (::), skip
79                idx += 1;
80            }
81        }
82
83        // Check for sig/data
84        if idx < parts.len() {
85            let part = parts[idx];
86            if part.starts_with("0x") {
87                // Raw calldata
88                data = Some(Bytes::from(
89                    hex::decode(part).map_err(|e| eyre!("Invalid hex data '{}': {}", part, e))?,
90                ));
91            } else if !part.is_empty() {
92                // Function signature
93                sig = Some(part.to_string());
94                idx += 1;
95
96                // Collect remaining parts as args (comma-separated in the last part)
97                if idx < parts.len() {
98                    let args_str = parts[idx..].join(":");
99                    args = args_str.split(',').map(|s| s.trim().to_string()).collect();
100                }
101            }
102        }
103
104        Ok(Self { to, value, sig, args, data })
105    }
106
107    /// Resolves this spec into a [`Call`], encoding function arguments if needed.
108    /// `i` is the 0-based index of this call; displayed as `i + 1` in error messages.
109    pub async fn resolve<N: Network, P: Provider<N>>(
110        &self,
111        i: usize,
112        chain: Chain,
113        provider: &P,
114        etherscan_api_key: Option<&str>,
115        etherscan_api_url: Option<&str>,
116    ) -> Result<Call> {
117        let input = if let Some(data) = &self.data {
118            data.clone()
119        } else if let Some(sig) = &self.sig {
120            let (encoded, _) = parse_function_args(
121                sig,
122                self.args.clone(),
123                Some(self.to),
124                chain,
125                provider,
126                etherscan_api_key,
127                etherscan_api_url,
128            )
129            .await
130            .map_err(|e| eyre!("Failed to encode call {}: {e}", i + 1))?;
131            Bytes::from(encoded)
132        } else {
133            Bytes::new()
134        };
135        Ok(Call { to: self.to.into(), value: self.value, input })
136    }
137}
138
139impl FromStr for CallSpec {
140    type Err = eyre::Error;
141
142    fn from_str(s: &str) -> Result<Self> {
143        Self::parse(s)
144    }
145}
146
147/// Parse a value string that can be in ether notation (e.g., "0.1ether") or raw wei.
148fn parse_ether_or_wei(s: &str) -> Result<U256> {
149    // Use alloy's DynSolType coercion which handles "1ether", "1gwei", "1000" etc.
150    if s.starts_with("0x") || s.starts_with("0X") {
151        U256::from_str(s).map_err(|e| eyre!("Invalid hex value '{}': {}", s, e))
152    } else {
153        alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), s)
154            .wrap_err_with(|| format!("Invalid value '{s}'"))?
155            .as_uint()
156            .map(|(v, _)| v)
157            .ok_or_else(|| eyre!("Could not parse value '{}'", s))
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_parse_address_only() {
167        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890").unwrap();
168        assert_eq!(
169            spec.to,
170            "0x1234567890123456789012345678901234567890".parse::<Address>().unwrap()
171        );
172        assert_eq!(spec.value, U256::ZERO);
173        assert!(spec.sig.is_none());
174        assert!(spec.args.is_empty());
175        assert!(spec.data.is_none());
176    }
177
178    #[test]
179    fn test_parse_with_value() {
180        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890:1ether").unwrap();
181        assert_eq!(spec.value, parse_ether_or_wei("1ether").unwrap());
182        assert!(spec.sig.is_none());
183    }
184
185    #[test]
186    fn test_parse_hex_value() {
187        assert_eq!(parse_ether_or_wei("0x10").unwrap(), U256::from(16));
188        assert_eq!(parse_ether_or_wei("0X10").unwrap(), U256::from(16));
189    }
190
191    #[test]
192    fn test_parse_with_sig() {
193        let spec = CallSpec::parse(
194            "0x1234567890123456789012345678901234567890::transfer(address,uint256):0xabc,1000",
195        )
196        .unwrap();
197        assert_eq!(spec.value, U256::ZERO);
198        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
199        assert_eq!(spec.args, vec!["0xabc", "1000"]);
200    }
201
202    #[test]
203    fn test_parse_with_value_and_sig() {
204        let spec = CallSpec::parse(
205            "0x1234567890123456789012345678901234567890:0.5ether:transfer(address,uint256):0xabc,1000",
206        )
207        .unwrap();
208        assert_eq!(spec.value, parse_ether_or_wei("0.5ether").unwrap());
209        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
210    }
211
212    #[test]
213    fn test_parse_with_raw_data() {
214        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890::0xabcdef").unwrap();
215        assert_eq!(spec.value, U256::ZERO);
216        assert!(spec.sig.is_none());
217        assert_eq!(spec.data, Some(Bytes::from(hex::decode("abcdef").unwrap())));
218    }
219}