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