1use alloy_network::Network;
16use alloy_primitives::{Address, Bytes, U256, hex};
17use alloy_provider::Provider;
18use eyre::{Result, WrapErr, eyre};
19use foundry_cli::utils::{parse_ether_value, parse_function_args};
20use foundry_config::Chain;
21use std::str::FromStr;
22use tempo_primitives::transaction::Call;
23
24#[derive(Debug, Clone)]
26pub struct CallSpec {
27 pub to: Address,
29 pub value: U256,
31 pub sig: Option<String>,
33 pub args: Vec<String>,
35 legacy_args: Option<Vec<String>>,
37 pub data: Option<Bytes>,
39}
40
41impl CallSpec {
42 pub fn parse(s: &str) -> Result<Self> {
47 let s = s.trim();
48 if s.is_empty() {
49 return Err(eyre!("Empty call specification"));
50 }
51
52 let parts: Vec<&str> = s.split(':').collect();
53 let to = Address::from_str(parts[0])
54 .map_err(|e| eyre!("Invalid address '{}': {}", parts[0], e))?;
55 let mut spec = Self {
56 to,
57 value: U256::ZERO,
58 sig: None,
59 args: Vec::new(),
60 legacy_args: None,
61 data: None,
62 };
63
64 let mut rest = &parts[1..];
67 if let Some((part, tail)) = rest.split_first() {
68 if part.is_empty() {
69 rest = tail;
70 } else if (!part.starts_with("0x") || !tail.is_empty()) && !part.contains('(') {
71 spec.value =
72 parse_ether_value(part).wrap_err_with(|| format!("Invalid value '{part}'"))?;
73 rest = tail;
74 }
75 }
76
77 match rest.split_first() {
78 Some((part, tail)) if part.starts_with("0x") => {
79 let decoded =
80 hex::decode(part).map_err(|e| eyre!("Invalid hex data '{}': {}", part, e))?;
81 eyre::ensure!(tail.is_empty(), "Unexpected trailing field(s) after raw calldata");
82 spec.data = Some(Bytes::from(decoded));
83 }
84 Some((part, tail)) if !part.is_empty() => {
85 spec.sig = Some(part.to_string());
86 if !tail.is_empty() {
87 let args_str = tail.join(":");
89 spec.args = split_call_args(&args_str);
90 let legacy_args =
91 args_str.split(',').map(|arg| arg.trim().to_string()).collect::<Vec<_>>();
92 if legacy_args != spec.args {
93 spec.legacy_args = Some(legacy_args);
94 }
95 }
96 }
97 _ => {}
98 }
99
100 Ok(spec)
101 }
102
103 pub async fn resolve<N: Network, P: Provider<N>>(
106 &self,
107 i: usize,
108 chain: Chain,
109 provider: &P,
110 etherscan_api_key: Option<&str>,
111 etherscan_api_url: Option<&str>,
112 ) -> Result<Call> {
113 let input = if let Some(data) = &self.data {
114 data.clone()
115 } else if let Some(sig) = &self.sig {
116 let mut result = None;
117 for args in self.legacy_args.iter().chain(std::iter::once(&self.args)) {
118 result = Some(
119 parse_function_args(
120 sig,
121 args.clone(),
122 Some(self.to),
123 chain,
124 provider,
125 etherscan_api_key,
126 etherscan_api_url,
127 )
128 .await,
129 );
130 if result.as_ref().is_some_and(Result::is_ok) {
131 break;
132 }
133 }
134 let (encoded, _) = result
135 .expect("argument candidates are never empty")
136 .map_err(|e| eyre!("Failed to encode call {}: {e}", i + 1))?;
137 Bytes::from(encoded)
138 } else {
139 Bytes::new()
140 };
141 Ok(Call { to: self.to.into(), value: self.value, input })
142 }
143}
144
145fn split_call_args(s: &str) -> Vec<String> {
153 let mut args = Vec::new();
154 let mut depth = 0usize;
155 let mut start = 0usize;
156 let mut quote = None;
157 let mut at_value_start = true;
158
159 for (idx, ch) in s.char_indices() {
160 if let Some(active_quote) = quote {
161 if ch == active_quote {
162 quote = None;
163 }
164 continue;
165 }
166
167 match ch {
168 '\'' | '"' if at_value_start => {
169 quote = Some(ch);
170 at_value_start = false;
171 }
172 '(' | '[' => {
173 depth += 1;
174 at_value_start = true;
175 }
176 ')' | ']' => {
177 depth = depth.saturating_sub(1);
178 at_value_start = false;
179 }
180 ',' if depth == 0 => {
181 args.push(s[start..idx].trim().to_string());
182 start = idx + ch.len_utf8();
183 at_value_start = true;
184 }
185 ',' => at_value_start = true,
186 ch if ch.is_whitespace() && at_value_start => {}
187 _ => {
188 at_value_start = false;
189 }
190 }
191 }
192
193 args.push(s[start..].trim().to_string());
194 args
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use alloy_provider::{ProviderBuilder, mock::Asserter};
201 use foundry_common::abi::{encode_function_args, get_func};
202
203 const ADDRESS: &str = "0x1234567890123456789012345678901234567890";
204
205 async fn assert_resolves(cases: &[(&str, &str, &[&str])]) {
206 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
207 for &(suffix, sig, args) in cases {
208 let spec = format!("{ADDRESS}{suffix}");
209 let call = CallSpec::parse(&spec)
210 .unwrap()
211 .resolve(0, Chain::from_id(1), &provider, None, None)
212 .await
213 .unwrap();
214 let expected = encode_function_args(&get_func(sig).unwrap(), args).unwrap();
215 assert_eq!(call.input.as_ref(), expected, "call spec: {spec}");
216 }
217 }
218
219 #[test]
220 fn test_parse_address_and_value() {
221 let address = "0x1234567890123456789012345678901234567890";
222
223 let spec = CallSpec::parse(address).unwrap();
224 assert_eq!(spec.to, address.parse::<Address>().unwrap());
225 assert_eq!(spec.value, U256::ZERO);
226 assert!(spec.sig.is_none() && spec.args.is_empty() && spec.data.is_none());
227
228 let spec = CallSpec::parse(&format!("{address}:1ether")).unwrap();
229 assert_eq!(spec.value, parse_ether_value("1ether").unwrap());
230 assert!(spec.sig.is_none());
231 }
232
233 #[test]
234 fn test_parse_lowercase_hex_value() {
235 let address = "0x1234567890123456789012345678901234567890";
236
237 let spec = CallSpec::parse(&format!("{address}:0x10:deposit()")).unwrap();
238 assert_eq!(spec.value, U256::from(16));
239 assert_eq!(spec.sig.as_deref(), Some("deposit()"));
240
241 let spec = CallSpec::parse(&format!("{address}:0x10")).unwrap();
242 assert_eq!(spec.value, U256::ZERO);
243 assert_eq!(spec.data, Some(Bytes::from([0x10])));
244 }
245
246 #[test]
247 fn test_parse_with_sig() {
248 let spec = CallSpec::parse(
249 "0x1234567890123456789012345678901234567890::transfer(address,uint256):0xabc,1000",
250 )
251 .unwrap();
252 assert_eq!(spec.value, U256::ZERO);
253 assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
254 assert_eq!(spec.args, vec!["0xabc", "1000"]);
255 }
256
257 #[test]
258 fn test_parse_with_value_and_sig() {
259 let spec = CallSpec::parse(
260 "0x1234567890123456789012345678901234567890:0.5ether:transfer(address,uint256):0xabc,1000",
261 )
262 .unwrap();
263 assert_eq!(spec.value, parse_ether_value("0.5ether").unwrap());
264 assert_eq!(spec.sig, Some("transfer(address,uint256)".to_string()));
265 }
266
267 #[test]
268 fn test_parse_with_raw_data() {
269 let spec = CallSpec::parse("0x1234567890123456789012345678901234567890::0xabcdef").unwrap();
270 assert_eq!(spec.value, U256::ZERO);
271 assert!(spec.sig.is_none());
272 assert_eq!(spec.data, Some(Bytes::from(hex::decode("abcdef").unwrap())));
273 }
274
275 #[test]
276 fn test_parse_raw_data_rejects_trailing_fields() {
277 for spec in [
278 "0x1234567890123456789012345678901234567890::0xabcdef:typo",
279 "0x1234567890123456789012345678901234567890:1wei:0xabcdef:unexpected",
280 ] {
281 assert_eq!(
282 CallSpec::parse(spec).unwrap_err().to_string(),
283 "Unexpected trailing field(s) after raw calldata"
284 );
285 }
286 }
287
288 #[test]
289 fn test_parse_array_args() {
290 let spec =
291 CallSpec::parse("0x1234567890123456789012345678901234567890::foo(uint256[]):[1,2]")
292 .unwrap();
293 assert_eq!(spec.sig, Some("foo(uint256[])".to_string()));
294 assert_eq!(spec.args, vec!["[1,2]"]);
295
296 let spec = CallSpec::parse(
297 "0x1234567890123456789012345678901234567890::foo(uint256[][]):[[1,2],[3,4]]",
298 )
299 .unwrap();
300 assert_eq!(spec.sig, Some("foo(uint256[][])".to_string()));
301 assert_eq!(spec.args, vec!["[[1,2],[3,4]]"]);
302 }
303
304 #[test]
305 fn test_parse_tuple_args() {
306 let spec = CallSpec::parse(
307 "0x1234567890123456789012345678901234567890::foo((uint256,string)):(7,hello)",
308 )
309 .unwrap();
310 assert_eq!(spec.sig, Some("foo((uint256,string))".to_string()));
311 assert_eq!(spec.args, vec!["(7,hello)"]);
312
313 let spec = CallSpec::parse(
314 "0x1234567890123456789012345678901234567890::foo((uint256,string),uint256):(7,hello),9",
315 )
316 .unwrap();
317 assert_eq!(spec.sig, Some("foo((uint256,string),uint256)".to_string()));
318 assert_eq!(spec.args, vec!["(7,hello)", "9"]);
319 }
320
321 #[test]
322 fn test_parse_nested_structures() {
323 let spec = CallSpec::parse(
324 "0x1234567890123456789012345678901234567890::foo((uint256[],string)):([1,2],hello)",
325 )
326 .unwrap();
327 assert_eq!(spec.sig, Some("foo((uint256[],string))".to_string()));
328 assert_eq!(spec.args, vec!["([1,2],hello)"]);
329 }
330
331 #[test]
332 fn test_split_call_args() {
333 assert_eq!(split_call_args("[1,2]"), vec!["[1,2]"]);
334 assert_eq!(split_call_args("[[1,2],[3,4]]"), vec!["[[1,2],[3,4]]"]);
335 assert_eq!(split_call_args("(7,hello)"), vec!["(7,hello)"]);
336 assert_eq!(split_call_args("(7,hello),9"), vec!["(7,hello)", "9"]);
337 assert_eq!(split_call_args("1,2,3"), vec!["1", "2", "3"]);
338 assert_eq!(split_call_args("(1,2),(3,4)"), vec!["(1,2)", "(3,4)"]);
339 assert_eq!(split_call_args("[1,2],[3,4]"), vec!["[1,2]", "[3,4]"]);
340 assert_eq!(split_call_args("\"a,b\",9"), vec!["\"a,b\"", "9"]);
341 assert_eq!(split_call_args("(7,\"a],b\"),9"), vec!["(7,\"a],b\")", "9"]);
342 assert_eq!(split_call_args("can't,9"), vec!["can't", "9"]);
343 }
344
345 #[tokio::test]
346 async fn test_resolve_nested_args() {
347 const MIXED_ARGS: &str = "[(0x1111111111111111111111111111111111111111,[1,2]),(0x2222222222222222222222222222222222222222,[3])]";
348 let mixed =
349 format!("{ADDRESS}:1ether:airdrop((address,uint256[])[],bytes):{MIXED_ARGS},0x00");
350 assert_eq!(CallSpec::parse(&mixed).unwrap().value, parse_ether_value("1ether").unwrap());
351 assert_resolves(&[
352 ("::foo(uint256[][]):[[1,2],[3,4]]", "foo(uint256[][])", &["[[1,2],[3,4]]"]),
353 (
354 ":1ether:airdrop((address,uint256[])[],bytes):[(0x1111111111111111111111111111111111111111,[1,2]),(0x2222222222222222222222222222222222222222,[3])],0x00",
355 "airdrop((address,uint256[])[],bytes)",
356 &[MIXED_ARGS, "0x00"],
357 ),
358 (
359 "::foo((uint256,string),uint256):(7,hello),9",
360 "foo((uint256,string),uint256)",
361 &["(7,hello)", "9"],
362 ),
363 (
364 "::foo((uint256,string),uint256):(7,\"a],b\"),9",
365 "foo((uint256,string),uint256)",
366 &["(7,\"a],b\")", "9"],
367 ),
368 ])
369 .await;
370 }
371
372 #[tokio::test]
373 async fn test_resolve_preserves_legacy_string_args() {
374 assert_resolves(&[
375 ("::foo(string,string):(a,b)", "foo(string,string)", &["(a", "b)"]),
376 ("::foo(string,uint256):hello[,9", "foo(string,uint256)", &["hello[", "9"]),
377 ("::foo(string[],uint256):[\"[\"],9", "foo(string[],uint256)", &["[\"[\"]", "9"]),
378 ("::foo(string,uint256):can't,9", "foo(string,uint256)", &["can't", "9"]),
379 ])
380 .await;
381 }
382
383 #[tokio::test]
384 async fn test_resolve_quoted_commas_and_colons() {
385 assert_resolves(&[
386 ("::foo(string,uint256):\"a,b\",9", "foo(string,uint256)", &["\"a,b\"", "9"]),
387 ("::foo(string,uint256):'a,b',9", "foo(string,uint256)", &["'a,b'", "9"]),
388 ("::foo(string,uint256): urn:a:b , 9", "foo(string,uint256)", &["urn:a:b", "9"]),
389 ])
390 .await;
391 }
392
393 #[tokio::test]
394 async fn test_resolve_empty_args() {
395 assert_resolves(&[
396 ("::foo()", "foo()", &[]),
397 ("::foo(string):", "foo(string)", &[""]),
398 ("::foo(string,string):,value", "foo(string,string)", &["", "value"]),
399 ("::foo(string,string):value,", "foo(string,string)", &["value", ""]),
400 ("::foo(uint256[]):[]", "foo(uint256[])", &["[]"]),
401 ])
402 .await;
403 }
404
405 #[tokio::test]
406 async fn test_resolve_rejects_malformed_nested_args() {
407 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
408 for args in ["[1,2)", "[1,2", "[1,2]]", "\"a,b,9"] {
409 let sig = if args.starts_with('[') { "foo(uint256[])" } else { "foo(string,uint256)" };
410 let spec = CallSpec::parse(&format!("{ADDRESS}::{sig}:{args}")).unwrap();
411 assert!(
412 spec.resolve(0, Chain::from_id(1), &provider, None, None).await.is_err(),
413 "malformed arguments unexpectedly encoded: {args}"
414 );
415 }
416 }
417}