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_ether_value, 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>]`. A double colon
39    /// (`::`) separates the address from the sig/data when the value is omitted.
40    pub fn parse(s: &str) -> Result<Self> {
41        let s = s.trim();
42        if s.is_empty() {
43            return Err(eyre!("Empty call specification"));
44        }
45
46        let parts: Vec<&str> = s.split(':').collect();
47        let to = Address::from_str(parts[0])
48            .map_err(|e| eyre!("Invalid address '{}': {}", parts[0], e))?;
49        let mut spec = Self { to, value: U256::ZERO, sig: None, args: Vec::new(), data: None };
50
51        // The first field is the value unless it is empty, a signature, or a terminal lowercase
52        // hex field, which is raw calldata.
53        let mut rest = &parts[1..];
54        if let Some((part, tail)) = rest.split_first() {
55            if part.is_empty() {
56                rest = tail;
57            } else if (!part.starts_with("0x") || !tail.is_empty()) && !part.contains('(') {
58                spec.value =
59                    parse_ether_value(part).wrap_err_with(|| format!("Invalid value '{part}'"))?;
60                rest = tail;
61            }
62        }
63
64        match rest.split_first() {
65            Some((part, tail)) if part.starts_with("0x") => {
66                let decoded =
67                    hex::decode(part).map_err(|e| eyre!("Invalid hex data '{}': {}", part, e))?;
68                eyre::ensure!(tail.is_empty(), "Unexpected trailing field(s) after raw calldata");
69                spec.data = Some(Bytes::from(decoded));
70            }
71            Some((part, tail)) if !part.is_empty() => {
72                spec.sig = Some(part.to_string());
73                if !tail.is_empty() {
74                    // Args are comma-separated; rejoin any colons that were split off.
75                    spec.args = tail.join(":").split(',').map(|s| s.trim().to_string()).collect();
76                }
77            }
78            _ => {}
79        }
80
81        Ok(spec)
82    }
83
84    /// Resolves this spec into a [`Call`], encoding function arguments if needed.
85    /// `i` is the 0-based index of this call; displayed as `i + 1` in error messages.
86    pub async fn resolve<N: Network, P: Provider<N>>(
87        &self,
88        i: usize,
89        chain: Chain,
90        provider: &P,
91        etherscan_api_key: Option<&str>,
92        etherscan_api_url: Option<&str>,
93    ) -> Result<Call> {
94        let input = if let Some(data) = &self.data {
95            data.clone()
96        } else if let Some(sig) = &self.sig {
97            let (encoded, _) = parse_function_args(
98                sig,
99                self.args.clone(),
100                Some(self.to),
101                chain,
102                provider,
103                etherscan_api_key,
104                etherscan_api_url,
105            )
106            .await
107            .map_err(|e| eyre!("Failed to encode call {}: {e}", i + 1))?;
108            Bytes::from(encoded)
109        } else {
110            Bytes::new()
111        };
112        Ok(Call { to: self.to.into(), value: self.value, input })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_parse_address_and_value() {
122        let address = "0x1234567890123456789012345678901234567890";
123
124        let spec = CallSpec::parse(address).unwrap();
125        assert_eq!(spec.to, address.parse::<Address>().unwrap());
126        assert_eq!(spec.value, U256::ZERO);
127        assert!(spec.sig.is_none() && spec.args.is_empty() && spec.data.is_none());
128
129        let spec = CallSpec::parse(&format!("{address}:1ether")).unwrap();
130        assert_eq!(spec.value, parse_ether_value("1ether").unwrap());
131        assert!(spec.sig.is_none());
132    }
133
134    #[test]
135    fn test_parse_lowercase_hex_value() {
136        let address = "0x1234567890123456789012345678901234567890";
137
138        let spec = CallSpec::parse(&format!("{address}:0x10:deposit()")).unwrap();
139        assert_eq!(spec.value, U256::from(16));
140        assert_eq!(spec.sig.as_deref(), Some("deposit()"));
141
142        let spec = CallSpec::parse(&format!("{address}:0x10")).unwrap();
143        assert_eq!(spec.value, U256::ZERO);
144        assert_eq!(spec.data, Some(Bytes::from([0x10])));
145    }
146
147    #[test]
148    fn test_parse_with_sig() {
149        let spec = CallSpec::parse(
150            "0x1234567890123456789012345678901234567890::transfer(address,uint256):0xabc,1000",
151        )
152        .unwrap();
153        assert_eq!(spec.value, U256::ZERO);
154        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
155        assert_eq!(spec.args, vec!["0xabc", "1000"]);
156    }
157
158    #[test]
159    fn test_parse_with_value_and_sig() {
160        let spec = CallSpec::parse(
161            "0x1234567890123456789012345678901234567890:0.5ether:transfer(address,uint256):0xabc,1000",
162        )
163        .unwrap();
164        assert_eq!(spec.value, parse_ether_value("0.5ether").unwrap());
165        assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
166    }
167
168    #[test]
169    fn test_parse_with_raw_data() {
170        let spec = CallSpec::parse("0x1234567890123456789012345678901234567890::0xabcdef").unwrap();
171        assert_eq!(spec.value, U256::ZERO);
172        assert!(spec.sig.is_none());
173        assert_eq!(spec.data, Some(Bytes::from(hex::decode("abcdef").unwrap())));
174    }
175
176    #[test]
177    fn test_parse_raw_data_rejects_trailing_fields() {
178        for spec in [
179            "0x1234567890123456789012345678901234567890::0xabcdef:typo",
180            "0x1234567890123456789012345678901234567890:1wei:0xabcdef:unexpected",
181        ] {
182            assert_eq!(
183                CallSpec::parse(spec).unwrap_err().to_string(),
184                "Unexpected trailing field(s) after raw calldata"
185            );
186        }
187    }
188}