openzeppelin_relayer/services/signer/evm/
local_signer.rs1use alloy::{
17 consensus::{SignableTransaction, TxEip1559, TxLegacy},
18 network::{EthereumWallet, TransactionBuilder, TxSigner},
19 rpc::types::Transaction,
20 signers::{
21 k256::ecdsa::SigningKey, local::LocalSigner as AlloyLocalSignerClient,
22 Signer as AlloySigner, SignerSync,
23 },
24};
25
26use alloy::primitives::{address, Address as AlloyAddress, Bytes, FixedBytes, TxKind, U256};
27
28use async_trait::async_trait;
29
30use crate::{
31 domain::{
32 SignDataRequest, SignDataResponse, SignDataResponseEvm, SignTransactionResponse,
33 SignTransactionResponseEvm, SignTypedDataRequest,
34 },
35 models::{
36 Address, EvmTransactionData, EvmTransactionDataSignature, EvmTransactionDataTrait,
37 NetworkTransactionData, Signer as SignerDomainModel, SignerError, SignerRepoModel,
38 SignerType, TransactionRepoModel,
39 },
40 services::Signer,
41};
42
43use super::DataSignerTrait;
44
45use alloy::rpc::types::TransactionRequest;
46
47#[derive(Clone)]
48pub struct LocalSigner {
49 local_signer_client: AlloyLocalSignerClient<SigningKey>,
50}
51
52impl LocalSigner {
53 pub fn new(signer_model: &SignerDomainModel) -> Result<Self, SignerError> {
54 let config = signer_model
55 .config
56 .get_local()
57 .ok_or_else(|| SignerError::Configuration("Local config not found".to_string()))?;
58
59 let local_signer_client = {
60 let key_bytes = config.raw_key.borrow();
61
62 AlloyLocalSignerClient::from_bytes(&FixedBytes::from_slice(&key_bytes)).map_err(
63 |e| SignerError::Configuration(format!("Failed to create local signer: {}", e)),
64 )?
65 };
66
67 Ok(Self {
68 local_signer_client,
69 })
70 }
71}
72
73impl From<AlloyAddress> for Address {
74 fn from(addr: AlloyAddress) -> Self {
75 Address::Evm(addr.into_array())
76 }
77}
78
79#[async_trait]
80impl Signer for LocalSigner {
81 async fn address(&self) -> Result<Address, SignerError> {
82 let address: Address = self.local_signer_client.address().into();
83 Ok(address)
84 }
85
86 async fn sign_transaction(
87 &self,
88 transaction: NetworkTransactionData,
89 ) -> Result<SignTransactionResponse, SignerError> {
90 let evm_data = transaction.get_evm_transaction_data()?;
91 if evm_data.is_eip1559() {
92 let mut unsigned_tx = TxEip1559::try_from(transaction)?;
94
95 let signature = self
96 .local_signer_client
97 .sign_transaction(&mut unsigned_tx)
98 .await
99 .map_err(|e| {
100 SignerError::SigningError(format!("Failed to sign EIP-1559 transaction: {e}"))
101 })?;
102
103 let signed_tx = unsigned_tx.into_signed(signature);
104 let mut signature_bytes = signature.as_bytes();
105
106 if signature_bytes[64] == 27 {
108 signature_bytes[64] = 0;
109 } else if signature_bytes[64] == 28 {
110 signature_bytes[64] = 1;
111 }
112
113 let mut raw = Vec::with_capacity(signed_tx.eip2718_encoded_length());
114 signed_tx.eip2718_encode(&mut raw);
115
116 Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
117 hash: signed_tx.hash().to_string(),
118 signature: EvmTransactionDataSignature::from(&signature_bytes),
119 raw,
120 }))
121 } else {
122 let mut unsigned_tx = TxLegacy::try_from(transaction.clone())?;
124
125 let signature = self
126 .local_signer_client
127 .sign_transaction(&mut unsigned_tx)
128 .await
129 .map_err(|e| {
130 SignerError::SigningError(format!("Failed to sign legacy transaction: {e}"))
131 })?;
132
133 let signed_tx = unsigned_tx.into_signed(signature);
134 let signature_bytes = signature.as_bytes();
135
136 let mut raw = Vec::with_capacity(signed_tx.rlp_encoded_length());
137 signed_tx.rlp_encode(&mut raw);
138
139 Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
140 hash: signed_tx.hash().to_string(),
141 signature: EvmTransactionDataSignature::from(&signature_bytes),
142 raw,
143 }))
144 }
145 }
146}
147
148#[async_trait]
149impl DataSignerTrait for LocalSigner {
150 async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, SignerError> {
151 let message = request.message.as_bytes();
152
153 let signature = self
154 .local_signer_client
155 .sign_message(message)
156 .await
157 .map_err(|e| SignerError::SigningError(format!("Failed to sign message: {}", e)))?;
158
159 let ste = signature.as_bytes();
160
161 Ok(SignDataResponse::Evm(SignDataResponseEvm {
162 r: hex::encode(&ste[0..32]),
163 s: hex::encode(&ste[32..64]),
164 v: ste[64],
165 sig: hex::encode(ste),
166 }))
167 }
168
169 async fn sign_typed_data(
170 &self,
171 _typed_data: SignTypedDataRequest,
172 ) -> Result<SignDataResponse, SignerError> {
173 todo!()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use secrets::SecretVec;
180
181 use crate::models::{EvmTransactionData, LocalSignerConfig, SignerConfig, U256};
182
183 use super::*;
184 use std::str::FromStr;
185
186 fn create_test_signer_model() -> SignerDomainModel {
187 let seed = vec![1u8; 32];
188 let raw_key = SecretVec::new(32, |v| v.copy_from_slice(&seed));
189 SignerDomainModel {
190 id: "test".to_string(),
191 config: SignerConfig::Local(LocalSignerConfig { raw_key }),
192 }
193 }
194
195 fn create_test_transaction() -> NetworkTransactionData {
196 NetworkTransactionData::Evm(EvmTransactionData {
197 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
198 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
199 gas_price: Some(20000000000),
200 gas_limit: Some(21000),
201 nonce: Some(0),
202 value: U256::from(1000000000000000000u64),
203 data: Some("0x".to_string()),
204 chain_id: 1,
205 hash: None,
206 signature: None,
207 raw: None,
208 max_fee_per_gas: None,
209 max_priority_fee_per_gas: None,
210 speed: None,
211 })
212 }
213
214 #[tokio::test]
215 async fn test_address_generation() {
216 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
217 let address = signer.address().await.unwrap();
218
219 match address {
220 Address::Evm(addr) => {
221 assert_eq!(addr.len(), 20); }
223 _ => panic!("Expected EVM address"),
224 }
225 }
226
227 #[tokio::test]
228 async fn test_sign_transaction_invalid_data() {
229 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
230 let mut tx = create_test_transaction();
231
232 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
233 evm_tx.data = Some("invalid_hex".to_string());
234 }
235
236 let result = signer.sign_transaction(tx).await;
237 assert!(result.is_err());
238 }
239
240 #[tokio::test]
241 async fn test_sign_data() {
242 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
243 let request = SignDataRequest {
244 message: "Test message".to_string(),
245 };
246
247 let result = signer.sign_data(request).await.unwrap();
248
249 match result {
250 SignDataResponse::Evm(sig) => {
251 assert_eq!(sig.r.len(), 64); assert_eq!(sig.s.len(), 64); assert!(sig.v == 27 || sig.v == 28); assert_eq!(sig.sig.len(), 130); }
256 _ => panic!("Expected EVM signature"),
257 }
258 }
259
260 #[tokio::test]
261 async fn test_sign_data_empty_message() {
262 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
263 let request = SignDataRequest {
264 message: "".to_string(),
265 };
266
267 let result = signer.sign_data(request).await;
268 assert!(result.is_ok());
269 }
270
271 #[tokio::test]
272 async fn test_sign_transaction_with_contract_creation() {
273 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
274 let mut tx = create_test_transaction();
275
276 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
277 evm_tx.to = None;
278 evm_tx.data = Some("0x6080604000".to_string()); }
280
281 let result = signer.sign_transaction(tx).await.unwrap();
282 match result {
283 SignTransactionResponse::Evm(signed_tx) => {
284 assert!(!signed_tx.hash.is_empty());
285 assert!(!signed_tx.raw.is_empty());
286 assert!(!signed_tx.signature.sig.is_empty());
287 }
288 _ => panic!("Expected EVM transaction response"),
289 }
290 }
291
292 #[tokio::test]
293 async fn test_sign_eip1559_transaction() {
294 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
295 let mut tx = create_test_transaction();
296
297 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
299 evm_tx.gas_price = None;
300 evm_tx.max_fee_per_gas = Some(30_000_000_000);
301 evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
302 }
303
304 let result = signer.sign_transaction(tx).await;
305 assert!(result.is_ok());
306
307 match result.unwrap() {
308 SignTransactionResponse::Evm(signed_tx) => {
309 assert!(!signed_tx.hash.is_empty());
310 assert!(!signed_tx.raw.is_empty());
311 assert!(!signed_tx.signature.sig.is_empty());
312 assert_eq!(signed_tx.signature.r.len(), 64); assert_eq!(signed_tx.signature.s.len(), 64); assert!(signed_tx.signature.v == 0 || signed_tx.signature.v == 1);
316 }
318 _ => panic!("Expected EVM transaction response"),
319 }
320 }
321
322 #[tokio::test]
323 async fn test_sign_eip1559_transaction_with_contract_creation() {
324 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
325 let mut tx = create_test_transaction();
326
327 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
328 evm_tx.to = None;
329 evm_tx.data = Some("0x6080604000".to_string()); evm_tx.gas_price = None;
331 evm_tx.max_fee_per_gas = Some(30_000_000_000);
332 evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
333 }
334
335 let result = signer.sign_transaction(tx).await;
336 assert!(result.is_ok());
337
338 match result.unwrap() {
339 SignTransactionResponse::Evm(signed_tx) => {
340 assert!(!signed_tx.hash.is_empty());
341 assert!(!signed_tx.raw.is_empty());
342 assert!(!signed_tx.signature.sig.is_empty());
343 }
344 _ => panic!("Expected EVM transaction response"),
345 }
346 }
347}