alloy_signer_local/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4    html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
8
9use alloy_consensus::SignableTransaction;
10use alloy_network::{impl_into_wallet, TxSigner, TxSignerSync};
11use alloy_primitives::{Address, ChainId, PrimitiveSignature as Signature, B256};
12use alloy_signer::{sign_transaction_with_chain_id, Result, Signer, SignerSync};
13use async_trait::async_trait;
14use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId};
15use std::fmt;
16
17mod error;
18pub use error::LocalSignerError;
19
20#[cfg(feature = "mnemonic")]
21mod mnemonic;
22#[cfg(feature = "mnemonic")]
23pub use mnemonic::{MnemonicBuilder, MnemonicBuilderError};
24
25mod private_key;
26
27#[cfg(feature = "yubihsm")]
28mod yubi;
29
30#[cfg(feature = "yubihsm")]
31pub use yubihsm;
32
33#[cfg(feature = "mnemonic")]
34pub use coins_bip39;
35
36/// A signer instantiated with a locally stored private key.
37pub type PrivateKeySigner = LocalSigner<k256::ecdsa::SigningKey>;
38
39#[doc(hidden)]
40#[deprecated(note = "use `PrivateKeySigner` instead")]
41pub type LocalWallet = PrivateKeySigner;
42
43/// A signer instantiated with a YubiHSM.
44#[cfg(feature = "yubihsm")]
45pub type YubiSigner = LocalSigner<yubihsm::ecdsa::Signer<k256::Secp256k1>>;
46
47#[cfg(feature = "yubihsm")]
48#[doc(hidden)]
49#[deprecated(note = "use `YubiSigner` instead")]
50pub type YubiWallet = YubiSigner;
51
52/// An Ethereum private-public key pair which can be used for signing messages.
53///
54/// # Examples
55///
56/// ## Signing and Verifying a message
57///
58/// The signer can be used to produce ECDSA [`Signature`] objects, which can be
59/// then verified. Note that this uses
60/// [`eip191_hash_message`](alloy_primitives::eip191_hash_message) under the hood which will
61/// prefix the message being hashed with the `Ethereum Signed Message` domain separator.
62///
63/// ```
64/// use alloy_signer::{Signer, SignerSync};
65/// use alloy_signer_local::PrivateKeySigner;
66///
67/// let signer = PrivateKeySigner::random();
68///
69/// // Optionally, the signer's chain id can be set, in order to use EIP-155
70/// // replay protection with different chains
71/// let signer = signer.with_chain_id(Some(1337));
72///
73/// // The signer can be used to sign messages
74/// let message = b"hello";
75/// let signature = signer.sign_message_sync(message)?;
76/// assert_eq!(signature.recover_address_from_msg(&message[..]).unwrap(), signer.address());
77///
78/// // LocalSigner is cloneable:
79/// let signer_clone = signer.clone();
80/// let signature2 = signer_clone.sign_message_sync(message)?;
81/// assert_eq!(signature, signature2);
82/// # Ok::<_, Box<dyn std::error::Error>>(())
83/// ```
84#[derive(Clone)]
85pub struct LocalSigner<C> {
86    /// The signer's credential.
87    pub(crate) credential: C,
88    /// The signer's address.
89    pub(crate) address: Address,
90    /// The signer's chain ID (for EIP-155).
91    pub(crate) chain_id: Option<ChainId>,
92}
93
94#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
95#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
96impl<C: PrehashSigner<(ecdsa::Signature, RecoveryId)> + Send + Sync> Signer for LocalSigner<C> {
97    #[inline]
98    async fn sign_hash(&self, hash: &B256) -> Result<Signature> {
99        self.sign_hash_sync(hash)
100    }
101
102    #[inline]
103    fn address(&self) -> Address {
104        self.address
105    }
106
107    #[inline]
108    fn chain_id(&self) -> Option<ChainId> {
109        self.chain_id
110    }
111
112    #[inline]
113    fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
114        self.chain_id = chain_id;
115    }
116}
117
118impl<C: PrehashSigner<(ecdsa::Signature, RecoveryId)>> SignerSync for LocalSigner<C> {
119    #[inline]
120    fn sign_hash_sync(&self, hash: &B256) -> Result<Signature> {
121        Ok(self.credential.sign_prehash(hash.as_ref())?.into())
122    }
123
124    #[inline]
125    fn chain_id_sync(&self) -> Option<ChainId> {
126        self.chain_id
127    }
128}
129
130impl<C: PrehashSigner<(ecdsa::Signature, RecoveryId)>> LocalSigner<C> {
131    /// Construct a new credential with an external [`PrehashSigner`].
132    #[inline]
133    pub const fn new_with_credential(
134        credential: C,
135        address: Address,
136        chain_id: Option<ChainId>,
137    ) -> Self {
138        Self { credential, address, chain_id }
139    }
140
141    /// Returns this signer's credential.
142    #[inline]
143    pub const fn credential(&self) -> &C {
144        &self.credential
145    }
146
147    /// Consumes this signer and returns its credential.
148    #[inline]
149    pub fn into_credential(self) -> C {
150        self.credential
151    }
152
153    /// Returns this signer's address.
154    #[inline]
155    pub const fn address(&self) -> Address {
156        self.address
157    }
158
159    /// Returns this signer's chain ID.
160    #[inline]
161    pub const fn chain_id(&self) -> Option<ChainId> {
162        self.chain_id
163    }
164}
165
166// do not log the signer
167impl<C: PrehashSigner<(ecdsa::Signature, RecoveryId)>> fmt::Debug for LocalSigner<C> {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.debug_struct("LocalSigner")
170            .field("address", &self.address)
171            .field("chain_id", &self.chain_id)
172            .finish()
173    }
174}
175
176#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
177#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
178impl<C> TxSigner<Signature> for LocalSigner<C>
179where
180    C: PrehashSigner<(ecdsa::Signature, RecoveryId)> + Send + Sync,
181{
182    fn address(&self) -> Address {
183        self.address
184    }
185
186    #[doc(alias = "sign_tx")]
187    async fn sign_transaction(
188        &self,
189        tx: &mut dyn SignableTransaction<Signature>,
190    ) -> alloy_signer::Result<Signature> {
191        sign_transaction_with_chain_id!(self, tx, self.sign_hash_sync(&tx.signature_hash()))
192    }
193}
194
195impl<C> TxSignerSync<Signature> for LocalSigner<C>
196where
197    C: PrehashSigner<(ecdsa::Signature, RecoveryId)>,
198{
199    fn address(&self) -> Address {
200        self.address
201    }
202
203    #[doc(alias = "sign_tx_sync")]
204    fn sign_transaction_sync(
205        &self,
206        tx: &mut dyn SignableTransaction<Signature>,
207    ) -> alloy_signer::Result<Signature> {
208        sign_transaction_with_chain_id!(self, tx, self.sign_hash_sync(&tx.signature_hash()))
209    }
210}
211
212impl_into_wallet!(@[C: PrehashSigner<(ecdsa::Signature, RecoveryId)> + Send + Sync + 'static] LocalSigner<C>);
213
214#[cfg(test)]
215mod test {
216    use super::*;
217    use alloy_consensus::TxLegacy;
218    use alloy_primitives::{address, U256};
219
220    #[tokio::test]
221    async fn signs_tx() {
222        async fn sign_tx_test(tx: &mut TxLegacy, chain_id: Option<ChainId>) -> Result<Signature> {
223            let mut before = tx.clone();
224            let sig = sign_dyn_tx_test(tx, chain_id).await?;
225            if let Some(chain_id) = chain_id {
226                assert_eq!(tx.chain_id, Some(chain_id), "chain ID was not set");
227                before.chain_id = Some(chain_id);
228            }
229            assert_eq!(*tx, before);
230            Ok(sig)
231        }
232
233        async fn sign_dyn_tx_test(
234            tx: &mut dyn SignableTransaction<Signature>,
235            chain_id: Option<ChainId>,
236        ) -> Result<Signature> {
237            let mut signer: PrivateKeySigner =
238                "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
239            signer.set_chain_id(chain_id);
240
241            let sig = signer.sign_transaction_sync(tx)?;
242            let sighash = tx.signature_hash();
243            assert_eq!(sig.recover_address_from_prehash(&sighash).unwrap(), signer.address());
244
245            let sig_async = signer.sign_transaction(tx).await.unwrap();
246            assert_eq!(sig_async, sig);
247
248            Ok(sig)
249        }
250
251        // retrieved test vector from:
252        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
253        let mut tx = TxLegacy {
254            to: address!("F0109fC8DF283027b6285cc889F5aA624EaC1F55").into(),
255            value: U256::from(1_000_000_000),
256            gas_limit: 2_000_000,
257            nonce: 0,
258            gas_price: 21_000_000_000,
259            input: Default::default(),
260            chain_id: None,
261        };
262        let sig_none = sign_tx_test(&mut tx, None).await.unwrap();
263
264        tx.chain_id = Some(1);
265        let sig_1 = sign_tx_test(&mut tx, None).await.unwrap();
266        let expected = "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa6825".parse().unwrap();
267        assert_eq!(sig_1, expected);
268        assert_ne!(sig_1, sig_none);
269
270        tx.chain_id = Some(2);
271        let sig_2 = sign_tx_test(&mut tx, None).await.unwrap();
272        assert_ne!(sig_2, sig_1);
273        assert_ne!(sig_2, sig_none);
274
275        // Sets chain ID.
276        tx.chain_id = None;
277        let sig_none_none = sign_tx_test(&mut tx, None).await.unwrap();
278        assert_eq!(sig_none_none, sig_none);
279
280        tx.chain_id = None;
281        let sig_none_1 = sign_tx_test(&mut tx, Some(1)).await.unwrap();
282        assert_eq!(sig_none_1, sig_1);
283
284        tx.chain_id = None;
285        let sig_none_2 = sign_tx_test(&mut tx, Some(2)).await.unwrap();
286        assert_eq!(sig_none_2, sig_2);
287
288        // Errors on mismatch.
289        tx.chain_id = Some(2);
290        let error = sign_tx_test(&mut tx, Some(1)).await.unwrap_err();
291        let expected_error = alloy_signer::Error::TransactionChainIdMismatch { signer: 1, tx: 2 };
292        assert_eq!(error.to_string(), expected_error.to_string());
293    }
294
295    // <https://github.com/alloy-rs/core/issues/705>
296    #[test]
297    fn test_parity() {
298        let signer = PrivateKeySigner::random();
299        let message = b"hello";
300        let signature = signer.sign_message_sync(message).unwrap();
301        let value = signature.as_bytes().to_vec();
302        let recovered_signature: Signature = value.as_slice().try_into().unwrap();
303        assert_eq!(signature, recovered_signature);
304    }
305}