Skip to main content

hypercall_api/
request_auth.rs

1pub use hypercall_auth::RequestAuthError;
2use hypercall_auth::{AgentAuthorizer, PurportedWallet, SignedRequest};
3use hypercall_types::WalletAddress;
4
5use crate::middleware::SignerContext;
6use hypercall_runtime_api::AgentAuthProvider;
7
8struct EngineAgentAuthorizer<'a>(&'a dyn AgentAuthProvider);
9
10impl AgentAuthorizer for EngineAgentAuthorizer<'_> {
11    fn is_agent_authorized(
12        &self,
13        wallet: &hypercall_types::WalletAddress,
14        signer: &hypercall_types::WalletAddress,
15    ) -> bool {
16        self.0.is_agent_authorized(wallet, signer)
17    }
18}
19
20#[derive(Debug, Clone)]
21pub struct AuthorizedRequest {
22    pub signer: SignerContext,
23    pub nonce: u64,
24}
25
26pub fn verify_request<R: PurportedWallet + SignedRequest + ?Sized>(
27    agent_auth: &dyn AgentAuthProvider,
28    request: &R,
29    chain_id: u64,
30) -> Result<AuthorizedRequest, RequestAuthError> {
31    let authorizer = EngineAgentAuthorizer(agent_auth);
32    let authorized = hypercall_auth::verify_request(&authorizer, request, chain_id)?;
33    Ok(AuthorizedRequest {
34        signer: SignerContext {
35            wallet_address: authorized.signer.wallet_address,
36            signer_address: authorized.signer.signer_address,
37        },
38        nonce: authorized.nonce,
39    })
40}
41
42pub fn authorize_signer(
43    agent_auth: &dyn AgentAuthProvider,
44    wallet: WalletAddress,
45    signer: WalletAddress,
46) -> Result<SignerContext, RequestAuthError> {
47    let authorizer = EngineAgentAuthorizer(agent_auth);
48    hypercall_auth::authorize_signer(&authorizer, wallet, signer).map(|verified| SignerContext {
49        wallet_address: verified.wallet_address,
50        signer_address: verified.signer_address,
51    })
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use alloy::primitives::Address;
58    use hypercall_auth::SignatureRecoveryResult;
59    use std::collections::HashSet;
60
61    struct StubSignedRequest {
62        purported_wallet: WalletAddress,
63        nonce: u64,
64        recovered_signer: Result<Address, &'static str>,
65    }
66
67    impl PurportedWallet for StubSignedRequest {
68        fn purported_wallet(&self) -> WalletAddress {
69            self.purported_wallet
70        }
71    }
72
73    impl SignedRequest for StubSignedRequest {
74        fn nonce(&self) -> u64 {
75            self.nonce
76        }
77
78        fn signature(&self) -> &str {
79            "0xstub"
80        }
81
82        fn recover_signer(&self, _chain_id: u64) -> SignatureRecoveryResult {
83            self.recovered_signer.map_err(|message| {
84                Box::new(std::io::Error::new(
85                    std::io::ErrorKind::InvalidInput,
86                    message,
87                )) as Box<dyn std::error::Error + Send + Sync>
88            })
89        }
90    }
91
92    struct AllowListAgentAuthProvider {
93        allowed: HashSet<(WalletAddress, WalletAddress)>,
94    }
95
96    impl AgentAuthProvider for AllowListAgentAuthProvider {
97        fn is_agent_authorized(&self, wallet: &WalletAddress, agent: &WalletAddress) -> bool {
98            wallet == agent || self.allowed.contains(&(*wallet, *agent))
99        }
100
101        fn get_authorized_agents(&self, wallet: &WalletAddress) -> Vec<WalletAddress> {
102            self.allowed
103                .iter()
104                .filter_map(|(allowed_wallet, agent)| {
105                    if allowed_wallet == wallet {
106                        Some(*agent)
107                    } else {
108                        None
109                    }
110                })
111                .collect()
112        }
113    }
114
115    #[test]
116    fn authorize_request_checks_recovered_signer_against_purported_wallet() {
117        let wallet = WalletAddress(Address::repeat_byte(0x11));
118        let recovered_agent = WalletAddress(Address::repeat_byte(0x22));
119        let request = StubSignedRequest {
120            purported_wallet: wallet,
121            nonce: 123,
122            recovered_signer: Ok(recovered_agent.inner()),
123        };
124        let agent_auth = AllowListAgentAuthProvider {
125            allowed: HashSet::from([(wallet, recovered_agent)]),
126        };
127
128        let authorized = verify_request(&agent_auth, &request, 998).expect("agent is authorized");
129
130        assert_eq!(authorized.signer.wallet_address, wallet);
131        assert_eq!(authorized.signer.signer_address, recovered_agent);
132        assert_eq!(authorized.nonce, 123);
133    }
134
135    #[test]
136    fn authorize_request_rejects_recovered_signer_not_authorized_for_purported_wallet() {
137        let wallet = WalletAddress(Address::repeat_byte(0x11));
138        let recovered_signer = WalletAddress(Address::repeat_byte(0x22));
139        let request = StubSignedRequest {
140            purported_wallet: wallet,
141            nonce: 123,
142            recovered_signer: Ok(recovered_signer.inner()),
143        };
144        let agent_auth = AllowListAgentAuthProvider {
145            allowed: HashSet::new(),
146        };
147
148        let error = verify_request(&agent_auth, &request, 998).expect_err("agent is unauthorized");
149
150        assert!(matches!(
151            error,
152            RequestAuthError::Unauthorized { wallet: w, signer: s }
153                if w == wallet && s == recovered_signer
154        ));
155    }
156
157    #[test]
158    fn authorize_request_maps_recovery_errors_to_signature_errors() {
159        let request = StubSignedRequest {
160            purported_wallet: WalletAddress(Address::repeat_byte(0x11)),
161            nonce: 123,
162            recovered_signer: Err("bad signature"),
163        };
164        let agent_auth = AllowListAgentAuthProvider {
165            allowed: HashSet::new(),
166        };
167
168        let error = verify_request(&agent_auth, &request, 998).expect_err("signature should fail");
169
170        assert!(
171            matches!(error, RequestAuthError::Signature(message) if message == "bad signature")
172        );
173    }
174}