use crate::config::{AuthConfig, BuilderConfig, RelayerApiKeyConfig};
use crate::error::RelayError;
use alloy::primitives::Address;
use alloy::signers::local::PrivateKeySigner;
#[derive(Clone)]
pub struct BuilderAccount {
pub(crate) signer: PrivateKeySigner,
pub(crate) config: Option<AuthConfig>,
}
fn parse_signer(private_key: impl Into<String>) -> Result<PrivateKeySigner, RelayError> {
private_key
.into()
.parse::<PrivateKeySigner>()
.map_err(|e| RelayError::Signer(format!("Failed to parse private key: {}", e)))
}
impl std::fmt::Debug for BuilderAccount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuilderAccount")
.field("address", &self.signer.address())
.field("config", &self.config)
.finish()
}
}
impl BuilderAccount {
pub fn new(
private_key: impl Into<String>,
config: Option<BuilderConfig>,
) -> Result<Self, RelayError> {
let signer = parse_signer(private_key)?;
Ok(Self {
signer,
config: config.map(AuthConfig::Builder),
})
}
pub fn with_relayer_api_key(
private_key: impl Into<String>,
key: String,
address: String,
) -> Result<Self, RelayError> {
let signer = parse_signer(private_key)?;
let relayer = RelayerApiKeyConfig::new(key, address)?;
Ok(Self {
signer,
config: Some(AuthConfig::RelayerApiKey(relayer)),
})
}
pub fn with_auth_config(
private_key: impl Into<String>,
config: Option<AuthConfig>,
) -> Result<Self, RelayError> {
let signer = parse_signer(private_key)?;
Ok(Self { signer, config })
}
pub fn address(&self) -> Address {
self.signer.address()
}
pub fn signer(&self) -> &PrivateKeySigner {
&self.signer
}
pub fn auth_config(&self) -> Option<&AuthConfig> {
self.config.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AuthConfig;
const TEST_PRIVATE_KEY: &str =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
#[test]
fn test_new_valid_private_key() {
let account = BuilderAccount::new(TEST_PRIVATE_KEY, None);
assert!(account.is_ok());
}
#[test]
fn test_new_with_0x_prefix() {
let key = format!("0x{}", TEST_PRIVATE_KEY);
let account = BuilderAccount::new(key, None);
assert!(account.is_ok());
}
#[test]
fn test_new_invalid_private_key() {
let result = BuilderAccount::new("not_a_valid_key", None);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
RelayError::Signer(msg) => {
assert!(
msg.contains("Failed to parse private key"),
"unexpected: {msg}"
);
}
other => panic!("Expected Signer error, got: {other:?}"),
}
}
#[test]
fn test_new_empty_key() {
let result = BuilderAccount::new("", None);
assert!(result.is_err());
}
#[test]
fn test_address_derivation_deterministic() {
let a1 = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
let a2 = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
assert_eq!(a1.address(), a2.address());
}
#[test]
fn test_address_matches_known_value() {
let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
.parse()
.unwrap();
assert_eq!(account.address(), expected);
}
#[test]
fn test_debug_redacts_private_key() {
let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
let debug_output = format!("{:?}", account);
assert!(
debug_output.contains("address"),
"Debug should show address, got: {debug_output}"
);
assert!(
!debug_output.contains(TEST_PRIVATE_KEY),
"Debug should not contain the private key, got: {debug_output}"
);
}
#[test]
fn test_config_none() {
let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
assert!(account.auth_config().is_none());
}
#[test]
fn test_config_some() {
let config = BuilderConfig::new("key".into(), "secret".into(), None);
let account = BuilderAccount::new(TEST_PRIVATE_KEY, Some(config)).unwrap();
assert!(account.auth_config().is_some());
}
#[test]
fn test_with_relayer_api_key() {
let account = BuilderAccount::with_relayer_api_key(
TEST_PRIVATE_KEY,
"my-key".to_string(),
"0xaddr".to_string(),
)
.unwrap();
assert!(account.auth_config().is_some());
assert!(matches!(
account.auth_config(),
Some(AuthConfig::RelayerApiKey(_))
));
}
#[test]
fn test_new_wraps_builder_config_in_auth_config() {
let config = BuilderConfig::new("key".into(), "secret".into(), None);
let account = BuilderAccount::new(TEST_PRIVATE_KEY, Some(config)).unwrap();
assert!(matches!(
account.auth_config(),
Some(AuthConfig::Builder(_))
));
}
#[test]
fn test_with_auth_config_none() {
let account = BuilderAccount::with_auth_config(TEST_PRIVATE_KEY, None).unwrap();
assert!(account.auth_config().is_none());
}
#[test]
fn test_with_auth_config_relayer_api_key_variant() {
let relayer =
crate::config::RelayerApiKeyConfig::new("rk".into(), "0xaddr".into()).unwrap();
let auth = AuthConfig::RelayerApiKey(relayer);
let account = BuilderAccount::with_auth_config(TEST_PRIVATE_KEY, Some(auth)).unwrap();
assert!(matches!(
account.auth_config(),
Some(AuthConfig::RelayerApiKey(_))
));
}
#[test]
fn test_with_auth_config_invalid_private_key() {
let result = BuilderAccount::with_auth_config("not_a_valid_key", None);
assert!(result.is_err());
match result.unwrap_err() {
RelayError::Signer(msg) => {
assert!(
msg.contains("Failed to parse private key"),
"unexpected: {msg}"
);
}
other => panic!("Expected Signer error, got: {other:?}"),
}
}
}