Skip to main content

polyoxide_relay/
config.rs

1use crate::error::RelayError;
2use alloy::primitives::{address, Address};
3use polyoxide_core::{current_timestamp, Base64Format, Signer};
4use reqwest::header::{HeaderMap, HeaderValue};
5
6/// On-chain contract addresses and RPC configuration for a specific chain.
7#[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
16/// Returns contract addresses for a supported chain, or `None` for unknown chain IDs.
17///
18/// Supported chains: Polygon mainnet (137), Amoy testnet (80002).
19pub 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, // Proxy not supported on Amoy testnet
32            relay_hub: None,
33            rpc_url: "https://rpc-amoy.polygon.technology",
34        }),
35        _ => None,
36    }
37}
38
39/// API credentials for authenticating relay requests.
40///
41/// The `Debug` implementation redacts all secret fields to prevent accidental
42/// leakage in logs.
43#[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    /// Create a new builder config with the given API credentials.
65    pub fn new(key: String, secret: String, passphrase: Option<String>) -> Self {
66        Self {
67            key,
68            secret,
69            passphrase,
70        }
71    }
72
73    /// Generate HMAC-authenticated headers for Relay v1 requests.
74    ///
75    /// Uses the raw secret string for HMAC signing with standard base64 output.
76    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        // Create signer from raw string secret (Relay v1 uses raw secrets)
86        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(&timestamp.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    /// Generate HMAC-authenticated headers for Relay v2 requests.
114    ///
115    /// Uses base64-decoded secret for HMAC signing with URL-safe base64 output.
116    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        // Create signer from base64-encoded secret (Relay v2 uses base64 secrets)
126        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(&timestamp.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/// Relayer API Key credentials for authenticated relay requests.
155///
156/// A simpler alternative to [`BuilderConfig`] that uses static headers
157/// instead of HMAC-signed requests. See
158/// <https://docs.polymarket.com/trading/gasless#using-relayer-api-keys>.
159///
160/// The `Debug` implementation redacts all secret fields to prevent accidental
161/// leakage in logs.
162#[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    /// Create a new relayer API key config.
179    ///
180    /// Returns an error if `key` or `address` is empty or whitespace-only.
181    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    /// Returns the relayer API key.
196    pub fn key(&self) -> &str {
197        &self.key
198    }
199
200    /// Returns the on-chain address associated with the relayer API key.
201    pub fn address(&self) -> &str {
202        &self.address
203    }
204
205    /// Generate static authentication headers for relayer API key requests.
206    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/// Authentication configuration for relay requests.
221///
222/// Two authentication schemes are supported:
223/// - [`Builder`](AuthConfig::Builder) — HMAC-SHA256 signed headers (builder API credentials)
224/// - [`RelayerApiKey`](AuthConfig::RelayerApiKey) — static headers (relayer API key)
225#[derive(Clone, Debug)]
226pub enum AuthConfig {
227    /// HMAC-authenticated builder API credentials.
228    Builder(BuilderConfig),
229    /// Static relayer API key headers.
230    RelayerApiKey(RelayerApiKeyConfig),
231}
232
233impl AuthConfig {
234    /// Generate authentication headers for Relay v2 requests.
235    ///
236    /// For [`Builder`](AuthConfig::Builder), this produces HMAC-signed headers.
237    /// For [`RelayerApiKey`](AuthConfig::RelayerApiKey), this produces static headers
238    /// (the `method`, `path`, and `body` parameters are ignored).
239    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        // A newline in a header value is not legal; HeaderValue::from_str must reject it.
377        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}