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