1use crate::error::RelayError;
2use alloy::primitives::{address, Address};
3use polyoxide_core::{current_timestamp, Base64Format, Signer};
4use reqwest::header::{HeaderMap, HeaderValue};
5
6#[derive(Clone, Debug)]
8pub struct ContractConfig {
9 pub safe_factory: Address,
10 pub safe_multisend: Address,
11 pub proxy_factory: Option<Address>,
12 pub relay_hub: Option<Address>,
13 pub rpc_url: &'static str,
14}
15
16pub fn get_contract_config(chain_id: u64) -> Option<ContractConfig> {
20 match chain_id {
21 137 => Some(ContractConfig {
22 safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
23 safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
24 proxy_factory: Some(address!("aB45c5A4B0c941a2F231C04C3f49182e1A254052")),
25 relay_hub: Some(address!("D216153c06E857cD7f72665E0aF1d7D82172F494")),
26 rpc_url: "https://polygon.drpc.org",
27 }),
28 80002 => Some(ContractConfig {
29 safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
30 safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
31 proxy_factory: None, relay_hub: None,
33 rpc_url: "https://rpc-amoy.polygon.technology",
34 }),
35 _ => None,
36 }
37}
38
39#[derive(Clone)]
44pub struct BuilderConfig {
45 pub key: String,
46 pub secret: String,
47 pub passphrase: Option<String>,
48}
49
50impl std::fmt::Debug for BuilderConfig {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("BuilderConfig")
53 .field("key", &"[REDACTED]")
54 .field("secret", &"[REDACTED]")
55 .field(
56 "passphrase",
57 &self.passphrase.as_ref().map(|_| "[REDACTED]"),
58 )
59 .finish()
60 }
61}
62
63impl BuilderConfig {
64 pub fn new(key: String, secret: String, passphrase: Option<String>) -> Self {
66 Self {
67 key,
68 secret,
69 passphrase,
70 }
71 }
72
73 pub fn generate_headers(
77 &self,
78 method: &str,
79 path: &str,
80 body: Option<&str>,
81 ) -> Result<HeaderMap, String> {
82 let mut headers = HeaderMap::new();
83 let timestamp = current_timestamp();
84
85 let signer = Signer::from_raw(&self.secret);
87 let message = Signer::create_message(timestamp, method, path, body);
88 let signature = signer.sign(&message, Base64Format::Standard)?;
89
90 headers.insert(
91 "POLY-API-KEY",
92 HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
93 );
94 headers.insert(
95 "POLY-TIMESTAMP",
96 HeaderValue::from_str(×tamp.to_string()).map_err(|e| e.to_string())?,
97 );
98 headers.insert(
99 "POLY-SIGNATURE",
100 HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
101 );
102
103 if let Some(passphrase) = &self.passphrase {
104 headers.insert(
105 "POLY-PASSPHRASE",
106 HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
107 );
108 }
109
110 Ok(headers)
111 }
112
113 pub fn generate_relayer_v2_headers(
117 &self,
118 method: &str,
119 path: &str,
120 body: Option<&str>,
121 ) -> Result<HeaderMap, String> {
122 let mut headers = HeaderMap::new();
123 let timestamp = current_timestamp();
124
125 let signer = Signer::new(&self.secret);
127 let message = Signer::create_message(timestamp, method, path, body);
128 let signature = signer.sign(&message, Base64Format::UrlSafe)?;
129
130 headers.insert(
131 "POLY_BUILDER_API_KEY",
132 HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
133 );
134 headers.insert(
135 "POLY_BUILDER_TIMESTAMP",
136 HeaderValue::from_str(×tamp.to_string()).map_err(|e| e.to_string())?,
137 );
138 headers.insert(
139 "POLY_BUILDER_SIGNATURE",
140 HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
141 );
142
143 if let Some(passphrase) = &self.passphrase {
144 headers.insert(
145 "POLY_BUILDER_PASSPHRASE",
146 HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
147 );
148 }
149
150 Ok(headers)
151 }
152}
153
154#[derive(Clone)]
163pub struct RelayerApiKeyConfig {
164 key: String,
165 address: String,
166}
167
168impl std::fmt::Debug for RelayerApiKeyConfig {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 f.debug_struct("RelayerApiKeyConfig")
171 .field("key", &"[REDACTED]")
172 .field("address", &self.address)
173 .finish()
174 }
175}
176
177impl RelayerApiKeyConfig {
178 pub fn new(key: String, address: String) -> Result<Self, RelayError> {
182 if key.trim().is_empty() {
183 return Err(RelayError::Api(
184 "RelayerApiKeyConfig: key must not be empty or whitespace".to_string(),
185 ));
186 }
187 if address.trim().is_empty() {
188 return Err(RelayError::Api(
189 "RelayerApiKeyConfig: address must not be empty or whitespace".to_string(),
190 ));
191 }
192 Ok(Self { key, address })
193 }
194
195 pub fn key(&self) -> &str {
197 &self.key
198 }
199
200 pub fn address(&self) -> &str {
202 &self.address
203 }
204
205 pub fn generate_headers(&self) -> Result<HeaderMap, String> {
207 let mut headers = HeaderMap::new();
208 headers.insert(
209 "RELAYER_API_KEY",
210 HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
211 );
212 headers.insert(
213 "RELAYER_API_KEY_ADDRESS",
214 HeaderValue::from_str(&self.address).map_err(|e| e.to_string())?,
215 );
216 Ok(headers)
217 }
218}
219
220#[derive(Clone, Debug)]
226pub enum AuthConfig {
227 Builder(BuilderConfig),
229 RelayerApiKey(RelayerApiKeyConfig),
231}
232
233impl AuthConfig {
234 pub fn generate_relayer_v2_headers(
240 &self,
241 method: &str,
242 path: &str,
243 body: Option<&str>,
244 ) -> Result<HeaderMap, String> {
245 match self {
246 AuthConfig::Builder(config) => config.generate_relayer_v2_headers(method, path, body),
247 AuthConfig::RelayerApiKey(config) => config.generate_headers(),
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_builder_config_debug_redacts_secrets() {
258 let config = BuilderConfig::new(
259 "my-api-key".to_string(),
260 "my-secret".to_string(),
261 Some("my-passphrase".to_string()),
262 );
263 let debug_output = format!("{:?}", config);
264
265 assert!(debug_output.contains("[REDACTED]"));
266 assert!(
267 !debug_output.contains("my-api-key"),
268 "Debug leaked API key: {}",
269 debug_output
270 );
271 assert!(
272 !debug_output.contains("my-secret"),
273 "Debug leaked secret: {}",
274 debug_output
275 );
276 assert!(
277 !debug_output.contains("my-passphrase"),
278 "Debug leaked passphrase: {}",
279 debug_output
280 );
281 }
282
283 #[test]
284 fn test_builder_config_debug_without_passphrase() {
285 let config = BuilderConfig::new("key".to_string(), "secret".to_string(), None);
286 let debug_output = format!("{:?}", config);
287
288 assert!(debug_output.contains("[REDACTED]"));
289 assert!(debug_output.contains("passphrase: None"));
290 }
291
292 #[test]
293 fn test_relayer_api_key_generates_correct_headers() {
294 let config =
295 RelayerApiKeyConfig::new("my-relayer-key".to_string(), "0xabc123".to_string()).unwrap();
296 let headers = config.generate_headers().unwrap();
297 assert_eq!(
298 headers.get("RELAYER_API_KEY").unwrap().to_str().unwrap(),
299 "my-relayer-key"
300 );
301 assert_eq!(
302 headers
303 .get("RELAYER_API_KEY_ADDRESS")
304 .unwrap()
305 .to_str()
306 .unwrap(),
307 "0xabc123"
308 );
309 assert_eq!(headers.len(), 2);
310 }
311
312 #[test]
313 fn test_relayer_api_key_debug_redacts_secrets() {
314 let config =
315 RelayerApiKeyConfig::new("my-relayer-key".to_string(), "0xabc123".to_string()).unwrap();
316 let debug_output = format!("{:?}", config);
317 assert!(debug_output.contains("[REDACTED]"));
318 assert!(
319 !debug_output.contains("my-relayer-key"),
320 "Debug leaked API key: {debug_output}"
321 );
322 }
323
324 #[test]
325 fn test_auth_config_builder_delegates_correctly() {
326 let builder = BuilderConfig::new(
327 "key".to_string(),
328 "c2VjcmV0".to_string(),
329 Some("pass".to_string()),
330 );
331 let auth = AuthConfig::Builder(builder);
332 let headers = auth
333 .generate_relayer_v2_headers("POST", "/submit", Some("{}"))
334 .unwrap();
335 assert!(headers.get("POLY_BUILDER_API_KEY").is_some());
336 assert!(headers.get("RELAYER_API_KEY").is_none());
337 }
338
339 #[test]
340 fn test_auth_config_relayer_api_key_delegates_correctly() {
341 let relayer = RelayerApiKeyConfig::new("rk".to_string(), "0xaddr".to_string()).unwrap();
342 let auth = AuthConfig::RelayerApiKey(relayer);
343 let headers = auth
344 .generate_relayer_v2_headers("POST", "/submit", Some("{}"))
345 .unwrap();
346 assert!(headers.get("RELAYER_API_KEY").is_some());
347 assert!(headers.get("POLY_BUILDER_API_KEY").is_none());
348 }
349
350 #[test]
351 fn test_relayer_api_key_new_rejects_empty_key() {
352 let result = RelayerApiKeyConfig::new(String::new(), "0xaddr".to_string());
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_relayer_api_key_new_rejects_whitespace_key() {
358 let result = RelayerApiKeyConfig::new(" ".to_string(), "0xaddr".to_string());
359 assert!(result.is_err());
360 }
361
362 #[test]
363 fn test_relayer_api_key_new_rejects_empty_address() {
364 let result = RelayerApiKeyConfig::new("key".to_string(), String::new());
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_relayer_api_key_new_rejects_whitespace_address() {
370 let result = RelayerApiKeyConfig::new("key".to_string(), "\t\n".to_string());
371 assert!(result.is_err());
372 }
373
374 #[test]
375 fn test_relayer_api_key_generate_headers_rejects_invalid_header_value() {
376 let config = RelayerApiKeyConfig {
378 key: "bad\nkey".to_string(),
379 address: "0xaddr".to_string(),
380 };
381 let result = config.generate_headers();
382 assert!(result.is_err());
383 }
384
385 #[test]
386 fn test_relayer_api_key_headers_parameter_independent() {
387 let relayer = RelayerApiKeyConfig::new("rk".to_string(), "0xaddr".to_string()).unwrap();
388 let auth = AuthConfig::RelayerApiKey(relayer);
389
390 let h1 = auth
391 .generate_relayer_v2_headers("POST", "/submit", Some("{}"))
392 .unwrap();
393 let h2 = auth
394 .generate_relayer_v2_headers("GET", "/other/path", None)
395 .unwrap();
396 let h3 = auth
397 .generate_relayer_v2_headers("PUT", "/yet-another", Some("{\"a\":1}"))
398 .unwrap();
399
400 assert_eq!(h1, h2);
401 assert_eq!(h2, h3);
402 }
403}