1use alloy_primitives::{Address, hex};
2use foundry_common::abi::get_func;
3use tempo_contracts::precompiles::IAccountKeychain::{CallScope, SelectorRule};
4
5pub(crate) fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> {
14 let s = s.trim();
15 if s.starts_with("0x") || s.starts_with("0X") {
16 let hex_str = &s[2..];
17 if hex_str.len() != 8 {
18 return Err(format!("hex selector must be 4 bytes (8 hex chars), got: {s}"));
19 }
20 let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex selector '{s}': {e}"))?;
21 let mut arr = [0u8; 4];
22 arr.copy_from_slice(&bytes);
23 Ok(arr)
24 } else {
25 let sig = if s.contains('(') || s.contains(')') {
26 s.to_string()
27 } else {
28 match s {
29 "transfer" => "transfer(address,uint256)".to_string(),
30 "approve" => "approve(address,uint256)".to_string(),
31 "transferFrom" => "transferFrom(address,address,uint256)".to_string(),
32 "transferWithMemo" => "transferWithMemo(address,uint256,bytes32)".to_string(),
33 "transferFromWithMemo" => {
34 "transferFromWithMemo(address,address,uint256,bytes32)".to_string()
35 }
36 _ => format!("{s}()"),
37 }
38 };
39 get_func(&sig)
40 .map(|func| func.selector().into())
41 .map_err(|e| format!("invalid function signature '{sig}': {e}"))
42 }
43}
44
45pub(crate) fn parse_scope(s: &str) -> Result<CallScope, String> {
47 let (target_str, selectors_str) =
48 s.split_once(':').map_or((s, None), |(target, selectors)| (target, Some(selectors)));
49
50 let target: Address =
51 target_str.parse().map_err(|e| format!("invalid target address '{target_str}': {e}"))?;
52 let selector_rules = selectors_str.map_or(Ok(vec![]), parse_selector_rules)?;
53
54 Ok(CallScope { target, selectorRules: selector_rules })
55}
56
57fn parse_selector_rules(s: &str) -> Result<Vec<SelectorRule>, String> {
58 let mut rules = Vec::<SelectorRule>::new();
59
60 for part in split_selector_rule_parts(s) {
61 let part = part.trim();
62 if part.is_empty() {
63 continue;
64 }
65
66 if let Some(rule) = rules.last_mut()
68 && !rule.recipients.is_empty()
69 && (part.starts_with("0x") || part.starts_with("0X"))
70 && let Ok(addr) = part.parse::<Address>()
71 {
72 rule.recipients.push(addr);
73 continue;
74 }
75
76 let (selector_str, recipients_str) = part.split_once('@').unwrap_or((part, ""));
77 let selector = parse_selector_bytes(selector_str)?;
78 let recipients = recipients_str
79 .split(',')
80 .map(str::trim)
81 .filter(|s| !s.is_empty())
82 .map(|addr_str| {
83 addr_str
84 .parse::<Address>()
85 .map_err(|e| format!("invalid recipient address '{addr_str}': {e}"))
86 })
87 .collect::<Result<Vec<_>, _>>()?;
88
89 rules.push(SelectorRule { selector: selector.into(), recipients });
90 }
91
92 Ok(rules)
93}
94
95fn split_selector_rule_parts(s: &str) -> Vec<&str> {
96 let mut parts = Vec::new();
97 let mut depth = 0usize;
98 let mut start = 0usize;
99
100 for (idx, ch) in s.char_indices() {
101 match ch {
102 '(' => depth += 1,
103 ')' => depth = depth.saturating_sub(1),
104 ',' if depth == 0 => {
105 parts.push(&s[start..idx]);
106 start = idx + ch.len_utf8();
107 }
108 _ => {}
109 }
110 }
111
112 parts.push(&s[start..]);
113 parts
114}
115
116pub(crate) fn parse_period(s: &str) -> Result<u64, String> {
118 let s = s.trim();
119 if s.is_empty() {
120 return Err("period cannot be empty".to_string());
121 }
122
123 let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
124 if split == 0 {
125 return Err(format!(
126 "invalid period '{s}': expected a number followed by s, m, h, d, or w"
127 ));
128 }
129
130 let value: u64 =
131 s[..split].parse().map_err(|e| format!("invalid period value '{}': {e}", &s[..split]))?;
132 let multiplier = match &s[split..].to_ascii_lowercase()[..] {
133 "" | "s" => 1,
134 "m" => 60,
135 "h" => 60 * 60,
136 "d" => 24 * 60 * 60,
137 "w" => 7 * 24 * 60 * 60,
138 unit => {
139 return Err(format!(
140 "invalid period unit '{unit}' in '{s}' (expected s, m, h, d, or w)"
141 ));
142 }
143 };
144
145 value.checked_mul(multiplier).ok_or_else(|| format!("period '{s}' is too large"))
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use alloy_primitives::{address, keccak256};
152
153 fn selector(sig: &str) -> [u8; 4] {
154 keccak256(sig.as_bytes())[..4].try_into().unwrap()
155 }
156
157 #[test]
158 fn parse_selector_bytes_accepts_names_hex_and_signatures() {
159 for (input, expected) in [
160 ("transfer", selector("transfer(address,uint256)")),
161 ("approve", selector("approve(address,uint256)")),
162 ("transferWithMemo", selector("transferWithMemo(address,uint256,bytes32)")),
163 ("increment()", selector("increment()")),
164 ("transfer(address,uint256)", selector("transfer(address,uint256)")),
165 ("0xaabbccdd", [0xaa, 0xbb, 0xcc, 0xdd]),
166 ("0xd09de08a", [0xd0, 0x9d, 0xe0, 0x8a]),
167 ] {
168 assert_eq!(parse_selector_bytes(input).unwrap(), expected, "{input}");
169 }
170 for input in
171 ["0xaabb", "0xaabbccddee", "0xzzzzzzzz", "", "transfer(address,uint256", "transfer)"]
172 {
173 assert!(parse_selector_bytes(input).is_err(), "{input}");
174 }
175 }
176
177 #[test]
178 fn parse_scope_variants() {
179 let target = address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D");
180 let recipient = address!("0x1111111111111111111111111111111111111111");
181 let recipient2 = address!("0x2222222222222222222222222222222222222222");
182 let cases = [
184 ("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D", vec![]),
185 (
186 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:0xaabbccdd",
187 vec![([0xaa, 0xbb, 0xcc, 0xdd], vec![])],
188 ),
189 (
190 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer,approve",
191 vec![
192 (selector("transfer(address,uint256)"), vec![]),
193 (selector("approve(address,uint256)"), vec![]),
194 ],
195 ),
196 (
197 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer@0x1111111111111111111111111111111111111111",
198 vec![(selector("transfer(address,uint256)"), vec![recipient])],
199 ),
200 (
201 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:0xaabbccdd@0x1111111111111111111111111111111111111111",
202 vec![([0xaa, 0xbb, 0xcc, 0xdd], vec![recipient])],
203 ),
204 (
205 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer@0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222",
207 vec![(selector("transfer(address,uint256)"), vec![recipient, recipient2])],
208 ),
209 (
210 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer@0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222,approve",
212 vec![
213 (selector("transfer(address,uint256)"), vec![recipient, recipient2]),
214 (selector("approve(address,uint256)"), vec![]),
215 ],
216 ),
217 (
218 "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer@0x1111111111111111111111111111111111111111,approve@0x2222222222222222222222222222222222222222",
220 vec![
221 (selector("transfer(address,uint256)"), vec![recipient]),
222 (selector("approve(address,uint256)"), vec![recipient2]),
223 ],
224 ),
225 ];
226 for (input, expected) in cases {
227 let scope = parse_scope(input).unwrap();
228 assert_eq!(scope.target, target, "{input}");
229 let rules: Vec<_> =
230 scope.selectorRules.iter().map(|r| (r.selector.0, r.recipients.clone())).collect();
231 assert_eq!(rules, expected, "{input}");
232 }
233 }
234
235 #[test]
236 fn parse_scope_hex_like_function_name_starts_a_new_rule() {
237 let scope = parse_scope(
238 "0x20c0000000000000000000000000000000000001:transfer@0x1111111111111111111111111111111111111111,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
239 )
240 .unwrap();
241 assert_eq!(scope.selectorRules.len(), 2);
242 assert_eq!(scope.selectorRules[0].recipients.len(), 1);
243 assert_eq!(
244 scope.selectorRules[1].selector.0,
245 selector("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa()")
246 );
247 assert!(scope.selectorRules[1].recipients.is_empty());
248 }
249
250 #[test]
251 fn parse_scope_bogus_selector_after_unrestricted_rule_still_errors() {
252 let err = parse_scope(
254 "0x20c0000000000000000000000000000000000001:transfer,0x2222222222222222222222222222222222222222",
255 )
256 .unwrap_err();
257 assert!(err.contains("hex selector must be 4 bytes"), "{err}");
258 }
259
260 #[test]
261 fn parse_scope_full_signatures_split_outside_parentheses() {
262 let scope = parse_scope(
263 "0x20c0000000000000000000000000000000000001:transfer(address,uint256),approve(address,uint256)",
264 )
265 .unwrap();
266 assert_eq!(scope.selectorRules.len(), 2);
267 assert_eq!(scope.selectorRules[0].selector.0, keccak256(b"transfer(address,uint256)")[..4]);
268 assert_eq!(scope.selectorRules[1].selector.0, keccak256(b"approve(address,uint256)")[..4]);
269 }
270
271 #[test]
272 fn parse_period_units() {
273 assert_eq!(parse_period("0").unwrap(), 0);
274 assert_eq!(parse_period("30s").unwrap(), 30);
275 assert_eq!(parse_period("5m").unwrap(), 300);
276 assert_eq!(parse_period("2h").unwrap(), 7200);
277 assert_eq!(parse_period("7d").unwrap(), 604800);
278 assert_eq!(parse_period("2w").unwrap(), 1209600);
279 assert!(parse_period("1mo").is_err());
280 }
281}