use alloy::network::EthereumWallet;
use alloy::primitives::Address;
use alloy::signers::local::PrivateKeySigner;
use alloy::signers::Signer;
use crate::onchain::{OnchainError, Result, WalletType};
#[derive(Clone)]
pub enum OnchainSigner {
EOA(PrivateKeySigner),
Proxy {
signer: PrivateKeySigner,
proxy_address: Address,
},
Safe {
signer: PrivateKeySigner,
safe_address: Address,
threshold: u64,
},
}
impl OnchainSigner {
pub fn from_private_key(private_key: impl AsRef<str>) -> Result<Self> {
let key_str = private_key.as_ref();
let key_str = key_str.strip_prefix("0x").unwrap_or(key_str);
let signer = PrivateKeySigner::from_slice(
&hex::decode(key_str)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid hex: {}", e)))?,
)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid key: {}", e)))?;
Ok(Self::EOA(signer))
}
pub fn from_proxy(private_key: impl AsRef<str>, proxy_address: Address) -> Result<Self> {
let key_str = private_key.as_ref();
let key_str = key_str.strip_prefix("0x").unwrap_or(key_str);
let signer = PrivateKeySigner::from_slice(
&hex::decode(key_str)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid hex: {}", e)))?,
)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid key: {}", e)))?;
Ok(Self::Proxy {
signer,
proxy_address,
})
}
pub fn from_safe(
private_key: impl AsRef<str>,
safe_address: Address,
threshold: Option<u64>,
) -> Result<Self> {
let key_str = private_key.as_ref();
let key_str = key_str.strip_prefix("0x").unwrap_or(key_str);
let signer = PrivateKeySigner::from_slice(
&hex::decode(key_str)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid hex: {}", e)))?,
)
.map_err(|e| OnchainError::InvalidPrivateKey(format!("Invalid key: {}", e)))?;
Ok(Self::Safe {
signer,
safe_address,
threshold: threshold.unwrap_or(1),
})
}
pub fn wallet_type(&self) -> WalletType {
match self {
Self::EOA(_) => WalletType::PrivateKey,
Self::Proxy { .. } => WalletType::Proxy,
Self::Safe { .. } => WalletType::Safe,
}
}
pub fn address(&self) -> Address {
match self {
Self::EOA(signer) => signer.address(),
Self::Proxy { proxy_address, .. } => *proxy_address,
Self::Safe { safe_address, .. } => *safe_address,
}
}
pub fn signer_address(&self) -> Address {
match self {
Self::EOA(signer) => signer.address(),
Self::Proxy { signer, .. } => signer.address(),
Self::Safe { signer, .. } => signer.address(),
}
}
pub(crate) fn private_key_signer(&self) -> &PrivateKeySigner {
match self {
Self::EOA(signer) => signer,
Self::Proxy { signer, .. } => signer,
Self::Safe { signer, .. } => signer,
}
}
pub fn to_ethereum_wallet(&self, chain_id: u64) -> EthereumWallet {
let mut signer = self.private_key_signer().clone();
signer = signer.with_chain_id(Some(chain_id));
EthereumWallet::from(signer)
}
pub async fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>> {
let signer = self.private_key_signer();
let signature = signer
.sign_message(message)
.await
.map_err(|e| OnchainError::SignerError(e.to_string()))?;
Ok(signature.as_bytes().to_vec())
}
pub fn is_smart_wallet(&self) -> bool {
matches!(self, Self::Proxy { .. } | Self::Safe { .. })
}
pub fn proxy_address(&self) -> Option<Address> {
match self {
Self::Proxy { proxy_address, .. } => Some(*proxy_address),
_ => None,
}
}
pub fn safe_address(&self) -> Option<Address> {
match self {
Self::Safe { safe_address, .. } => Some(*safe_address),
_ => None,
}
}
pub fn safe_threshold(&self) -> Option<u64> {
match self {
Self::Safe { threshold, .. } => Some(*threshold),
_ => None,
}
}
}
impl std::fmt::Debug for OnchainSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EOA(signer) => f
.debug_struct("EOA")
.field("address", &signer.address())
.finish(),
Self::Proxy {
signer,
proxy_address,
} => f
.debug_struct("Proxy")
.field("signer_address", &signer.address())
.field("proxy_address", proxy_address)
.finish(),
Self::Safe {
signer,
safe_address,
threshold,
} => f
.debug_struct("Safe")
.field("signer_address", &signer.address())
.field("safe_address", safe_address)
.field("threshold", threshold)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_PRIVATE_KEY: &str =
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
fn test_from_private_key() {
let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();
assert_eq!(signer.wallet_type(), WalletType::PrivateKey);
assert!(!signer.is_smart_wallet());
}
#[test]
fn test_from_private_key_without_0x() {
let key_without_prefix = TEST_PRIVATE_KEY.strip_prefix("0x").unwrap();
let signer = OnchainSigner::from_private_key(key_without_prefix).unwrap();
assert_eq!(signer.wallet_type(), WalletType::PrivateKey);
}
#[test]
fn test_from_proxy() {
let proxy_address = Address::repeat_byte(0x42);
let signer = OnchainSigner::from_proxy(TEST_PRIVATE_KEY, proxy_address).unwrap();
assert_eq!(signer.wallet_type(), WalletType::Proxy);
assert!(signer.is_smart_wallet());
assert_eq!(signer.address(), proxy_address);
assert_eq!(signer.proxy_address(), Some(proxy_address));
assert_ne!(signer.signer_address(), proxy_address);
}
#[test]
fn test_from_safe() {
let safe_address = Address::repeat_byte(0x99);
let signer = OnchainSigner::from_safe(TEST_PRIVATE_KEY, safe_address, Some(2)).unwrap();
assert_eq!(signer.wallet_type(), WalletType::Safe);
assert!(signer.is_smart_wallet());
assert_eq!(signer.address(), safe_address);
assert_eq!(signer.safe_address(), Some(safe_address));
assert_eq!(signer.safe_threshold(), Some(2));
assert_ne!(signer.signer_address(), safe_address);
}
#[test]
fn test_invalid_private_key() {
let result = OnchainSigner::from_private_key("invalid_hex");
assert!(result.is_err());
let result = OnchainSigner::from_private_key("0x123"); assert!(result.is_err());
}
#[test]
fn test_debug_format() {
let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();
let debug_str = format!("{:?}", signer);
assert!(debug_str.contains("EOA"));
assert!(debug_str.contains("address"));
}
}