Skip to main content

cast/cmd/
tempo_policy_args.rs

1use alloy_primitives::{Address, hex};
2use foundry_common::abi::get_func;
3use tempo_contracts::precompiles::IAccountKeychain::{CallScope, SelectorRule};
4
5// Shared Tempo policy flag grammar used by both `cast keychain` and
6// `cast wallet session`. Keeping it here avoids duplicating parsing behavior
7// or making wallet-session commands depend on the larger keychain command module.
8
9/// Parse a selector string into 4-byte selector bytes.
10///
11/// Accepts 4-byte hex (`0xd09de08a`), a full signature
12/// (`transfer(address,uint256)`), or a well-known TIP-20 shorthand.
13pub(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
45/// Parse a `TARGET[:SELECTORS[@RECIPIENTS]]` scope string.
46pub(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        // Require a hex prefix to distinguish recipients from hex-like function names.
67        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
116/// Parse a period string like `10m`, `7d`, or `3600s`.
117pub(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        // (input, expected selectors, expected recipients per rule)
183        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                // Multiple recipients for one rule.
206                "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:transfer@0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222",
207                vec![(selector("transfer(address,uint256)"), vec![recipient, recipient2])],
208            ),
209            (
210                // An unrestricted rule ends the previous recipient list.
211                "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                // Recipient lists stay local to their rules.
219                "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        // A bare address cannot continue a rule without an @ clause.
253        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}