Skip to main content

tempo_alloy/provider/
keychain.rs

1use core::fmt;
2
3use alloy_primitives::{Address, Bytes, TxKind, U256};
4use alloy_sol_types::SolCall;
5use tempo_contracts::precompiles::{
6    ACCOUNT_KEYCHAIN_ADDRESS,
7    IAccountKeychain::{
8        KeyRestrictions as AbiKeyRestrictions, LegacyTokenLimit as AbiLegacyTokenLimit,
9        TokenLimit as AbiTokenLimit, removeAllowedCallsCall, revokeKeyCall, setAllowedCallsCall,
10        updateSpendingLimitCall,
11    },
12    ITIP20, authorizeKeyCall, legacyAuthorizeKeyCall,
13};
14use tempo_primitives::{
15    SignatureType,
16    transaction::{Call, CallScope, SelectorRule, TokenLimit},
17};
18
19/// SDK-level access-key restrictions used for AccountKeychain call builders.
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct KeyRestrictions {
22    /// Unix timestamp when the key expires. `None` means never expires.
23    expiry: Option<u64>,
24    /// Optional token spending limits. `None` means unlimited spending.
25    limits: Option<Vec<TokenLimit>>,
26    /// Optional call scopes. `None` means unrestricted calls.
27    allowed_calls: Option<Vec<CallScope>>,
28}
29
30impl KeyRestrictions {
31    /// Set an expiry timestamp.
32    pub fn with_expiry(mut self, expiry: u64) -> Self {
33        self.expiry = Some(expiry);
34        self
35    }
36
37    /// Set token spending limits.
38    pub fn with_limits(mut self, limits: Vec<TokenLimit>) -> Self {
39        self.limits = Some(limits);
40        self
41    }
42
43    /// Set call-scope restrictions.
44    pub fn with_allowed_calls(mut self, allowed_calls: Vec<CallScope>) -> Self {
45        self.allowed_calls = Some(allowed_calls);
46        self
47    }
48
49    /// Deny all spending (enforce limits with an empty allowlist).
50    pub fn with_no_spending(mut self) -> Self {
51        self.limits = Some(Vec::new());
52        self
53    }
54
55    /// Deny all calls (scoped mode with an empty allowlist).
56    pub fn with_no_calls(mut self) -> Self {
57        self.allowed_calls = Some(Vec::new());
58        self
59    }
60
61    /// Returns `true` if calls are unrestricted (no call-scope allowlist set).
62    pub fn is_unrestricted(&self) -> bool {
63        self.allowed_calls.is_none()
64    }
65
66    /// Returns `true` if a call to `target` with the given `input` is allowed.
67    ///
68    /// Checks target, selector, and recipient restrictions in order:
69    /// - No scopes configured → unrestricted, always allowed.
70    /// - Target not in any scope → denied.
71    /// - Scope has no selector rules → any call to that target is allowed.
72    /// - Selector not in rules → denied.
73    /// - Rule has no recipients → any recipient is allowed.
74    /// - Otherwise the first ABI word after the selector must match an allowed recipient.
75    pub fn is_call_allowed(&self, target: &Address, input: &[u8]) -> bool {
76        (|| {
77            let Some(scopes) = &self.allowed_calls else {
78                return Some(true);
79            };
80            let scope = scopes.iter().find(|s| s.target == *target)?;
81
82            if scope.selector_rules.is_empty() {
83                return Some(true);
84            }
85
86            let selector: [u8; 4] = input.get(..4)?.try_into().ok()?;
87            let rule = scope
88                .selector_rules
89                .iter()
90                .find(|r| r.selector == selector)?;
91
92            if rule.recipients.is_empty() {
93                return Some(true);
94            }
95
96            let word: [u8; 32] = input.get(4..36)?.try_into().ok()?;
97            Some(rule.recipients.contains(&Address::from_word(word.into())))
98        })()
99        .unwrap_or(false)
100    }
101
102    /// Returns the expiry timestamp, if one is set.
103    pub fn expiry(&self) -> Option<u64> {
104        self.expiry
105    }
106
107    /// Returns the token spending limits, if any.
108    pub fn limits(&self) -> Option<&[TokenLimit]> {
109        self.limits.as_deref()
110    }
111
112    /// Returns the call scope allowlist, if any.
113    pub fn allowed_calls(&self) -> Option<&[CallScope]> {
114        self.allowed_calls.as_deref()
115    }
116
117    fn has_periodic_limits(&self) -> bool {
118        self.limits
119            .as_ref()
120            .is_some_and(|limits| limits.iter().any(|limit| limit.period != 0))
121    }
122
123    fn has_call_scopes(&self) -> bool {
124        self.allowed_calls.is_some()
125    }
126}
127
128impl From<KeyRestrictions> for AbiKeyRestrictions {
129    fn from(restrictions: KeyRestrictions) -> Self {
130        let KeyRestrictions {
131            expiry,
132            limits,
133            allowed_calls,
134        } = restrictions;
135
136        Self {
137            expiry: expiry.unwrap_or(u64::MAX),
138            enforceLimits: limits.is_some(),
139            limits: limits
140                .unwrap_or_default()
141                .into_iter()
142                .map(|limit| AbiTokenLimit {
143                    token: limit.token,
144                    amount: limit.limit,
145                    period: limit.period,
146                })
147                .collect(),
148            allowAnyCalls: allowed_calls.is_none(),
149            allowedCalls: allowed_calls
150                .unwrap_or_default()
151                .into_iter()
152                .map(Into::into)
153                .collect(),
154        }
155    }
156}
157
158/// Builder for constructing a [`CallScope`] with ergonomic helpers for common TIP-20 selectors.
159///
160/// # Examples
161///
162/// ```ignore
163/// use alloy_primitives::address;
164/// use tempo_alloy::provider::keychain::CallScopeBuilder;
165///
166/// // Allow transfer and approve to any recipient on a specific token
167/// let scope = CallScopeBuilder::new(PATH_USD)
168///     .transfer(vec![])
169///     .approve(vec![])
170///     .build();
171///
172/// // Allow transfer only to a specific recipient
173/// let scope = CallScopeBuilder::new(PATH_USD)
174///     .transfer(vec![address!("0x1111111111111111111111111111111111111111")])
175///     .build();
176///
177/// // Allow an arbitrary selector on a contract
178/// let scope = CallScopeBuilder::new(MY_CONTRACT)
179///     .with_selector([0xaa, 0xbb, 0xcc, 0xdd])
180///     .build();
181/// ```
182#[derive(Clone, Debug)]
183pub struct CallScopeBuilder {
184    target: Address,
185    selector_rules: Vec<SelectorRule>,
186}
187
188impl CallScopeBuilder {
189    /// Create a new builder for the given target contract address.
190    pub fn new(target: Address) -> Self {
191        Self {
192            target,
193            selector_rules: Vec::new(),
194        }
195    }
196
197    /// Allow calls matching an arbitrary 4-byte function selector.
198    pub fn with_selector(mut self, selector: [u8; 4]) -> Self {
199        self.selector_rules.push(SelectorRule {
200            selector,
201            recipients: vec![],
202        });
203        self
204    }
205
206    /// Allow `transfer(address,uint256)` calls, optionally restricted to the given recipients.
207    pub fn transfer(mut self, recipients: Vec<Address>) -> Self {
208        self.selector_rules.push(SelectorRule {
209            selector: ITIP20::transferCall::SELECTOR,
210            recipients,
211        });
212        self
213    }
214
215    /// Allow `transferWithMemo(address,uint256,bytes32)` calls, optionally restricted to the given recipients.
216    pub fn transfer_with_memo(mut self, recipients: Vec<Address>) -> Self {
217        self.selector_rules.push(SelectorRule {
218            selector: ITIP20::transferWithMemoCall::SELECTOR,
219            recipients,
220        });
221        self
222    }
223
224    /// Allow `approve(address,uint256)` calls, optionally restricted to the given spenders.
225    pub fn approve(mut self, recipients: Vec<Address>) -> Self {
226        self.selector_rules.push(SelectorRule {
227            selector: ITIP20::approveCall::SELECTOR,
228            recipients,
229        });
230        self
231    }
232
233    /// Consume the builder and produce a [`CallScope`].
234    pub fn build(self) -> CallScope {
235        CallScope {
236            target: self.target,
237            selector_rules: self.selector_rules,
238        }
239    }
240}
241
242/// Error raised when building AccountKeychain calls with incompatible restrictions.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum KeychainBuildError {
245    /// Legacy authorizeKey cannot encode periodic token limits.
246    LegacyPeriodicLimits,
247    /// Legacy authorizeKey cannot encode call-scope restrictions.
248    LegacyCallScopes,
249}
250
251impl std::error::Error for KeychainBuildError {}
252impl fmt::Display for KeychainBuildError {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        let msg = match self {
255            Self::LegacyPeriodicLimits => {
256                "legacy authorizeKey does not support periodic token limits"
257            }
258            Self::LegacyCallScopes => {
259                "legacy authorizeKey does not support call-scope restrictions"
260            }
261        };
262        write!(f, "{msg}")
263    }
264}
265
266/// Build a pre-T3 `authorizeKey` call.
267pub fn authorize_key_legacy(
268    key_id: Address,
269    signature_type: SignatureType,
270    restrictions: KeyRestrictions,
271) -> Result<Call, KeychainBuildError> {
272    if restrictions.has_call_scopes() {
273        return Err(KeychainBuildError::LegacyCallScopes);
274    }
275    if restrictions.has_periodic_limits() {
276        return Err(KeychainBuildError::LegacyPeriodicLimits);
277    }
278
279    let KeyRestrictions {
280        expiry,
281        limits,
282        allowed_calls: _,
283    } = restrictions;
284    let enforce_limits = limits.is_some();
285    let limits = limits
286        .unwrap_or_default()
287        .into_iter()
288        .map(|limit| AbiLegacyTokenLimit {
289            token: limit.token,
290            amount: limit.limit,
291        })
292        .collect();
293
294    Ok(account_keychain_call(legacyAuthorizeKeyCall {
295        keyId: key_id,
296        signatureType: signature_type.into(),
297        expiry: expiry.unwrap_or(u64::MAX),
298        enforceLimits: enforce_limits,
299        limits,
300    }))
301}
302
303/// Build an `authorizeKey(address,uint8,KeyRestrictions)` precompile call.
304pub fn authorize_key(
305    key_id: Address,
306    signature_type: SignatureType,
307    restrictions: KeyRestrictions,
308) -> Call {
309    account_keychain_call(authorizeKeyCall {
310        keyId: key_id,
311        signatureType: signature_type.into(),
312        config: restrictions.into(),
313    })
314}
315
316/// Build a `revokeKey(address)` precompile call.
317pub fn revoke_key(key_id: Address) -> Call {
318    account_keychain_call(revokeKeyCall { keyId: key_id })
319}
320
321/// Build an `updateSpendingLimit(address,address,uint256)` precompile call.
322pub fn update_spending_limit(key_id: Address, token: Address, new_limit: U256) -> Call {
323    account_keychain_call(updateSpendingLimitCall {
324        keyId: key_id,
325        token,
326        newLimit: new_limit,
327    })
328}
329
330/// Build a `setAllowedCalls(address,CallScope[])` precompile call.
331///
332/// # Examples
333///
334/// ```ignore
335/// use alloy_primitives::address;
336/// use tempo_alloy::provider::keychain::{CallScopeBuilder, set_allowed_calls};
337///
338/// let key_id = address!("0x1111111111111111111111111111111111111111");
339/// let token = address!("0x20c0000000000000000000000000000000000001");
340///
341/// let scope = CallScopeBuilder::new(token)
342///     .transfer(vec![])
343///     .approve(vec![])
344///     .build();
345///
346/// let call = set_allowed_calls(key_id, vec![scope]);
347/// ```
348pub fn set_allowed_calls(key_id: Address, scopes: Vec<CallScope>) -> Call {
349    account_keychain_call(setAllowedCallsCall {
350        keyId: key_id,
351        scopes: scopes.into_iter().map(Into::into).collect(),
352    })
353}
354
355/// Build a `removeAllowedCalls(address,address)` precompile call.
356///
357/// # Examples
358///
359/// ```ignore
360/// use alloy_primitives::address;
361/// use tempo_alloy::provider::keychain::remove_allowed_calls;
362///
363/// let key_id = address!("0x1111111111111111111111111111111111111111");
364/// let token = address!("0x20c0000000000000000000000000000000000001");
365///
366/// // Remove all call-scope rules targeting `token`
367/// let call = remove_allowed_calls(key_id, token);
368/// ```
369pub fn remove_allowed_calls(key_id: Address, target: Address) -> Call {
370    account_keychain_call(removeAllowedCallsCall {
371        keyId: key_id,
372        target,
373    })
374}
375
376fn account_keychain_call(call: impl SolCall) -> Call {
377    Call {
378        to: TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS),
379        value: U256::ZERO,
380        input: Bytes::from(call.abi_encode()),
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use alloy_primitives::{address, uint};
388    use tempo_contracts::precompiles::IAccountKeychain::{
389        CallScope as AbiCallScope, SelectorRule as AbiSelectorRule,
390        SignatureType as AbiSignatureType, removeAllowedCallsCall, revokeKeyCall,
391        setAllowedCallsCall, updateSpendingLimitCall,
392    };
393
394    #[test]
395    fn test_authorize_key_t3_defaults_to_unrestricted_never_expiring() {
396        let call = authorize_key(
397            address!("0x1111111111111111111111111111111111111111"),
398            SignatureType::Secp256k1,
399            KeyRestrictions::default(),
400        );
401
402        let decoded = authorizeKeyCall::abi_decode(&call.input).expect("decode authorizeKey");
403        assert_eq!(
404            decoded.keyId,
405            address!("0x1111111111111111111111111111111111111111")
406        );
407        assert_eq!(decoded.signatureType, AbiSignatureType::Secp256k1);
408        assert_eq!(decoded.config.expiry, u64::MAX);
409        assert!(!decoded.config.enforceLimits);
410        assert!(decoded.config.limits.is_empty());
411        assert!(decoded.config.allowAnyCalls);
412        assert!(decoded.config.allowedCalls.is_empty());
413    }
414
415    #[test]
416    fn test_authorize_key_t3_preserves_call_scopes() {
417        let restrictions = KeyRestrictions::default()
418            .with_expiry(123)
419            .with_limits(vec![TokenLimit {
420                token: address!("0x20c0000000000000000000000000000000000001"),
421                limit: uint!(42_U256),
422                period: 60,
423            }])
424            .with_allowed_calls(vec![CallScope {
425                target: address!("0x20c0000000000000000000000000000000000002"),
426                selector_rules: vec![SelectorRule {
427                    selector: [0xaa, 0xbb, 0xcc, 0xdd],
428                    recipients: vec![address!("0x3333333333333333333333333333333333333333")],
429                }],
430            }]);
431
432        let call = authorize_key(
433            address!("0x1111111111111111111111111111111111111111"),
434            SignatureType::P256,
435            restrictions,
436        );
437
438        let decoded = authorizeKeyCall::abi_decode(&call.input).expect("decode authorizeKey");
439        assert_eq!(decoded.signatureType, AbiSignatureType::P256);
440        assert_eq!(decoded.config.expiry, 123);
441        assert!(decoded.config.enforceLimits);
442        assert_eq!(decoded.config.limits.len(), 1);
443        assert!(!decoded.config.allowAnyCalls);
444        assert_eq!(decoded.config.allowedCalls.len(), 1);
445        assert_eq!(decoded.config.allowedCalls[0].selectorRules.len(), 1);
446        assert_eq!(
447            decoded.config.allowedCalls[0].selectorRules[0].selector,
448            [0xaa_u8, 0xbb, 0xcc, 0xdd]
449        );
450    }
451
452    #[test]
453    fn test_authorize_key_legacy_rejects_t3_only_restrictions() {
454        let scoped = authorize_key_legacy(
455            address!("0x1111111111111111111111111111111111111111"),
456            SignatureType::Secp256k1,
457            KeyRestrictions::default().with_no_calls(),
458        )
459        .expect_err("legacy ABI should reject call scopes");
460        assert_eq!(scoped, KeychainBuildError::LegacyCallScopes);
461
462        let periodic = authorize_key_legacy(
463            address!("0x1111111111111111111111111111111111111111"),
464            SignatureType::Secp256k1,
465            KeyRestrictions::default().with_limits(vec![TokenLimit {
466                token: address!("0x20c0000000000000000000000000000000000001"),
467                limit: U256::from(1),
468                period: 1,
469            }]),
470        )
471        .expect_err("legacy ABI should reject periodic limits");
472        assert_eq!(periodic, KeychainBuildError::LegacyPeriodicLimits);
473    }
474
475    #[test]
476    fn test_authorize_key_legacy_flattens_limits() {
477        let call = authorize_key_legacy(
478            address!("0x1111111111111111111111111111111111111111"),
479            SignatureType::WebAuthn,
480            KeyRestrictions::default()
481                .with_expiry(999)
482                .with_limits(vec![TokenLimit {
483                    token: address!("0x20c0000000000000000000000000000000000001"),
484                    limit: U256::from(7),
485                    period: 0,
486                }]),
487        )
488        .expect("legacy restrictions are compatible");
489
490        let decoded =
491            legacyAuthorizeKeyCall::abi_decode(&call.input).expect("decode legacy authorizeKey");
492        assert_eq!(decoded.signatureType, AbiSignatureType::WebAuthn);
493        assert_eq!(decoded.expiry, 999);
494        assert!(decoded.enforceLimits);
495        assert_eq!(decoded.limits.len(), 1);
496        assert_eq!(decoded.limits[0].amount, U256::from(7));
497    }
498
499    #[test]
500    fn test_call_scope_builder_tip20_selectors() {
501        let token = address!("0x20c0000000000000000000000000000000000001");
502        let recipient = address!("0x3333333333333333333333333333333333333333");
503
504        let scope = CallScopeBuilder::new(token)
505            .transfer(vec![recipient])
506            .approve(vec![])
507            .build();
508
509        assert_eq!(scope.target, token);
510        assert_eq!(scope.selector_rules.len(), 2);
511        assert_eq!(
512            scope.selector_rules[0].selector,
513            ITIP20::transferCall::SELECTOR
514        );
515        assert_eq!(scope.selector_rules[0].recipients, vec![recipient]);
516
517        assert_eq!(
518            scope.selector_rules[1].selector,
519            ITIP20::approveCall::SELECTOR
520        );
521        assert!(scope.selector_rules[1].recipients.is_empty());
522    }
523
524    #[test]
525    fn test_roundtrip_abi_call_scope_conversion() {
526        let scopes = vec![AbiCallScope {
527            target: address!("0x20c0000000000000000000000000000000000002"),
528            selectorRules: vec![AbiSelectorRule {
529                selector: [0x12, 0x34, 0x56, 0x78].into(),
530                recipients: vec![address!("0x3333333333333333333333333333333333333333")],
531            }],
532        }];
533
534        let primitive: Vec<CallScope> = scopes.clone().into_iter().map(Into::into).collect();
535        let roundtrip: Vec<AbiCallScope> = primitive.into_iter().map(Into::into).collect();
536        assert_eq!(roundtrip, scopes);
537    }
538
539    #[test]
540    fn test_revoke_key_encodes_correctly() {
541        let key_id = address!("0x1111111111111111111111111111111111111111");
542        let call = revoke_key(key_id);
543
544        assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
545        assert_eq!(call.value, U256::ZERO);
546
547        let decoded = revokeKeyCall::abi_decode(&call.input).expect("decode revokeKey");
548        assert_eq!(decoded.keyId, key_id);
549    }
550
551    #[test]
552    fn test_update_spending_limit_encodes_correctly() {
553        let key_id = address!("0x1111111111111111111111111111111111111111");
554        let token = address!("0x2222222222222222222222222222222222222222");
555        let limit = uint!(1000_U256);
556        let call = update_spending_limit(key_id, token, limit);
557
558        assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
559        assert_eq!(call.value, U256::ZERO);
560
561        let decoded =
562            updateSpendingLimitCall::abi_decode(&call.input).expect("decode updateSpendingLimit");
563        assert_eq!(decoded.keyId, key_id);
564        assert_eq!(decoded.token, token);
565        assert_eq!(decoded.newLimit, limit);
566    }
567
568    #[test]
569    fn test_set_allowed_calls_encodes_correctly() {
570        let key_id = address!("0x1111111111111111111111111111111111111111");
571        let scopes = vec![CallScope {
572            target: address!("0x2222222222222222222222222222222222222222"),
573            selector_rules: vec![SelectorRule {
574                selector: [0xaa, 0xbb, 0xcc, 0xdd],
575                recipients: vec![address!("0x3333333333333333333333333333333333333333")],
576            }],
577        }];
578        let call = set_allowed_calls(key_id, scopes);
579
580        assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
581        assert_eq!(call.value, U256::ZERO);
582
583        let decoded = setAllowedCallsCall::abi_decode(&call.input).expect("decode setAllowedCalls");
584        assert_eq!(decoded.keyId, key_id);
585        assert_eq!(decoded.scopes.len(), 1);
586        assert_eq!(decoded.scopes[0].selectorRules.len(), 1);
587    }
588
589    #[test]
590    fn test_is_call_allowed_unrestricted() {
591        let r = KeyRestrictions::default();
592        let target = address!("0x2222222222222222222222222222222222222222");
593        assert!(r.is_call_allowed(&target, &[]));
594        assert!(r.is_call_allowed(&target, &[0xaa, 0xbb, 0xcc, 0xdd]));
595    }
596
597    #[test]
598    fn test_is_call_allowed_empty_scopes_denies_all() {
599        let r = KeyRestrictions::default().with_no_calls();
600        let target = address!("0x2222222222222222222222222222222222222222");
601        assert!(!r.is_call_allowed(&target, &[0xaa, 0xbb, 0xcc, 0xdd]));
602    }
603
604    #[test]
605    fn test_is_call_allowed_target_not_in_scope() {
606        let token = address!("0x20c0000000000000000000000000000000000001");
607        let other = address!("0x3333333333333333333333333333333333333333");
608        let r = KeyRestrictions::default()
609            .with_allowed_calls(vec![CallScopeBuilder::new(token).build()]);
610        assert!(!r.is_call_allowed(&other, &[0xaa, 0xbb, 0xcc, 0xdd]));
611    }
612
613    #[test]
614    fn test_is_call_allowed_no_selector_rules_allows_any_call() {
615        let token = address!("0x20c0000000000000000000000000000000000001");
616        let r = KeyRestrictions::default()
617            .with_allowed_calls(vec![CallScopeBuilder::new(token).build()]);
618        assert!(r.is_call_allowed(&token, &[0xaa, 0xbb, 0xcc, 0xdd]));
619        assert!(r.is_call_allowed(&token, &[]));
620    }
621
622    #[test]
623    fn test_is_call_allowed_selector_match() {
624        let token = address!("0x20c0000000000000000000000000000000000001");
625        let r = KeyRestrictions::default().with_allowed_calls(vec![
626            CallScopeBuilder::new(token)
627                .with_selector([0xaa, 0xbb, 0xcc, 0xdd])
628                .build(),
629        ]);
630
631        assert!(r.is_call_allowed(&token, &[0xaa, 0xbb, 0xcc, 0xdd]));
632        assert!(!r.is_call_allowed(&token, &[0x11, 0x22, 0x33, 0x44]));
633        assert!(!r.is_call_allowed(&token, &[0xaa, 0xbb]));
634    }
635
636    #[test]
637    fn test_is_call_allowed_tip20_transfer_with_recipients() {
638        let token = address!("0x20c0000000000000000000000000000000000001");
639        let allowed = address!("0x4444444444444444444444444444444444444444");
640        let denied = address!("0x5555555555555555555555555555555555555555");
641
642        let r = KeyRestrictions::default().with_allowed_calls(vec![
643            CallScopeBuilder::new(token).transfer(vec![allowed]).build(),
644        ]);
645
646        // Build valid transfer(address,uint256) calldata
647        let mut input = Vec::new();
648        input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
649        // recipient as ABI-encoded address (left-padded to 32 bytes)
650        input.extend_from_slice(&[0u8; 12]);
651        input.extend_from_slice(allowed.as_slice());
652        // amount (unused for scope check, just pad)
653        input.extend_from_slice(&[0u8; 32]);
654
655        assert!(r.is_call_allowed(&token, &input));
656
657        // Same selector but different recipient
658        let mut bad_input = Vec::new();
659        bad_input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
660        bad_input.extend_from_slice(&[0u8; 12]);
661        bad_input.extend_from_slice(denied.as_slice());
662        bad_input.extend_from_slice(&[0u8; 32]);
663
664        assert!(!r.is_call_allowed(&token, &bad_input));
665    }
666
667    #[test]
668    fn test_is_call_allowed_recipient_word_too_short() {
669        let token = address!("0x20c0000000000000000000000000000000000001");
670        let allowed = address!("0x4444444444444444444444444444444444444444");
671        let r = KeyRestrictions::default().with_allowed_calls(vec![
672            CallScopeBuilder::new(token).transfer(vec![allowed]).build(),
673        ]);
674
675        // Selector only, no recipient word
676        let input = ITIP20::transferCall::SELECTOR.to_vec();
677        assert!(!r.is_call_allowed(&token, &input));
678    }
679
680    #[test]
681    fn test_is_call_allowed_no_recipients_allows_any() {
682        let token = address!("0x20c0000000000000000000000000000000000001");
683        let anyone = address!("0x9999999999999999999999999999999999999999");
684
685        let r = KeyRestrictions::default()
686            .with_allowed_calls(vec![CallScopeBuilder::new(token).transfer(vec![]).build()]);
687
688        let mut input = Vec::new();
689        input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
690        input.extend_from_slice(&[0u8; 12]);
691        input.extend_from_slice(anyone.as_slice());
692        input.extend_from_slice(&[0u8; 32]);
693
694        assert!(r.is_call_allowed(&token, &input));
695    }
696
697    #[test]
698    fn test_remove_allowed_calls_encodes_correctly() {
699        let key_id = address!("0x1111111111111111111111111111111111111111");
700        let target = address!("0x2222222222222222222222222222222222222222");
701        let call = remove_allowed_calls(key_id, target);
702
703        assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
704        assert_eq!(call.value, U256::ZERO);
705
706        let decoded =
707            removeAllowedCallsCall::abi_decode(&call.input).expect("decode removeAllowedCalls");
708        assert_eq!(decoded.keyId, key_id);
709        assert_eq!(decoded.target, target);
710    }
711}