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