1use 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#[derive(Debug, Clone)]
22pub struct CallSpec {
23 pub to: Address,
25 pub value: U256,
27 pub sig: Option<String>,
29 pub args: Vec<String>,
31 pub data: Option<Bytes>,
33}
34
35impl CallSpec {
36 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 let parts: Vec<&str> = s.split(':').collect();
52
53 if parts.is_empty() {
54 return Err(eyre!("Invalid call specification: {}", s));
55 }
56
57 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 let mut idx = 1;
69
70 if idx < parts.len() {
73 let part = parts[idx];
74 if !part.is_empty()
75 && (!part.starts_with("0x") || idx + 1 < parts.len())
76 && !part.contains('(')
77 {
78 value = parse_ether_or_wei(part)?;
80 idx += 1;
81 } else if part.is_empty() {
82 idx += 1;
84 }
85 }
86
87 if idx < parts.len() {
89 let part = parts[idx];
90 if part.starts_with("0x") {
91 let decoded =
93 hex::decode(part).map_err(|e| eyre!("Invalid hex data '{}': {}", part, e))?;
94 if idx + 1 != parts.len() {
95 return Err(eyre!("Unexpected trailing field(s) after raw calldata"));
96 }
97 data = Some(Bytes::from(decoded));
98 } else if !part.is_empty() {
99 sig = Some(part.to_string());
101 idx += 1;
102
103 if idx < parts.len() {
105 let args_str = parts[idx..].join(":");
106 args = args_str.split(',').map(|s| s.trim().to_string()).collect();
107 }
108 }
109 }
110
111 Ok(Self { to, value, sig, args, data })
112 }
113
114 pub async fn resolve<N: Network, P: Provider<N>>(
117 &self,
118 i: usize,
119 chain: Chain,
120 provider: &P,
121 etherscan_api_key: Option<&str>,
122 etherscan_api_url: Option<&str>,
123 ) -> Result<Call> {
124 let input = if let Some(data) = &self.data {
125 data.clone()
126 } else if let Some(sig) = &self.sig {
127 let (encoded, _) = parse_function_args(
128 sig,
129 self.args.clone(),
130 Some(self.to),
131 chain,
132 provider,
133 etherscan_api_key,
134 etherscan_api_url,
135 )
136 .await
137 .map_err(|e| eyre!("Failed to encode call {}: {e}", i + 1))?;
138 Bytes::from(encoded)
139 } else {
140 Bytes::new()
141 };
142 Ok(Call { to: self.to.into(), value: self.value, input })
143 }
144}
145
146impl FromStr for CallSpec {
147 type Err = eyre::Error;
148
149 fn from_str(s: &str) -> Result<Self> {
150 Self::parse(s)
151 }
152}
153
154fn parse_ether_or_wei(s: &str) -> Result<U256> {
156 if s.starts_with("0x") || s.starts_with("0X") {
158 U256::from_str(s).map_err(|e| eyre!("Invalid hex value '{}': {}", s, e))
159 } else {
160 alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), s)
161 .wrap_err_with(|| format!("Invalid value '{s}'"))?
162 .as_uint()
163 .map(|(v, _)| v)
164 .ok_or_else(|| eyre!("Could not parse value '{}'", s))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_parse_address_only() {
174 let spec = CallSpec::parse("0x1234567890123456789012345678901234567890").unwrap();
175 assert_eq!(
176 spec.to,
177 "0x1234567890123456789012345678901234567890".parse::<Address>().unwrap()
178 );
179 assert_eq!(spec.value, U256::ZERO);
180 assert!(spec.sig.is_none());
181 assert!(spec.args.is_empty());
182 assert!(spec.data.is_none());
183 }
184
185 #[test]
186 fn test_parse_with_value() {
187 let spec = CallSpec::parse("0x1234567890123456789012345678901234567890:1ether").unwrap();
188 assert_eq!(spec.value, parse_ether_or_wei("1ether").unwrap());
189 assert!(spec.sig.is_none());
190 }
191
192 #[test]
193 fn test_parse_hex_value() {
194 assert_eq!(parse_ether_or_wei("0x10").unwrap(), U256::from(16));
195 assert_eq!(parse_ether_or_wei("0X10").unwrap(), U256::from(16));
196 }
197
198 #[test]
199 fn test_parse_lowercase_hex_value() {
200 let address = "0x1234567890123456789012345678901234567890";
201
202 let spec = CallSpec::parse(&format!("{address}:0x10:deposit()")).unwrap();
203 assert_eq!(spec.value, U256::from(16));
204 assert_eq!(spec.sig.as_deref(), Some("deposit()"));
205
206 let spec = CallSpec::parse(&format!("{address}:0x10")).unwrap();
207 assert_eq!(spec.value, U256::ZERO);
208 assert_eq!(spec.data, Some(Bytes::from([0x10])));
209 }
210
211 #[test]
212 fn test_parse_with_sig() {
213 let spec = CallSpec::parse(
214 "0x1234567890123456789012345678901234567890::transfer(address,uint256):0xabc,1000",
215 )
216 .unwrap();
217 assert_eq!(spec.value, U256::ZERO);
218 assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
219 assert_eq!(spec.args, vec!["0xabc", "1000"]);
220 }
221
222 #[test]
223 fn test_parse_with_value_and_sig() {
224 let spec = CallSpec::parse(
225 "0x1234567890123456789012345678901234567890:0.5ether:transfer(address,uint256):0xabc,1000",
226 )
227 .unwrap();
228 assert_eq!(spec.value, parse_ether_or_wei("0.5ether").unwrap());
229 assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
230 }
231
232 #[test]
233 fn test_parse_with_raw_data() {
234 let spec = CallSpec::parse("0x1234567890123456789012345678901234567890::0xabcdef").unwrap();
235 assert_eq!(spec.value, U256::ZERO);
236 assert!(spec.sig.is_none());
237 assert_eq!(spec.data, Some(Bytes::from(hex::decode("abcdef").unwrap())));
238 }
239
240 #[test]
241 fn test_parse_raw_data_rejects_trailing_fields() {
242 for spec in [
243 "0x1234567890123456789012345678901234567890::0xabcdef:typo",
244 "0x1234567890123456789012345678901234567890:1wei:0xabcdef:unexpected",
245 ] {
246 assert_eq!(
247 CallSpec::parse(spec).unwrap_err().to_string(),
248 "Unexpected trailing field(s) after raw calldata"
249 );
250 }
251 }
252}