use crate::error::RelayError;
use alloy::primitives::{address, Address};
use polyoxide_core::{current_timestamp, Base64Format, Signer};
use reqwest::header::{HeaderMap, HeaderValue};
#[derive(Clone, Debug)]
pub struct ContractConfig {
pub safe_factory: Address,
pub safe_multisend: Address,
pub proxy_factory: Option<Address>,
pub relay_hub: Option<Address>,
pub rpc_url: &'static str,
}
pub fn get_contract_config(chain_id: u64) -> Option<ContractConfig> {
match chain_id {
137 => Some(ContractConfig {
safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
proxy_factory: Some(address!("aB45c5A4B0c941a2F231C04C3f49182e1A254052")),
relay_hub: Some(address!("D216153c06E857cD7f72665E0aF1d7D82172F494")),
rpc_url: "https://polygon.drpc.org",
}),
80002 => Some(ContractConfig {
safe_factory: address!("aacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
safe_multisend: address!("A238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"),
proxy_factory: None, relay_hub: None,
rpc_url: "https://rpc-amoy.polygon.technology",
}),
_ => None,
}
}
#[derive(Clone)]
pub struct BuilderConfig {
pub key: String,
pub secret: String,
pub passphrase: Option<String>,
}
impl std::fmt::Debug for BuilderConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuilderConfig")
.field("key", &"[REDACTED]")
.field("secret", &"[REDACTED]")
.field(
"passphrase",
&self.passphrase.as_ref().map(|_| "[REDACTED]"),
)
.finish()
}
}
impl BuilderConfig {
pub fn new(key: String, secret: String, passphrase: Option<String>) -> Self {
Self {
key,
secret,
passphrase,
}
}
pub fn generate_headers(
&self,
method: &str,
path: &str,
body: Option<&str>,
) -> Result<HeaderMap, String> {
let mut headers = HeaderMap::new();
let timestamp = current_timestamp();
let signer = Signer::from_raw(&self.secret);
let message = Signer::create_message(timestamp, method, path, body);
let signature = signer.sign(&message, Base64Format::Standard)?;
headers.insert(
"POLY-API-KEY",
HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
);
headers.insert(
"POLY-TIMESTAMP",
HeaderValue::from_str(×tamp.to_string()).map_err(|e| e.to_string())?,
);
headers.insert(
"POLY-SIGNATURE",
HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
);
if let Some(passphrase) = &self.passphrase {
headers.insert(
"POLY-PASSPHRASE",
HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
);
}
Ok(headers)
}
pub fn generate_relayer_v2_headers(
&self,
method: &str,
path: &str,
body: Option<&str>,
) -> Result<HeaderMap, String> {
let mut headers = HeaderMap::new();
let timestamp = current_timestamp();
let signer = Signer::new(&self.secret);
let message = Signer::create_message(timestamp, method, path, body);
let signature = signer.sign(&message, Base64Format::UrlSafe)?;
headers.insert(
"POLY_BUILDER_API_KEY",
HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
);
headers.insert(
"POLY_BUILDER_TIMESTAMP",
HeaderValue::from_str(×tamp.to_string()).map_err(|e| e.to_string())?,
);
headers.insert(
"POLY_BUILDER_SIGNATURE",
HeaderValue::from_str(&signature).map_err(|e| e.to_string())?,
);
if let Some(passphrase) = &self.passphrase {
headers.insert(
"POLY_BUILDER_PASSPHRASE",
HeaderValue::from_str(passphrase).map_err(|e| e.to_string())?,
);
}
Ok(headers)
}
}
#[derive(Clone)]
pub struct RelayerApiKeyConfig {
key: String,
address: String,
}
impl std::fmt::Debug for RelayerApiKeyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RelayerApiKeyConfig")
.field("key", &"[REDACTED]")
.field("address", &self.address)
.finish()
}
}
impl RelayerApiKeyConfig {
pub fn new(key: String, address: String) -> Result<Self, RelayError> {
if key.trim().is_empty() {
return Err(RelayError::Api(
"RelayerApiKeyConfig: key must not be empty or whitespace".to_string(),
));
}
if address.trim().is_empty() {
return Err(RelayError::Api(
"RelayerApiKeyConfig: address must not be empty or whitespace".to_string(),
));
}
Ok(Self { key, address })
}
pub fn key(&self) -> &str {
&self.key
}
pub fn address(&self) -> &str {
&self.address
}
pub fn generate_headers(&self) -> Result<HeaderMap, String> {
let mut headers = HeaderMap::new();
headers.insert(
"RELAYER_API_KEY",
HeaderValue::from_str(&self.key).map_err(|e| e.to_string())?,
);
headers.insert(
"RELAYER_API_KEY_ADDRESS",
HeaderValue::from_str(&self.address).map_err(|e| e.to_string())?,
);
Ok(headers)
}
}
#[derive(Clone, Debug)]
pub enum AuthConfig {
Builder(BuilderConfig),
RelayerApiKey(RelayerApiKeyConfig),
}
impl AuthConfig {
pub fn generate_relayer_v2_headers(
&self,
method: &str,
path: &str,
body: Option<&str>,
) -> Result<HeaderMap, String> {
match self {
AuthConfig::Builder(config) => config.generate_relayer_v2_headers(method, path, body),
AuthConfig::RelayerApiKey(config) => config.generate_headers(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_config_debug_redacts_secrets() {
let config = BuilderConfig::new(
"my-api-key".to_string(),
"my-secret".to_string(),
Some("my-passphrase".to_string()),
);
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("[REDACTED]"));
assert!(
!debug_output.contains("my-api-key"),
"Debug leaked API key: {}",
debug_output
);
assert!(
!debug_output.contains("my-secret"),
"Debug leaked secret: {}",
debug_output
);
assert!(
!debug_output.contains("my-passphrase"),
"Debug leaked passphrase: {}",
debug_output
);
}
#[test]
fn test_builder_config_debug_without_passphrase() {
let config = BuilderConfig::new("key".to_string(), "secret".to_string(), None);
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("[REDACTED]"));
assert!(debug_output.contains("passphrase: None"));
}
#[test]
fn test_relayer_api_key_generates_correct_headers() {
let config =
RelayerApiKeyConfig::new("my-relayer-key".to_string(), "0xabc123".to_string()).unwrap();
let headers = config.generate_headers().unwrap();
assert_eq!(
headers.get("RELAYER_API_KEY").unwrap().to_str().unwrap(),
"my-relayer-key"
);
assert_eq!(
headers
.get("RELAYER_API_KEY_ADDRESS")
.unwrap()
.to_str()
.unwrap(),
"0xabc123"
);
assert_eq!(headers.len(), 2);
}
#[test]
fn test_relayer_api_key_debug_redacts_secrets() {
let config =
RelayerApiKeyConfig::new("my-relayer-key".to_string(), "0xabc123".to_string()).unwrap();
let debug_output = format!("{:?}", config);
assert!(debug_output.contains("[REDACTED]"));
assert!(
!debug_output.contains("my-relayer-key"),
"Debug leaked API key: {debug_output}"
);
}
#[test]
fn test_auth_config_builder_delegates_correctly() {
let builder = BuilderConfig::new(
"key".to_string(),
"c2VjcmV0".to_string(),
Some("pass".to_string()),
);
let auth = AuthConfig::Builder(builder);
let headers = auth
.generate_relayer_v2_headers("POST", "/submit", Some("{}"))
.unwrap();
assert!(headers.get("POLY_BUILDER_API_KEY").is_some());
assert!(headers.get("RELAYER_API_KEY").is_none());
}
#[test]
fn test_auth_config_relayer_api_key_delegates_correctly() {
let relayer = RelayerApiKeyConfig::new("rk".to_string(), "0xaddr".to_string()).unwrap();
let auth = AuthConfig::RelayerApiKey(relayer);
let headers = auth
.generate_relayer_v2_headers("POST", "/submit", Some("{}"))
.unwrap();
assert!(headers.get("RELAYER_API_KEY").is_some());
assert!(headers.get("POLY_BUILDER_API_KEY").is_none());
}
#[test]
fn test_relayer_api_key_new_rejects_empty_key() {
let result = RelayerApiKeyConfig::new(String::new(), "0xaddr".to_string());
assert!(result.is_err());
}
#[test]
fn test_relayer_api_key_new_rejects_whitespace_key() {
let result = RelayerApiKeyConfig::new(" ".to_string(), "0xaddr".to_string());
assert!(result.is_err());
}
#[test]
fn test_relayer_api_key_new_rejects_empty_address() {
let result = RelayerApiKeyConfig::new("key".to_string(), String::new());
assert!(result.is_err());
}
#[test]
fn test_relayer_api_key_new_rejects_whitespace_address() {
let result = RelayerApiKeyConfig::new("key".to_string(), "\t\n".to_string());
assert!(result.is_err());
}
#[test]
fn test_relayer_api_key_generate_headers_rejects_invalid_header_value() {
let config = RelayerApiKeyConfig {
key: "bad\nkey".to_string(),
address: "0xaddr".to_string(),
};
let result = config.generate_headers();
assert!(result.is_err());
}
#[test]
fn test_relayer_api_key_headers_parameter_independent() {
let relayer = RelayerApiKeyConfig::new("rk".to_string(), "0xaddr".to_string()).unwrap();
let auth = AuthConfig::RelayerApiKey(relayer);
let h1 = auth
.generate_relayer_v2_headers("POST", "/submit", Some("{}"))
.unwrap();
let h2 = auth
.generate_relayer_v2_headers("GET", "/other/path", None)
.unwrap();
let h3 = auth
.generate_relayer_v2_headers("PUT", "/yet-another", Some("{\"a\":1}"))
.unwrap();
assert_eq!(h1, h2);
assert_eq!(h2, h3);
}
}