@@ -3,7 +3,7 @@ use alloy_ens::NameOrAddress;
33use std:: time:: Duration ;
44
55use alloy_network:: { EthereumWallet , TransactionBuilder } ;
6- use alloy_primitives:: { Address , U256 , hex, keccak256 } ;
6+ use alloy_primitives:: { Address , U256 , hex} ;
77use alloy_provider:: { Provider , ProviderBuilder as AlloyProviderBuilder } ;
88use alloy_rpc_types:: BlockId ;
99use alloy_signer:: Signer ;
@@ -42,6 +42,10 @@ use tempo_primitives::transaction::{
4242} ;
4343use 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+ } ;
4549use foundry_cli:: utils:: { maybe_print_resolved_lane, resolve_lane} ;
4650
4751use crate :: {
@@ -338,9 +342,6 @@ pub enum KeychainPolicySubcommand {
338342 } ,
339343}
340344
341- #[ derive( Debug , Clone , Copy ) ]
342- pub struct SelectorArg ( [ u8 ; 4 ] ) ;
343-
344345fn 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 ) ]
552409struct 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 ( ) ;
0 commit comments