Skip to main content

polyoxide_relay/
config.rs

1use alloy::primitives::{address, Address};
2use polyoxide_core::{current_timestamp, Base64Format, Signer};
3use reqwest::header::{HeaderMap, HeaderValue};
4
5#[derive(Clone, Debug)]
6pub struct ContractConfig {
7    pub safe_factory: Address,
8    pub safe_multisend: Address,
9    pub proxy_factory: Option<Address>,
10    pub relay_hub: Option<Address>,
11    pub rpc_url: &'static str,
12}
13
14pub fn get_contract_config(chain_id: u64) -> Option<ContractConfig> {
15    match chain_id {
16        137 => Some(ContractConfig {
17            safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
18            safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
19            proxy_factory: Some(address!("aB45c5A4B0c941a2F231C04C3f49182e1A254052")),
20            relay_hub: Some(address!("D216153c06E857cD7f72665E0aF1d7D82172F494")),
21            rpc_url: "https://polygon.drpc.org",
22        }),
23        80002 => Some(ContractConfig {
24            safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
25            safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
26            proxy_factory: None, // Proxy not supported on Amoy testnet
27            relay_hub: None,
28            rpc_url: "https://rpc-amoy.polygon.technology",
29        }),
30        _ => None,
31    }
32}
33
34#[derive(Clone)]
35pub struct BuilderConfig {
36    pub key: String,
37    pub secret: String,
38    pub passphrase: Option<String>,
39}
40
41impl std::fmt::Debug for BuilderConfig {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("BuilderConfig")
44            .field("key", &"[REDACTED]")
45            .field("secret", &"[REDACTED]")
46            .field(
47                "passphrase",
48                &self.passphrase.as_ref().map(|_| "[REDACTED]"),
49            )
50            .finish()
51    }
52}
53
54impl BuilderConfig {
55    pub fn new(key: String, secret: String, passphrase: Option<String>) -> Self {
56        Self {
57            key,
58            secret,
59            passphrase,
60        }
61    }
62
63    pub fn generate_headers(
64        &self,
65        method: &str,
66        path: &str,
67        body: Option<&str>,
68    ) -> Result<HeaderMap, String> {
69        let mut headers = HeaderMap::new();
70        let timestamp = current_timestamp();
71
72        // Create signer from raw string secret (Relay v1 uses raw secrets)
73        let signer = Signer::from_raw(&self.secret);
74        let message = Signer::create_message(timestamp, method, path, body);
75        let signature = signer.sign(&message, Base64Format::Standard)?;
76
77        headers.insert(
78            "POLY-API-KEY",
79            HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
80        );
81        headers.insert(
82            "POLY-TIMESTAMP",
83            HeaderValue::from_str(&timestamp.to_string()).map_err(|e| e.to_string())?,
84        );
85        headers.insert(
86            "POLY-SIGNATURE",
87            HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
88        );
89
90        if let Some(passphrase) = &self.passphrase {
91            headers.insert(
92                "POLY-PASSPHRASE",
93                HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
94            );
95        }
96
97        Ok(headers)
98    }
99
100    pub fn generate_relayer_v2_headers(
101        &self,
102        method: &str,
103        path: &str,
104        body: Option<&str>,
105    ) -> Result<HeaderMap, String> {
106        let mut headers = HeaderMap::new();
107        let timestamp = current_timestamp();
108
109        // Create signer from base64-encoded secret (Relay v2 uses base64 secrets)
110        let signer = Signer::new(&self.secret);
111        let message = Signer::create_message(timestamp, method, path, body);
112        let signature = signer.sign(&message, Base64Format::UrlSafe)?;
113
114        headers.insert(
115            "POLY_BUILDER_API_KEY",
116            HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
117        );
118        headers.insert(
119            "POLY_BUILDER_TIMESTAMP",
120            HeaderValue::from_str(&timestamp.to_string()).map_err(|e| e.to_string())?,
121        );
122        headers.insert(
123            "POLY_BUILDER_SIGNATURE",
124            HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
125        );
126
127        if let Some(passphrase) = &self.passphrase {
128            headers.insert(
129                "POLY_BUILDER_PASSPHRASE",
130                HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
131            );
132        }
133
134        Ok(headers)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_builder_config_debug_redacts_secrets() {
144        let config = BuilderConfig::new(
145            "my-api-key".to_string(),
146            "my-secret".to_string(),
147            Some("my-passphrase".to_string()),
148        );
149        let debug_output = format!("{:?}", config);
150
151        assert!(debug_output.contains("[REDACTED]"));
152        assert!(
153            !debug_output.contains("my-api-key"),
154            "Debug leaked API key: {}",
155            debug_output
156        );
157        assert!(
158            !debug_output.contains("my-secret"),
159            "Debug leaked secret: {}",
160            debug_output
161        );
162        assert!(
163            !debug_output.contains("my-passphrase"),
164            "Debug leaked passphrase: {}",
165            debug_output
166        );
167    }
168
169    #[test]
170    fn test_builder_config_debug_without_passphrase() {
171        let config = BuilderConfig::new("key".to_string(), "secret".to_string(), None);
172        let debug_output = format!("{:?}", config);
173
174        assert!(debug_output.contains("[REDACTED]"));
175        assert!(debug_output.contains("passphrase: None"));
176    }
177}