Skip to main content

signer_evm/
lib.rs

1//! EVM transaction signer built on [`alloy-signer-local`].
2//!
3//! [`Signer`] wraps a [`PrivateKeySigner`] with [`Deref`](core::ops::Deref)
4//! for full alloy API access. The inner key is `ZeroizeOnDrop`.
5//!
6//! **Zero hand-rolled cryptography.**
7//!
8//! # Signing methods (via `Deref` to `PrivateKeySigner`)
9//!
10//! | Method | Standard |
11//! |---|---|
12//! | `sign_hash_sync` / `sign_hash` | Raw 32-byte hash |
13//! | `sign_message_sync` / `sign_message` | EIP-191 personal_sign |
14//! | `sign_typed_data_sync` / `sign_typed_data` | EIP-712 |
15//! | `sign_transaction_sync` / `sign_transaction` | All EVM tx types |
16//! | `address` | Signer's Ethereum address |
17//! | `chain_id` / `set_chain_id` | EIP-155 chain ID |
18
19mod error;
20
21use core::ops::{Deref, DerefMut};
22
23pub use alloy_consensus;
24pub use alloy_network;
25pub use alloy_network::{TxSigner, TxSignerSync};
26pub use alloy_primitives;
27pub use alloy_primitives::{Address, B256, ChainId, U256};
28pub use alloy_signer;
29pub use alloy_signer::{Signature, Signer as AlloySigner, SignerSync};
30pub use alloy_signer_local;
31use alloy_signer_local::PrivateKeySigner;
32pub use error::Error;
33
34/// EVM transaction signer.
35///
36/// Wraps a [`PrivateKeySigner`] with [`Deref`] for full alloy API access.
37/// The inner `k256::SigningKey` implements `ZeroizeOnDrop`.
38///
39/// # Examples
40///
41/// ```no_run
42/// use signer_evm::{Signer, SignerSync};
43///
44/// let signer = Signer::random();
45/// let sig = signer.sign_message_sync(b"hello").unwrap();
46/// ```
47#[derive(Debug, Clone)]
48pub struct Signer {
49    inner: PrivateKeySigner,
50}
51
52impl Deref for Signer {
53    type Target = PrivateKeySigner;
54
55    #[inline]
56    fn deref(&self) -> &Self::Target {
57        &self.inner
58    }
59}
60
61impl DerefMut for Signer {
62    #[inline]
63    fn deref_mut(&mut self) -> &mut Self::Target {
64        &mut self.inner
65    }
66}
67
68impl From<PrivateKeySigner> for Signer {
69    #[inline]
70    fn from(inner: PrivateKeySigner) -> Self {
71        Self { inner }
72    }
73}
74
75impl From<Signer> for PrivateKeySigner {
76    #[inline]
77    fn from(signer: Signer) -> Self {
78        signer.inner
79    }
80}
81
82impl Signer {
83    /// Create a signer from a raw 32-byte private key.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if the bytes do not represent a valid secp256k1 private key.
88    pub fn from_bytes(bytes: &B256) -> Result<Self, Error> {
89        let inner =
90            PrivateKeySigner::from_bytes(bytes).map_err(|e| Error::InvalidKey(e.to_string()))?;
91        Ok(Self { inner })
92    }
93
94    /// Create a signer from a hex-encoded private key.
95    ///
96    /// Accepts keys with or without `0x` prefix.
97    /// Compatible with [`kobe_evm::DerivedAddress::private_key_hex`] output.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if the hex string is invalid or the key is not a valid
102    /// secp256k1 private key.
103    pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
104        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
105        let bytes: [u8; 32] = hex::decode(hex_str)?.try_into().map_err(|v: Vec<u8>| {
106            Error::InvalidKey(format!("expected 32 bytes, got {}", v.len()))
107        })?;
108        Self::from_bytes(&B256::from(bytes))
109    }
110
111    /// Generate a random signer.
112    #[must_use]
113    pub fn random() -> Self {
114        Self {
115            inner: PrivateKeySigner::random(),
116        }
117    }
118
119    /// Consume the wrapper and return the inner [`PrivateKeySigner`].
120    #[must_use]
121    pub fn into_inner(self) -> PrivateKeySigner {
122        self.inner
123    }
124}
125
126#[cfg(feature = "kobe")]
127impl Signer {
128    /// Create a signer from a [`kobe_evm::DerivedAddress`].
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the private key in the derived address is invalid.
133    pub fn from_derived(derived: &kobe_evm::DerivedAddress) -> Result<Self, Error> {
134        Self::from_hex(&derived.private_key_hex)
135    }
136
137    /// Create a signer from a [`kobe_evm::StandardWallet`].
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the private key is invalid.
142    pub fn from_standard_wallet(wallet: &kobe_evm::StandardWallet) -> Result<Self, Error> {
143        Self::from_hex(&wallet.secret_hex())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn assert_send_sync() {
153        fn assert<T: Send + Sync>() {}
154        assert::<Signer>();
155    }
156
157    #[test]
158    fn assert_clone() {
159        let s = Signer::random();
160        let s2 = s.clone();
161        assert_eq!(s.address(), s2.address());
162    }
163
164    #[test]
165    fn random_signer() {
166        let s = Signer::random();
167        assert_ne!(s.address(), Address::ZERO);
168    }
169
170    #[test]
171    fn hex_roundtrip() {
172        let s = Signer::random();
173        let hex_key = hex::encode(s.inner.credential().to_bytes());
174        let restored = Signer::from_hex(&hex_key).unwrap();
175        assert_eq!(s.address(), restored.address());
176    }
177
178    #[test]
179    fn hex_with_prefix() {
180        let s = Signer::random();
181        let hex_key = format!("0x{}", hex::encode(s.inner.credential().to_bytes()));
182        let restored = Signer::from_hex(&hex_key).unwrap();
183        assert_eq!(s.address(), restored.address());
184    }
185
186    #[test]
187    fn sign_message_sync() {
188        let s = Signer::random();
189        let sig = s.sign_message_sync(b"hello").unwrap();
190        let recovered = sig.recover_address_from_msg("hello").unwrap();
191        assert_eq!(recovered, s.address());
192    }
193
194    #[test]
195    fn sign_hash_sync() {
196        let s = Signer::random();
197        let hash = B256::from([0xab; 32]);
198        let sig = s.sign_hash_sync(&hash).unwrap();
199        let recovered = sig.recover_address_from_prehash(&hash).unwrap();
200        assert_eq!(recovered, s.address());
201    }
202
203    #[test]
204    fn into_inner() {
205        let s = Signer::random();
206        let addr = s.address();
207        let inner = s.into_inner();
208        assert_eq!(inner.address(), addr);
209    }
210
211    #[test]
212    fn from_private_key_signer() {
213        let pks = PrivateKeySigner::random();
214        let addr = pks.address();
215        let s = Signer::from(pks);
216        assert_eq!(s.address(), addr);
217    }
218}