alloy_signer_local/
lib.rs1#![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
36pub type PrivateKeySigner = LocalSigner<k256::ecdsa::SigningKey>;
38
39#[doc(hidden)]
40#[deprecated(note = "use `PrivateKeySigner` instead")]
41pub type LocalWallet = PrivateKeySigner;
42
43#[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#[derive(Clone)]
85pub struct LocalSigner<C> {
86 pub(crate) credential: C,
88 pub(crate) address: Address,
90 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 #[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 #[inline]
143 pub const fn credential(&self) -> &C {
144 &self.credential
145 }
146
147 #[inline]
149 pub fn into_credential(self) -> C {
150 self.credential
151 }
152
153 #[inline]
155 pub const fn address(&self) -> Address {
156 self.address
157 }
158
159 #[inline]
161 pub const fn chain_id(&self) -> Option<ChainId> {
162 self.chain_id
163 }
164}
165
166impl<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 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 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 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 #[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}