Skip to main content

polyoxide_relay/
account.rs

1use crate::config::BuilderConfig;
2use crate::error::RelayError;
3use alloy::primitives::Address;
4use alloy::signers::local::PrivateKeySigner;
5
6#[derive(Clone)]
7pub struct BuilderAccount {
8    pub(crate) signer: PrivateKeySigner,
9    pub(crate) config: Option<BuilderConfig>,
10}
11
12impl std::fmt::Debug for BuilderAccount {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        f.debug_struct("BuilderAccount")
15            .field("address", &self.signer.address())
16            .field("config", &self.config)
17            .finish()
18    }
19}
20
21impl BuilderAccount {
22    pub fn new(
23        private_key: impl Into<String>,
24        config: Option<BuilderConfig>,
25    ) -> Result<Self, RelayError> {
26        let signer = private_key
27            .into()
28            .parse::<PrivateKeySigner>()
29            .map_err(|e| RelayError::Signer(format!("Failed to parse private key: {}", e)))?;
30
31        Ok(Self { signer, config })
32    }
33
34    pub fn address(&self) -> Address {
35        self.signer.address()
36    }
37
38    pub fn signer(&self) -> &PrivateKeySigner {
39        &self.signer
40    }
41
42    pub fn config(&self) -> Option<&BuilderConfig> {
43        self.config.as_ref()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    // A well-known test private key (DO NOT use for real funds)
52    // Address: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (anvil/hardhat default #0)
53    const TEST_PRIVATE_KEY: &str =
54        "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
55
56    #[test]
57    fn test_new_valid_private_key() {
58        let account = BuilderAccount::new(TEST_PRIVATE_KEY, None);
59        assert!(account.is_ok());
60    }
61
62    #[test]
63    fn test_new_with_0x_prefix() {
64        let key = format!("0x{}", TEST_PRIVATE_KEY);
65        let account = BuilderAccount::new(key, None);
66        // alloy accepts 0x-prefixed keys
67        assert!(account.is_ok());
68    }
69
70    #[test]
71    fn test_new_invalid_private_key() {
72        let result = BuilderAccount::new("not_a_valid_key", None);
73        assert!(result.is_err());
74        let err = result.unwrap_err();
75        match err {
76            RelayError::Signer(msg) => {
77                assert!(
78                    msg.contains("Failed to parse private key"),
79                    "unexpected: {msg}"
80                );
81            }
82            other => panic!("Expected Signer error, got: {other:?}"),
83        }
84    }
85
86    #[test]
87    fn test_new_empty_key() {
88        let result = BuilderAccount::new("", None);
89        assert!(result.is_err());
90    }
91
92    #[test]
93    fn test_address_derivation_deterministic() {
94        let a1 = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
95        let a2 = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
96        assert_eq!(a1.address(), a2.address());
97    }
98
99    #[test]
100    fn test_address_matches_known_value() {
101        // The first anvil/hardhat default account
102        let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
103        let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
104            .parse()
105            .unwrap();
106        assert_eq!(account.address(), expected);
107    }
108
109    #[test]
110    fn test_debug_redacts_private_key() {
111        let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
112        let debug_output = format!("{:?}", account);
113        assert!(
114            debug_output.contains("address"),
115            "Debug should show address, got: {debug_output}"
116        );
117        assert!(
118            !debug_output.contains(TEST_PRIVATE_KEY),
119            "Debug should not contain the private key, got: {debug_output}"
120        );
121    }
122
123    #[test]
124    fn test_config_none() {
125        let account = BuilderAccount::new(TEST_PRIVATE_KEY, None).unwrap();
126        assert!(account.config().is_none());
127    }
128
129    #[test]
130    fn test_config_some() {
131        let config = BuilderConfig::new("key".into(), "secret".into(), None);
132        let account = BuilderAccount::new(TEST_PRIVATE_KEY, Some(config)).unwrap();
133        assert!(account.config().is_some());
134    }
135}