openzeppelin_relayer/services/signer/evm/
mod.rs

1//! # EVM Signer Implementations
2//!
3//! This module provides various signer implementations for Ethereum Virtual Machine (EVM)
4//! transactions and data signing, including support for EIP-712 typed data.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! EVM Signer (trait implementations)
10//!   ├── LocalSigner             - Encrypted JSON keystore (development/testing)
11//!   ├── AwsKmsSigner           - AWS Key Management Service
12//!   ├── AzureKeyVaultSigner    - Azure Key Vault
13//!   ├── GoogleCloudKmsSigner   - Google Cloud Key Management Service
14//!   ├── VaultSigner            - HashiCorp Vault KV2 backend
15//!   ├── TurnkeySigner          - Turnkey API backend
16//!   └── CdpSigner              - Coinbase Developer Platform
17//! ```
18//!
19//! ## Features
20//!
21//! - **Transaction Signing**: Support for both legacy and EIP-1559 transactions
22//! - **Data Signing**: EIP-191 personal message signing
23//! - **Typed Data**: EIP-712 structured data signing
24//! - **Multi-Backend**: Pluggable signer backends with consistent API
25//!
26//! ## Security
27//!
28//! All implementations follow Ethereum signing standards and include:
29//! - Signature malleability protection (EIP-2 low-s normalization)
30//! - Proper v-value handling for different transaction types
31//! - Input validation and error handling
32
33mod aws_kms_signer;
34mod azure_key_vault_signer;
35mod cdp_signer;
36mod google_cloud_kms_signer;
37mod local_signer;
38mod turnkey_signer;
39pub(crate) mod utils;
40mod vault_signer;
41use aws_kms_signer::*;
42use azure_key_vault_signer::*;
43use cdp_signer::*;
44use google_cloud_kms_signer::*;
45use local_signer::*;
46use oz_keystore::HashicorpCloudClient;
47use turnkey_signer::*;
48use vault_signer::*;
49
50use async_trait::async_trait;
51use std::sync::Arc;
52
53use crate::{
54    domain::{
55        SignDataRequest, SignDataResponse, SignDataResponseEvm, SignTransactionResponse,
56        SignTypedDataRequest,
57    },
58    models::{
59        Address, NetworkTransactionData, Signer as SignerDomainModel, SignerConfig,
60        SignerRepoModel, SignerType, TransactionRepoModel, VaultSignerConfig,
61    },
62    services::{
63        signer::Signer,
64        signer::SignerError,
65        signer::SignerFactoryError,
66        turnkey::TurnkeyService,
67        vault::{VaultConfig, VaultService, VaultServiceTrait},
68        AwsKmsService, AzureKeyVaultService, CdpService, GoogleCloudKmsService,
69        TurnkeyServiceTrait,
70    },
71};
72use eyre::Result;
73
74// EIP-712 and ECDSA Constants
75const EIP712_PREFIX: [u8; 2] = [0x19, 0x01];
76const EIP712_MESSAGE_SIZE: usize = 66; // 2 (prefix) + 32 (domain) + 32 (struct)
77
78/// SECP256K1 signature length: 32 bytes (r) + 32 bytes (s) + 1 byte (v)
79const SECP256K1_SIGNATURE_LENGTH: usize = 65;
80
81/// Keccak256 hash output length
82const HASH_LENGTH: usize = 32;
83
84/// Validates and decodes a hex string
85///
86/// # Arguments
87/// * `value` - The hex string to decode (may have optional "0x" prefix)
88/// * `field_name` - Name of the field for error messages
89///
90/// # Returns
91/// * `Ok(Vec<u8>)` - The decoded bytes
92/// * `Err(SignerError)` - If the hex string is invalid
93fn validate_and_decode_hex(value: &str, field_name: &str) -> Result<Vec<u8>, SignerError> {
94    let hex_str = value.strip_prefix("0x").unwrap_or(value);
95
96    // Check for invalid characters and report position
97    if let Some((pos, ch)) = hex_str
98        .chars()
99        .enumerate()
100        .find(|(_, c)| !c.is_ascii_hexdigit())
101    {
102        return Err(SignerError::SigningError(format!(
103            "Invalid {} hex: non-hexadecimal character '{}' at position {} (input: {}...)",
104            field_name,
105            ch,
106            pos,
107            &hex_str[..hex_str.len().min(16)] // Show first 16 chars
108        )));
109    }
110
111    // Decode the hex string
112    hex::decode(hex_str).map_err(|e| {
113        SignerError::SigningError(format!(
114            "Invalid {} hex: failed to decode - {} (input: {}...)",
115            field_name,
116            e,
117            &hex_str[..hex_str.len().min(16)]
118        ))
119    })
120}
121
122/// Constructs an EIP-712 message hash from domain separator and struct hash
123///
124/// This function implements the EIP-712 typed data signing specification:
125/// <https://eips.ethereum.org/EIPS/eip-712>
126///
127/// # EIP-712 Format
128///
129/// The message hash is constructed as:
130/// ```text
131/// keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))
132/// ```
133///
134/// # Security Considerations
135///
136/// **CRITICAL SECURITY REQUIREMENTS:**
137///
138/// - **Domain Separator Uniqueness**: The domain separator MUST be unique to your dapp
139///   to prevent cross-contract replay attacks. Include at minimum:
140///   - Contract name
141///   - Contract version
142///   - Chain ID (prevents cross-chain replays)
143///   - Verifying contract address
144///
145/// - **Hash Struct Integrity**: The hashStruct MUST uniquely identify your message type
146///   and data. Collisions allow signature reuse across different messages.
147///
148/// - **Replay Protection**: Always include nonces or timestamps in your message struct
149///   to prevent replay attacks within the same contract.
150///
151/// - **Chain ID**: MUST be included in domain separator to prevent signatures from
152///   being valid on multiple chains (mainnet, testnet, etc.)
153///
154/// # Arguments
155///
156/// * `request` - The typed data signing request containing:
157///   - `domain_separator`: 32-byte hash uniquely identifying the signing domain
158///   - `hash_struct_message`: 32-byte hash of the structured message data
159///
160/// # Returns
161///
162/// * `Ok([u8; 32])` - The 32-byte message hash ready for signing
163/// * `Err(SignerError)` - If validation fails or hex decoding fails
164///
165/// # Errors
166///
167/// Returns `SignerError::SigningError` if:
168/// - Domain separator is not exactly 32 bytes
169/// - Hash struct is not exactly 32 bytes
170/// - Input contains invalid hexadecimal characters
171///
172/// # Examples
173///
174/// ```ignore
175/// use openzeppelin_relayer::domain::SignTypedDataRequest;
176/// use openzeppelin_relayer::services::signer::construct_eip712_message_hash;
177///
178/// let request = SignTypedDataRequest {
179///     // 32 bytes as hex (with or without 0x prefix)
180///     domain_separator: "a".repeat(64),
181///     hash_struct_message: "b".repeat(64),
182/// };
183///
184/// let hash = construct_eip712_message_hash(&request).unwrap();
185/// // hash is now ready for signing
186/// ```
187pub fn construct_eip712_message_hash(
188    request: &SignTypedDataRequest,
189) -> Result<[u8; 32], SignerError> {
190    // Decode and validate domain separator
191    let domain_separator = validate_and_decode_hex(&request.domain_separator, "domain separator")?;
192
193    // Decode and validate hash struct message
194    let hash_struct = validate_and_decode_hex(&request.hash_struct_message, "hash struct message")?;
195
196    // Validate lengths (both must be exactly 32 bytes)
197    if domain_separator.len() != HASH_LENGTH {
198        return Err(SignerError::SigningError(format!(
199            "Invalid domain separator length: expected {} bytes, got {}",
200            HASH_LENGTH,
201            domain_separator.len()
202        )));
203    }
204    if hash_struct.len() != HASH_LENGTH {
205        return Err(SignerError::SigningError(format!(
206            "Invalid hash struct length: expected {} bytes, got {}",
207            HASH_LENGTH,
208            hash_struct.len()
209        )));
210    }
211
212    // Construct EIP-712 message: "\x19\x01" ++ domainSeparator ++ hashStruct(message)
213    // Use fixed-size array instead of Vec for better performance (size known at compile time)
214    let mut eip712_message = [0u8; EIP712_MESSAGE_SIZE];
215    eip712_message[0..2].copy_from_slice(&EIP712_PREFIX);
216    eip712_message[2..34].copy_from_slice(&domain_separator);
217    eip712_message[34..66].copy_from_slice(&hash_struct);
218
219    // Hash the EIP-712 message
220    use alloy::primitives::keccak256;
221    let message_hash = keccak256(eip712_message);
222
223    Ok(message_hash.into())
224}
225
226/// Validates signature length and formats it into a SignDataResponse::Evm
227///
228/// This helper eliminates duplication across all EVM signer implementations by providing
229/// a single point for signature validation and formatting.
230///
231/// # Arguments
232/// * `signature_bytes` - The raw signature bytes (expected to be 65 bytes: r + s + v)
233/// * `signer_name` - Name of the signer for error messages (e.g., "AWS KMS", "Turnkey")
234///
235/// # Returns
236/// * `Ok(SignDataResponse)` - A properly formatted EVM signature response
237/// * `Err(SignerError)` - If the signature length is not 65 bytes
238///
239/// # Format
240/// The signature is split into:
241/// - `r`: First 32 bytes (hex encoded)
242/// - `s`: Next 32 bytes (hex encoded)
243/// - `v`: Last byte (recovery ID, typically 27 or 28)
244/// - `sig`: Full 65 bytes (hex encoded)
245pub(crate) fn validate_and_format_signature(
246    signature_bytes: &[u8],
247    signer_name: &str,
248) -> Result<SignDataResponse, SignerError> {
249    if signature_bytes.len() != SECP256K1_SIGNATURE_LENGTH {
250        return Err(SignerError::SigningError(format!(
251            "Invalid signature length from {}: expected {} bytes, got {}",
252            signer_name,
253            SECP256K1_SIGNATURE_LENGTH,
254            signature_bytes.len()
255        )));
256    }
257
258    let r = hex::encode(&signature_bytes[0..32]);
259    let s = hex::encode(&signature_bytes[32..64]);
260    let v = signature_bytes[64];
261
262    Ok(SignDataResponse::Evm(SignDataResponseEvm {
263        r,
264        s,
265        v,
266        sig: hex::encode(signature_bytes),
267    }))
268}
269
270#[async_trait]
271pub trait DataSignerTrait: Send + Sync {
272    /// Signs arbitrary message data
273    async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, SignerError>;
274
275    /// Signs EIP-712 typed data
276    async fn sign_typed_data(
277        &self,
278        request: SignTypedDataRequest,
279    ) -> Result<SignDataResponse, SignerError>;
280}
281
282pub enum EvmSigner {
283    Local(LocalSigner),
284    Vault(VaultSigner<VaultService>),
285    Turnkey(TurnkeySigner),
286    Cdp(CdpSigner),
287    AwsKms(AwsKmsSigner),
288    AzureKeyVault(AzureKeyVaultSigner),
289    GoogleCloudKms(Box<GoogleCloudKmsSigner>),
290}
291
292#[async_trait]
293impl Signer for EvmSigner {
294    async fn address(&self) -> Result<Address, SignerError> {
295        match self {
296            Self::Local(signer) => signer.address().await,
297            Self::Vault(signer) => signer.address().await,
298            Self::Turnkey(signer) => signer.address().await,
299            Self::Cdp(signer) => signer.address().await,
300            Self::AwsKms(signer) => signer.address().await,
301            Self::AzureKeyVault(signer) => signer.address().await,
302            Self::GoogleCloudKms(signer) => signer.address().await,
303        }
304    }
305
306    async fn sign_transaction(
307        &self,
308        transaction: NetworkTransactionData,
309    ) -> Result<SignTransactionResponse, SignerError> {
310        match self {
311            Self::Local(signer) => signer.sign_transaction(transaction).await,
312            Self::Vault(signer) => signer.sign_transaction(transaction).await,
313            Self::Turnkey(signer) => signer.sign_transaction(transaction).await,
314            Self::Cdp(signer) => signer.sign_transaction(transaction).await,
315            Self::AwsKms(signer) => signer.sign_transaction(transaction).await,
316            Self::AzureKeyVault(signer) => signer.sign_transaction(transaction).await,
317            Self::GoogleCloudKms(signer) => signer.sign_transaction(transaction).await,
318        }
319    }
320}
321
322#[async_trait]
323impl DataSignerTrait for EvmSigner {
324    async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, SignerError> {
325        match self {
326            Self::Local(signer) => signer.sign_data(request).await,
327            Self::Vault(signer) => signer.sign_data(request).await,
328            Self::Turnkey(signer) => signer.sign_data(request).await,
329            Self::Cdp(signer) => signer.sign_data(request).await,
330            Self::AwsKms(signer) => signer.sign_data(request).await,
331            Self::AzureKeyVault(signer) => signer.sign_data(request).await,
332            Self::GoogleCloudKms(signer) => signer.sign_data(request).await,
333        }
334    }
335
336    async fn sign_typed_data(
337        &self,
338        request: SignTypedDataRequest,
339    ) -> Result<SignDataResponse, SignerError> {
340        match self {
341            Self::Local(signer) => signer.sign_typed_data(request).await,
342            Self::Vault(signer) => signer.sign_typed_data(request).await,
343            Self::Turnkey(signer) => signer.sign_typed_data(request).await,
344            Self::Cdp(signer) => signer.sign_typed_data(request).await,
345            Self::AwsKms(signer) => signer.sign_typed_data(request).await,
346            Self::AzureKeyVault(signer) => signer.sign_typed_data(request).await,
347            Self::GoogleCloudKms(signer) => signer.sign_typed_data(request).await,
348        }
349    }
350}
351
352pub struct EvmSignerFactory;
353
354impl EvmSignerFactory {
355    pub async fn create_evm_signer(
356        signer_model: SignerDomainModel,
357    ) -> Result<EvmSigner, SignerFactoryError> {
358        let signer = match &signer_model.config {
359            SignerConfig::Local(_) => EvmSigner::Local(LocalSigner::new(&signer_model)?),
360            SignerConfig::Vault(config) => {
361                let vault_config = VaultConfig::new(
362                    config.address.clone(),
363                    config.role_id.clone(),
364                    config.secret_id.clone(),
365                    config.namespace.clone(),
366                    config
367                        .mount_point
368                        .clone()
369                        .unwrap_or_else(|| "secret".to_string()),
370                    None,
371                );
372                let vault_service = VaultService::new(vault_config);
373
374                EvmSigner::Vault(VaultSigner::new(
375                    signer_model.id.clone(),
376                    config.clone(),
377                    vault_service,
378                ))
379            }
380            SignerConfig::AwsKms(config) => {
381                let aws_service = AwsKmsService::new(config.clone()).await.map_err(|e| {
382                    SignerFactoryError::CreationFailed(format!("AWS KMS service error: {e}"))
383                })?;
384                EvmSigner::AwsKms(AwsKmsSigner::new(aws_service))
385            }
386            SignerConfig::AzureKeyVault(config) => {
387                let azure_service = AzureKeyVaultService::new(config).map_err(|e| {
388                    SignerFactoryError::CreationFailed(format!(
389                        "Azure Key Vault service error: {e}"
390                    ))
391                })?;
392                EvmSigner::AzureKeyVault(AzureKeyVaultSigner::new(azure_service))
393            }
394            SignerConfig::VaultTransit(_) => {
395                return Err(SignerFactoryError::UnsupportedType("Vault Transit".into()));
396            }
397            SignerConfig::Turnkey(config) => {
398                let turnkey_service = TurnkeyService::new(config.clone()).map_err(|e| {
399                    SignerFactoryError::CreationFailed(format!("Turnkey service error: {e}"))
400                })?;
401                EvmSigner::Turnkey(TurnkeySigner::new(turnkey_service))
402            }
403            SignerConfig::Cdp(config) => {
404                let cdp_signer = CdpSigner::new(config.clone()).map_err(|e| {
405                    SignerFactoryError::CreationFailed(format!("CDP service error: {e}"))
406                })?;
407                EvmSigner::Cdp(cdp_signer)
408            }
409            SignerConfig::GoogleCloudKms(config) => {
410                let gcp_service = GoogleCloudKmsService::new(config).map_err(|e| {
411                    SignerFactoryError::CreationFailed(format!(
412                        "Google Cloud KMS service error: {e}"
413                    ))
414                })?;
415                EvmSigner::GoogleCloudKms(Box::new(GoogleCloudKmsSigner::new(gcp_service)))
416            }
417        };
418
419        Ok(signer)
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use crate::models::{
427        AwsKmsSignerConfig, CdpSignerConfig, EvmTransactionData, GoogleCloudKmsSignerConfig,
428        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig,
429        SecretString, SignerConfig, SignerRepoModel, TurnkeySignerConfig, VaultTransitSignerConfig,
430        U256,
431    };
432    use futures;
433    use mockall::predicate::*;
434    use secrets::SecretVec;
435    use std::str::FromStr;
436    use std::sync::Arc;
437
438    fn test_key_bytes() -> SecretVec<u8> {
439        let key_bytes =
440            hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
441                .unwrap();
442        SecretVec::new(key_bytes.len(), |v| v.copy_from_slice(&key_bytes))
443    }
444
445    fn test_key_address() -> Address {
446        Address::Evm([
447            126, 95, 69, 82, 9, 26, 105, 18, 93, 93, 252, 183, 184, 194, 101, 144, 41, 57, 91, 223,
448        ])
449    }
450
451    #[tokio::test]
452    async fn test_create_evm_signer_local() {
453        let signer_model = SignerDomainModel {
454            id: "test".to_string(),
455            config: SignerConfig::Local(LocalSignerConfig {
456                raw_key: test_key_bytes(),
457            }),
458        };
459
460        let signer = EvmSignerFactory::create_evm_signer(signer_model)
461            .await
462            .unwrap();
463
464        assert!(matches!(signer, EvmSigner::Local(_)));
465    }
466
467    #[tokio::test]
468    async fn test_create_evm_signer_test() {
469        let signer_model = SignerDomainModel {
470            id: "test".to_string(),
471            config: SignerConfig::Local(LocalSignerConfig {
472                raw_key: test_key_bytes(),
473            }),
474        };
475
476        let signer = EvmSignerFactory::create_evm_signer(signer_model)
477            .await
478            .unwrap();
479
480        assert!(matches!(signer, EvmSigner::Local(_)));
481    }
482
483    #[tokio::test]
484    async fn test_create_evm_signer_vault() {
485        let signer_model = SignerDomainModel {
486            id: "test".to_string(),
487            config: SignerConfig::Vault(VaultSignerConfig {
488                address: "https://vault.test.com".to_string(),
489                namespace: Some("test-namespace".to_string()),
490                role_id: crate::models::SecretString::new("test-role-id"),
491                secret_id: crate::models::SecretString::new("test-secret-id"),
492                key_name: "test-key".to_string(),
493                mount_point: Some("secret".to_string()),
494            }),
495        };
496
497        let signer = EvmSignerFactory::create_evm_signer(signer_model)
498            .await
499            .unwrap();
500
501        assert!(matches!(signer, EvmSigner::Vault(_)));
502    }
503
504    #[tokio::test]
505    async fn test_create_evm_signer_aws_kms() {
506        let signer_model = SignerDomainModel {
507            id: "test".to_string(),
508            config: SignerConfig::AwsKms(AwsKmsSignerConfig {
509                region: Some("us-east-1".to_string()),
510                key_id: "test-key-id".to_string(),
511            }),
512        };
513
514        let signer = EvmSignerFactory::create_evm_signer(signer_model)
515            .await
516            .unwrap();
517
518        assert!(matches!(signer, EvmSigner::AwsKms(_)));
519    }
520
521    #[tokio::test]
522    async fn test_create_evm_signer_vault_transit() {
523        let signer_model = SignerDomainModel {
524            id: "test".to_string(),
525            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
526                key_name: "test".to_string(),
527                address: "address".to_string(),
528                namespace: None,
529                role_id: SecretString::new("test-role"),
530                secret_id: SecretString::new("test-secret"),
531                pubkey: "pubkey".to_string(),
532                mount_point: None,
533            }),
534        };
535
536        let result = EvmSignerFactory::create_evm_signer(signer_model).await;
537
538        assert!(matches!(
539            result,
540            Err(SignerFactoryError::UnsupportedType(_))
541        ));
542    }
543
544    #[tokio::test]
545    async fn test_create_evm_signer_turnkey() {
546        let signer_model = SignerDomainModel {
547            id: "test".to_string(),
548            config: SignerConfig::Turnkey(TurnkeySignerConfig {
549                api_private_key: SecretString::new("api_private_key"),
550                api_public_key: "api_public_key".to_string(),
551                organization_id: "organization_id".to_string(),
552                private_key_id: "private_key_id".to_string(),
553                public_key: "047d3bb8e0317927700cf19fed34e0627367be1390ec247dddf8c239e4b4321a49aea80090e49b206b6a3e577a4f11d721ab063482001ee10db40d6f2963233eec".to_string(),
554            }),
555        };
556
557        let signer = EvmSignerFactory::create_evm_signer(signer_model)
558            .await
559            .unwrap();
560        let signer_address = signer.address().await.unwrap();
561
562        assert_eq!(
563            "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a",
564            signer_address.to_string()
565        );
566    }
567
568    #[tokio::test]
569    async fn test_create_evm_signer_cdp() {
570        let signer_model = SignerDomainModel {
571            id: "test".to_string(),
572            config: SignerConfig::Cdp(CdpSignerConfig {
573                api_key_id: "test-api-key-id".to_string(),
574                api_key_secret: SecretString::new("test-api-key-secret"),
575                wallet_secret: SecretString::new("test-wallet-secret"),
576                account_address: "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a".to_string(),
577            }),
578        };
579
580        let signer = EvmSignerFactory::create_evm_signer(signer_model)
581            .await
582            .unwrap();
583
584        assert!(matches!(signer, EvmSigner::Cdp(_)));
585    }
586
587    #[tokio::test]
588    async fn test_address_evm_signer_local() {
589        let signer_model = SignerDomainModel {
590            id: "test".to_string(),
591            config: SignerConfig::Local(LocalSignerConfig {
592                raw_key: test_key_bytes(),
593            }),
594        };
595
596        let signer = EvmSignerFactory::create_evm_signer(signer_model)
597            .await
598            .unwrap();
599        let signer_address = signer.address().await.unwrap();
600
601        assert_eq!(test_key_address(), signer_address);
602    }
603
604    #[tokio::test]
605    async fn test_address_evm_signer_test() {
606        let signer_model = SignerDomainModel {
607            id: "test".to_string(),
608            config: SignerConfig::Local(LocalSignerConfig {
609                raw_key: test_key_bytes(),
610            }),
611        };
612
613        let signer = EvmSignerFactory::create_evm_signer(signer_model)
614            .await
615            .unwrap();
616        let signer_address = signer.address().await.unwrap();
617
618        assert_eq!(test_key_address(), signer_address);
619    }
620
621    #[tokio::test]
622    async fn test_address_evm_signer_turnkey() {
623        let signer_model = SignerDomainModel {
624            id: "test".to_string(),
625            config: SignerConfig::Turnkey(TurnkeySignerConfig {
626                api_private_key: SecretString::new("api_private_key"),
627                api_public_key: "api_public_key".to_string(),
628                organization_id: "organization_id".to_string(),
629                private_key_id: "private_key_id".to_string(),
630                public_key: "047d3bb8e0317927700cf19fed34e0627367be1390ec247dddf8c239e4b4321a49aea80090e49b206b6a3e577a4f11d721ab063482001ee10db40d6f2963233eec".to_string(),
631            }),
632        };
633
634        let signer = EvmSignerFactory::create_evm_signer(signer_model)
635            .await
636            .unwrap();
637        let signer_address = signer.address().await.unwrap();
638
639        assert_eq!(
640            "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a",
641            signer_address.to_string()
642        );
643    }
644
645    #[tokio::test]
646    async fn test_address_evm_signer_cdp() {
647        let signer_model = SignerDomainModel {
648            id: "test".to_string(),
649            config: SignerConfig::Cdp(CdpSignerConfig {
650                api_key_id: "test-api-key-id".to_string(),
651                api_key_secret: SecretString::new("test-api-key-secret"),
652                wallet_secret: SecretString::new("test-wallet-secret"),
653                account_address: "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a".to_string(),
654            }),
655        };
656
657        let signer = EvmSignerFactory::create_evm_signer(signer_model)
658            .await
659            .unwrap();
660        let signer_address = signer.address().await.unwrap();
661
662        assert_eq!(
663            "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a",
664            signer_address.to_string()
665        );
666    }
667
668    #[tokio::test]
669    async fn test_sign_data_evm_signer_local() {
670        let signer_model = SignerDomainModel {
671            id: "test".to_string(),
672            config: SignerConfig::Local(LocalSignerConfig {
673                raw_key: test_key_bytes(),
674            }),
675        };
676
677        let signer = EvmSignerFactory::create_evm_signer(signer_model)
678            .await
679            .unwrap();
680        let request = SignDataRequest {
681            message: "Test message".to_string(),
682        };
683
684        let result = signer.sign_data(request).await;
685
686        assert!(result.is_ok());
687
688        let response = result.unwrap();
689        assert!(matches!(response, SignDataResponse::Evm(_)));
690
691        if let SignDataResponse::Evm(sig) = response {
692            assert_eq!(sig.r.len(), 64); // 32 bytes in hex
693            assert_eq!(sig.s.len(), 64); // 32 bytes in hex
694            assert!(sig.v == 27 || sig.v == 28); // Valid v values
695            assert_eq!(sig.sig.len(), 130); // 65 bytes in hex
696        }
697    }
698
699    #[tokio::test]
700    async fn test_sign_transaction_evm() {
701        let signer_model = SignerDomainModel {
702            id: "test".to_string(),
703            config: SignerConfig::Local(LocalSignerConfig {
704                raw_key: test_key_bytes(),
705            }),
706        };
707
708        let signer = EvmSignerFactory::create_evm_signer(signer_model)
709            .await
710            .unwrap();
711
712        let transaction_data = NetworkTransactionData::Evm(EvmTransactionData {
713            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
714            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
715            gas_price: Some(20000000000),
716            gas_limit: Some(21000),
717            nonce: Some(0),
718            value: U256::from(1000000000000000000u64),
719            data: Some("0x".to_string()),
720            chain_id: 1,
721            hash: None,
722            signature: None,
723            raw: None,
724            max_fee_per_gas: None,
725            max_priority_fee_per_gas: None,
726            speed: None,
727        });
728
729        let result = signer.sign_transaction(transaction_data).await;
730
731        assert!(result.is_ok());
732
733        let signed_tx = result.unwrap();
734
735        assert!(matches!(signed_tx, SignTransactionResponse::Evm(_)));
736
737        if let SignTransactionResponse::Evm(evm_tx) = signed_tx {
738            assert!(!evm_tx.hash.is_empty());
739            assert!(!evm_tx.raw.is_empty());
740            assert!(!evm_tx.signature.sig.is_empty());
741        }
742    }
743
744    #[tokio::test]
745    async fn test_create_evm_signer_google_cloud_kms() {
746        let signer_model = SignerDomainModel {
747            id: "test".to_string(),
748            config: SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
749                service_account: GoogleCloudKmsSignerServiceAccountConfig {
750                    project_id: SecretString::new("project_id"),
751                    private_key_id: SecretString::new("private_key_id"),
752                    private_key: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
753                    client_email: SecretString::new("client_email@example.com"),
754                    client_id: SecretString::new("client_id"),
755                    auth_uri: SecretString::new("https://accounts.google.com/o/oauth2/auth"),
756                    token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
757                    auth_provider_x509_cert_url: SecretString::new("https://www.googleapis.com/oauth2/v1/certs"),
758                    client_x509_cert_url: SecretString::new("https://www.googleapis.com/robot/v1/metadata/x509/client_email%40example.com"),
759                    universe_domain: SecretString::new("googleapis.com"),
760                },
761                key: GoogleCloudKmsSignerKeyConfig {
762                    location: SecretString::new("global"),
763                    key_id: SecretString::new("id"),
764                    key_ring_id: SecretString::new("key_ring"),
765                    key_version: 1,
766                },
767            })),
768        };
769
770        let result = EvmSignerFactory::create_evm_signer(signer_model).await;
771
772        assert!(result.is_ok());
773        assert!(matches!(result.unwrap(), EvmSigner::GoogleCloudKms(_)));
774    }
775
776    #[tokio::test]
777    async fn test_sign_data_with_different_message_types() {
778        let signer_model = SignerDomainModel {
779            id: "test".to_string(),
780            config: SignerConfig::Local(LocalSignerConfig {
781                raw_key: test_key_bytes(),
782            }),
783        };
784
785        let signer = EvmSignerFactory::create_evm_signer(signer_model)
786            .await
787            .unwrap();
788
789        // Test with various message types
790        let long_message = "a".repeat(1000);
791        let test_cases = vec![
792            ("Simple message", "Test message"),
793            ("Empty message", ""),
794            ("Unicode message", "🚀 Test message with émojis"),
795            ("Long message", long_message.as_str()),
796            ("JSON message", r#"{"test": "value", "number": 123}"#),
797        ];
798
799        for (name, message) in test_cases {
800            let request = SignDataRequest {
801                message: message.to_string(),
802            };
803
804            let result = signer.sign_data(request).await;
805            assert!(result.is_ok(), "Failed to sign {name}");
806
807            if let Ok(SignDataResponse::Evm(sig)) = result {
808                assert_eq!(sig.r.len(), 64, "Invalid r length for {name}");
809                assert_eq!(sig.s.len(), 64, "Invalid s length for {name}");
810                assert!(sig.v == 27 || sig.v == 28, "Invalid v value for {name}");
811                assert_eq!(sig.sig.len(), 130, "Invalid signature length for {name}");
812            } else {
813                panic!("Expected EVM signature for {name}");
814            }
815        }
816    }
817
818    // Edge case tests for EIP-712 and signature validation
819    #[tokio::test]
820    async fn test_eip712_hash_with_0x_prefix() {
821        let request_with_prefix = SignTypedDataRequest {
822            domain_separator: format!("0x{}", "a".repeat(64)),
823            hash_struct_message: format!("0x{}", "b".repeat(64)),
824        };
825
826        let request_without_prefix = SignTypedDataRequest {
827            domain_separator: "a".repeat(64),
828            hash_struct_message: "b".repeat(64),
829        };
830
831        let hash1 = construct_eip712_message_hash(&request_with_prefix).unwrap();
832        let hash2 = construct_eip712_message_hash(&request_without_prefix).unwrap();
833
834        assert_eq!(
835            hash1, hash2,
836            "Hash should be same with or without 0x prefix"
837        );
838    }
839
840    #[tokio::test]
841    async fn test_eip712_deterministic() {
842        let request = SignTypedDataRequest {
843            domain_separator: "a".repeat(64),
844            hash_struct_message: "b".repeat(64),
845        };
846
847        let hash1 = construct_eip712_message_hash(&request).unwrap();
848        let hash2 = construct_eip712_message_hash(&request).unwrap();
849
850        assert_eq!(hash1, hash2, "Hash must be deterministic");
851    }
852
853    #[tokio::test]
854    async fn test_eip712_invalid_domain_length() {
855        let request = SignTypedDataRequest {
856            domain_separator: "a".repeat(30), // Too short
857            hash_struct_message: "b".repeat(64),
858        };
859
860        let result = construct_eip712_message_hash(&request);
861        assert!(result.is_err());
862        if let Err(e) = result {
863            assert!(e.to_string().contains("Invalid domain separator length"));
864        }
865    }
866
867    #[tokio::test]
868    async fn test_eip712_invalid_hash_struct_length() {
869        let request = SignTypedDataRequest {
870            domain_separator: "a".repeat(64),
871            hash_struct_message: "b".repeat(30), // Too short
872        };
873
874        let result = construct_eip712_message_hash(&request);
875        assert!(result.is_err());
876        if let Err(e) = result {
877            assert!(e.to_string().contains("Invalid hash struct length"));
878        }
879    }
880
881    #[tokio::test]
882    async fn test_eip712_invalid_hex_characters() {
883        let request = SignTypedDataRequest {
884            domain_separator: "zzzz".to_string(), // Invalid hex
885            hash_struct_message: "b".repeat(64),
886        };
887
888        let result = construct_eip712_message_hash(&request);
889        assert!(result.is_err());
890        if let Err(e) = result {
891            assert!(e.to_string().contains("non-hexadecimal character"));
892        }
893    }
894
895    #[tokio::test]
896    async fn test_eip712_invalid_hex_at_specific_position() {
897        let request = SignTypedDataRequest {
898            domain_separator: format!("{}z{}", "a".repeat(10), "a".repeat(53)), // 'z' at position 10
899            hash_struct_message: "b".repeat(64),
900        };
901
902        let result = construct_eip712_message_hash(&request);
903        assert!(result.is_err());
904        if let Err(e) = result {
905            let err_msg = e.to_string();
906            assert!(err_msg.contains("non-hexadecimal character"));
907            assert!(err_msg.contains("position 10")); // Error should report exact position
908        }
909    }
910
911    #[test]
912    fn test_signature_validation_wrong_length() {
913        let sig_64_bytes = vec![0u8; 64];
914        let result = validate_and_format_signature(&sig_64_bytes, "TestSigner");
915        assert!(result.is_err());
916        if let Err(e) = result {
917            assert!(e.to_string().contains("expected 65 bytes"));
918        }
919
920        let sig_66_bytes = vec![0u8; 66];
921        let result = validate_and_format_signature(&sig_66_bytes, "TestSigner");
922        assert!(result.is_err());
923
924        let sig_0_bytes = vec![];
925        let result = validate_and_format_signature(&sig_0_bytes, "TestSigner");
926        assert!(result.is_err());
927    }
928
929    #[test]
930    fn test_signature_validation_correct_length() {
931        let sig_65_bytes = vec![0u8; 65];
932        let result = validate_and_format_signature(&sig_65_bytes, "TestSigner");
933        assert!(result.is_ok());
934
935        if let Ok(SignDataResponse::Evm(sig)) = result {
936            assert_eq!(sig.r.len(), 64); // 32 bytes as hex
937            assert_eq!(sig.s.len(), 64); // 32 bytes as hex
938            assert_eq!(sig.v, 0); // Last byte
939            assert_eq!(sig.sig.len(), 130); // 65 bytes as hex
940        }
941    }
942
943    #[tokio::test]
944    async fn test_eip712_odd_length_hex_string() {
945        // Odd-length hex strings should fail during decoding
946        let request = SignTypedDataRequest {
947            domain_separator: "a".repeat(63), // Odd length
948            hash_struct_message: "b".repeat(64),
949        };
950
951        let result = construct_eip712_message_hash(&request);
952        assert!(result.is_err());
953    }
954
955    #[tokio::test]
956    async fn test_eip712_mixed_case_hex() {
957        // Mixed case hex should work
958        let request = SignTypedDataRequest {
959            domain_separator: "AaBbCcDdEeFf11223344556677889900AaBbCcDdEeFf11223344556677889900"
960                .to_string(),
961            hash_struct_message: "b".repeat(64),
962        };
963
964        let result = construct_eip712_message_hash(&request);
965        assert!(result.is_ok());
966    }
967
968    #[tokio::test]
969    async fn test_validate_and_decode_hex_error_includes_input_preview() {
970        let long_invalid_hex = format!("{}z{}", "a".repeat(20), "a".repeat(100));
971        let result = validate_and_decode_hex(&long_invalid_hex, "test_field");
972
973        assert!(result.is_err());
974        if let Err(e) = result {
975            let err_msg = e.to_string();
976            // Should include first 16 chars of input for debugging
977            assert!(err_msg.contains("input:"));
978            assert!(err_msg.len() < 200); // Error message should be reasonably sized
979        }
980    }
981}