hypercall_api/
request_auth.rs1pub 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}