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