Skip to main content

cast/cmd/
tempo_policy_args.rs

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// Shared Tempo policy flag grammar used by both `cast keychain` and
10// `cast wallet session`. Keeping it here avoids duplicating parsing behavior
11// or making wallet-session commands depend on the larger keychain command module.
12
13/// Parsed selector argument used by policy-editing commands.
14#[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
23/// Parse a selector string into 4-byte selector bytes.
24///
25/// Accepts 4-byte hex (`0xd09de08a`), a full signature
26/// (`transfer(address,uint256)`), or a well-known TIP-20 shorthand.
27pub(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
59/// Parse a selector string into a named selector argument.
60pub(crate) fn parse_selector_arg(s: &str) -> Result<SelectorArg, String> {
61    parse_selector_bytes(s).map(SelectorArg)
62}
63
64/// Parse a `TARGET[:SELECTORS[@RECIPIENTS]]` scope string.
65pub(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
139/// Parse a policy token label or address into an address.
140pub(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
147/// Parse a period string like `10m`, `7d`, or `3600s`.
148pub(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}