Skip to content

Commit e27ccc5

Browse files
authored
feat(cast): add Tempo wallet session create and revoke commands (#14978)
1 parent e5cb874 commit e27ccc5

5 files changed

Lines changed: 670 additions & 267 deletions

File tree

crates/cast/src/cmd/keychain.rs

Lines changed: 7 additions & 267 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use alloy_ens::NameOrAddress;
33
use std::time::Duration;
44

55
use alloy_network::{EthereumWallet, TransactionBuilder};
6-
use alloy_primitives::{Address, U256, hex, keccak256};
6+
use alloy_primitives::{Address, U256, hex};
77
use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
88
use alloy_rpc_types::BlockId;
99
use alloy_signer::Signer;
@@ -42,6 +42,10 @@ use tempo_primitives::transaction::{
4242
};
4343
use yansi::Paint;
4444

45+
use crate::cmd::tempo_policy_args::{
46+
SelectorArg, parse_period, parse_policy_token, parse_scope, parse_selector_arg,
47+
parse_selector_bytes,
48+
};
4549
use foundry_cli::utils::{maybe_print_resolved_lane, resolve_lane};
4650

4751
use crate::{
@@ -338,9 +342,6 @@ pub enum KeychainPolicySubcommand {
338342
},
339343
}
340344

341-
#[derive(Debug, Clone, Copy)]
342-
pub struct SelectorArg([u8; 4]);
343-
344345
fn parse_signature_type(s: &str) -> Result<SignatureType, String> {
345346
match s.to_lowercase().as_str() {
346347
"secp256k1" => Ok(SignatureType::Secp256k1),
@@ -403,150 +404,6 @@ fn parse_limit(s: &str) -> Result<TokenLimit, String> {
403404
Ok(TokenLimit { token, amount, period: 0 })
404405
}
405406

406-
/// Parse a `--scope TARGET[:SELECTORS[@RECIPIENTS]]` flag value.
407-
///
408-
/// Formats:
409-
/// - `0xAddr` — allow all calls to target
410-
/// - `0xAddr:transfer,approve` — allow only those selectors (by name or 4-byte hex)
411-
/// - `0xAddr:transfer@0xRecipient` — selector with recipient restriction
412-
fn parse_scope(s: &str) -> Result<CallScope, String> {
413-
let (target_str, selectors_str) = match s.split_once(':') {
414-
Some((t, sel)) => (t, Some(sel)),
415-
None => (s, None),
416-
};
417-
418-
let target: Address =
419-
target_str.parse().map_err(|e| format!("invalid target address '{target_str}': {e}"))?;
420-
421-
let selector_rules = match selectors_str {
422-
None => vec![],
423-
Some(sel_str) => parse_selector_rules(sel_str)?,
424-
};
425-
426-
Ok(CallScope { target, selectorRules: selector_rules })
427-
}
428-
429-
/// Parse comma-separated selectors, each optionally with `@recipient1,recipient2,...`.
430-
///
431-
/// Example: `transfer,approve` or `transfer@0x123` or `0xd09de08a`
432-
fn parse_selector_rules(s: &str) -> Result<Vec<SelectorRule>, String> {
433-
let mut rules = Vec::new();
434-
435-
for part in s.split(',') {
436-
let part = part.trim();
437-
if part.is_empty() {
438-
continue;
439-
}
440-
441-
let (selector_str, recipients_str) = match part.split_once('@') {
442-
Some((sel, recip)) => (sel, Some(recip)),
443-
None => (part, None),
444-
};
445-
446-
let selector = parse_selector_bytes(selector_str)?;
447-
448-
let recipients = match recipients_str {
449-
None => vec![],
450-
Some(r) => r
451-
.split(',')
452-
.filter(|s| !s.trim().is_empty())
453-
.map(|addr_str| {
454-
let addr_str = addr_str.trim();
455-
addr_str
456-
.parse::<Address>()
457-
.map_err(|e| format!("invalid recipient address '{addr_str}': {e}"))
458-
})
459-
.collect::<Result<Vec<_>, _>>()?,
460-
};
461-
462-
rules.push(SelectorRule { selector: selector.into(), recipients });
463-
}
464-
465-
Ok(rules)
466-
}
467-
468-
/// Parse a selector string: a 4-byte hex (`0xd09de08a`), a full signature
469-
/// (`transfer(address,uint256)`), or a well-known TIP-20 function name shorthand.
470-
///
471-
/// Recognized shorthands: `transfer`, `approve`, `transferFrom`, `transferWithMemo`,
472-
/// `transferFromWithMemo`. These resolve to the standard ERC20/TIP-20 signatures.
473-
/// Unknown names without parentheses are hashed as `name()`.
474-
fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> {
475-
let s = s.trim();
476-
if s.starts_with("0x") || s.starts_with("0X") {
477-
let hex_str = &s[2..];
478-
if hex_str.len() != 8 {
479-
return Err(format!("hex selector must be 4 bytes (8 hex chars), got: {s}"));
480-
}
481-
let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex selector '{s}': {e}"))?;
482-
let mut arr = [0u8; 4];
483-
arr.copy_from_slice(&bytes);
484-
Ok(arr)
485-
} else {
486-
// Expand well-known TIP-20 shorthands to full signatures.
487-
let sig = if s.contains('(') {
488-
s.to_string()
489-
} else {
490-
match s {
491-
"transfer" => "transfer(address,uint256)".to_string(),
492-
"approve" => "approve(address,uint256)".to_string(),
493-
"transferFrom" => "transferFrom(address,address,uint256)".to_string(),
494-
"transferWithMemo" => "transferWithMemo(address,uint256,bytes32)".to_string(),
495-
"transferFromWithMemo" => {
496-
"transferFromWithMemo(address,address,uint256,bytes32)".to_string()
497-
}
498-
_ => format!("{s}()"),
499-
}
500-
};
501-
let hash = keccak256(sig.as_bytes());
502-
let mut arr = [0u8; 4];
503-
arr.copy_from_slice(&hash[..4]);
504-
Ok(arr)
505-
}
506-
}
507-
508-
fn parse_selector_arg(s: &str) -> Result<SelectorArg, String> {
509-
parse_selector_bytes(s).map(SelectorArg)
510-
}
511-
512-
fn parse_policy_token(s: &str) -> Result<Address, String> {
513-
match s.to_ascii_lowercase().as_str() {
514-
"pathusd" | "path_usd" | "path-usd" | "usd" => Ok(PATH_USD_ADDRESS),
515-
_ => foundry_cli::utils::parse_fee_token_address(s).map_err(|e| e.to_string()),
516-
}
517-
}
518-
519-
fn parse_period(s: &str) -> Result<u64, String> {
520-
let s = s.trim();
521-
if s.is_empty() {
522-
return Err("period cannot be empty".to_string());
523-
}
524-
525-
let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
526-
if split == 0 {
527-
return Err(format!(
528-
"invalid period '{s}': expected a number followed by s, m, h, d, or w"
529-
));
530-
}
531-
532-
let value: u64 =
533-
s[..split].parse().map_err(|e| format!("invalid period value '{}': {e}", &s[..split]))?;
534-
let multiplier = match &s[split..].to_ascii_lowercase()[..] {
535-
"" | "s" => 1,
536-
"m" => 60,
537-
"h" => 60 * 60,
538-
"d" => 24 * 60 * 60,
539-
"w" => 7 * 24 * 60 * 60,
540-
unit => {
541-
return Err(format!(
542-
"invalid period unit '{unit}' in '{s}' (expected s, m, h, d, or w)"
543-
));
544-
}
545-
};
546-
547-
value.checked_mul(multiplier).ok_or_else(|| format!("period '{s}' is too large"))
548-
}
549-
550407
/// Represents a single scope entry in JSON format for `--scopes`.
551408
#[derive(serde::Deserialize)]
552409
struct JsonCallScope {
@@ -634,7 +491,7 @@ impl KeychainSubcommand {
634491
key_address,
635492
root_account,
636493
to,
637-
selector.map(|s| s.0),
494+
selector.map(SelectorArg::into_bytes),
638495
recipient,
639496
fee_token,
640497
tempo,
@@ -704,7 +561,7 @@ impl KeychainPolicySubcommand {
704561
key_address,
705562
root_account,
706563
target,
707-
selector.0,
564+
selector.into_bytes(),
708565
recipients,
709566
tx,
710567
send_tx,
@@ -3369,106 +3226,6 @@ mod tests {
33693226
use std::str::FromStr;
33703227
use tempo_primitives::transaction::{KeyAuthorization, PrimitiveSignature};
33713228

3372-
#[test]
3373-
fn test_parse_selector_bytes_named() {
3374-
let sel = parse_selector_bytes("transfer").unwrap();
3375-
assert_eq!(sel, keccak256(b"transfer(address,uint256)")[..4]);
3376-
3377-
let sel = parse_selector_bytes("approve").unwrap();
3378-
assert_eq!(sel, keccak256(b"approve(address,uint256)")[..4]);
3379-
3380-
let sel = parse_selector_bytes("transferWithMemo").unwrap();
3381-
assert_eq!(sel, keccak256(b"transferWithMemo(address,uint256,bytes32)")[..4]);
3382-
}
3383-
3384-
#[test]
3385-
fn test_parse_selector_bytes_hex() {
3386-
let sel = parse_selector_bytes("0xaabbccdd").unwrap();
3387-
assert_eq!(sel, [0xaa, 0xbb, 0xcc, 0xdd]);
3388-
3389-
let sel = parse_selector_bytes("0xd09de08a").unwrap();
3390-
assert_eq!(sel, [0xd0, 0x9d, 0xe0, 0x8a]);
3391-
}
3392-
3393-
#[test]
3394-
fn test_parse_selector_bytes_hex_invalid() {
3395-
assert!(parse_selector_bytes("0xaabb").is_err());
3396-
assert!(parse_selector_bytes("0xaabbccddee").is_err());
3397-
assert!(parse_selector_bytes("0xzzzzzzzz").is_err());
3398-
}
3399-
3400-
#[test]
3401-
fn test_parse_selector_bytes_full_signature() {
3402-
let sel = parse_selector_bytes("increment()").unwrap();
3403-
assert_eq!(sel, keccak256(b"increment()")[..4]);
3404-
}
3405-
3406-
#[test]
3407-
fn test_parse_selector_rules_simple() {
3408-
let rules = parse_selector_rules("transfer,approve").unwrap();
3409-
assert_eq!(rules.len(), 2);
3410-
assert!(rules[0].recipients.is_empty());
3411-
assert!(rules[1].recipients.is_empty());
3412-
}
3413-
3414-
#[test]
3415-
fn test_parse_selector_rules_with_recipient() {
3416-
let rules =
3417-
parse_selector_rules("transfer@0x1111111111111111111111111111111111111111").unwrap();
3418-
assert_eq!(rules.len(), 1);
3419-
assert_eq!(rules[0].recipients.len(), 1);
3420-
assert_eq!(
3421-
rules[0].recipients[0],
3422-
Address::from_str("0x1111111111111111111111111111111111111111").unwrap()
3423-
);
3424-
}
3425-
3426-
#[test]
3427-
fn test_parse_selector_rules_hex_with_recipient() {
3428-
let rules =
3429-
parse_selector_rules("0xaabbccdd@0x1111111111111111111111111111111111111111").unwrap();
3430-
assert_eq!(rules.len(), 1);
3431-
assert_eq!(rules[0].selector.0, [0xaa, 0xbb, 0xcc, 0xdd]);
3432-
assert_eq!(rules[0].recipients.len(), 1);
3433-
}
3434-
3435-
#[test]
3436-
fn test_parse_scope_target_only() {
3437-
let scope = parse_scope("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").unwrap();
3438-
assert_eq!(
3439-
scope.target,
3440-
Address::from_str("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").unwrap()
3441-
);
3442-
assert!(scope.selectorRules.is_empty());
3443-
}
3444-
3445-
#[test]
3446-
fn test_parse_scope_with_selectors() {
3447-
let scope =
3448-
parse_scope("0x20c0000000000000000000000000000000000001:transfer,approve").unwrap();
3449-
assert_eq!(scope.selectorRules.len(), 2);
3450-
assert!(scope.selectorRules[0].recipients.is_empty());
3451-
assert!(scope.selectorRules[1].recipients.is_empty());
3452-
}
3453-
3454-
#[test]
3455-
fn test_parse_scope_hex_selector() {
3456-
let scope = parse_scope("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D:0xaabbccdd").unwrap();
3457-
assert_eq!(scope.selectorRules.len(), 1);
3458-
assert_eq!(scope.selectorRules[0].selector.0, [0xaa, 0xbb, 0xcc, 0xdd]);
3459-
assert!(scope.selectorRules[0].recipients.is_empty());
3460-
}
3461-
3462-
#[test]
3463-
fn test_parse_scope_selector_with_recipient() {
3464-
let scope = parse_scope(
3465-
"0x20c0000000000000000000000000000000000001:transfer@0x1111111111111111111111111111111111111111",
3466-
)
3467-
.unwrap();
3468-
assert_eq!(scope.selectorRules.len(), 1);
3469-
assert_eq!(scope.selectorRules[0].recipients.len(), 1);
3470-
}
3471-
34723229
#[test]
34733230
fn test_parse_scopes_json_plain() {
34743231
let json = r#"[{"target":"0x20c0000000000000000000000000000000000001","selectors":["transfer","approve"]},{"target":"0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D"}]"#;
@@ -3493,23 +3250,6 @@ mod tests {
34933250
assert!(parse_scopes_json(json).is_err());
34943251
}
34953252

3496-
#[test]
3497-
fn test_parse_policy_token_path_usd() {
3498-
assert_eq!(parse_policy_token("PathUSD").unwrap(), PATH_USD_ADDRESS);
3499-
assert_eq!(parse_policy_token("path-usd").unwrap(), PATH_USD_ADDRESS);
3500-
}
3501-
3502-
#[test]
3503-
fn test_parse_period_units() {
3504-
assert_eq!(parse_period("0").unwrap(), 0);
3505-
assert_eq!(parse_period("30s").unwrap(), 30);
3506-
assert_eq!(parse_period("5m").unwrap(), 300);
3507-
assert_eq!(parse_period("2h").unwrap(), 7200);
3508-
assert_eq!(parse_period("7d").unwrap(), 604800);
3509-
assert_eq!(parse_period("2w").unwrap(), 1209600);
3510-
assert!(parse_period("1mo").is_err());
3511-
}
3512-
35133253
#[test]
35143254
fn test_add_selector_rule_merges_recipients() {
35153255
let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap();

crates/cast/src/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub mod run;
3030
pub mod send;
3131
pub mod storage;
3232
pub mod tempo;
33+
pub(crate) mod tempo_policy_args;
3334
pub mod tip20;
3435
pub mod trace;
3536
pub mod txpool;

0 commit comments

Comments
 (0)