openzeppelin_relayer/services/cdp/
mod.rs

1//! # CDP Service Module
2//!
3//! This module provides integration with CDP API for secure wallet management
4//! and cryptographic operations.
5//!
6//! ## Features
7//!
8//! - API key-based authentication via WalletAuth
9//! - Digital signature generation for EVM
10//! - Message signing via CDP API
11//! - Secure transaction signing for blockchain operations
12//!
13//! ## Architecture
14//!
15//! ```text
16//! CdpService (implements CdpServiceTrait)
17//!   ├── Authentication (WalletAuth)
18//!   ├── Transaction Signing
19//!   └── Raw Payload Signing
20//! ```
21use async_trait::async_trait;
22use base64::{engine::general_purpose, Engine as _};
23use reqwest_middleware::ClientBuilder;
24use std::{str, time::Duration};
25use thiserror::Error;
26
27use crate::models::{Address, CdpSignerConfig};
28
29use cdp_sdk::{auth::WalletAuth, types, Client, CDP_BASE_URL};
30
31#[derive(Error, Debug, serde::Serialize)]
32pub enum CdpError {
33    #[error("HTTP error: {0}")]
34    HttpError(String),
35
36    #[error("Authentication failed: {0}")]
37    AuthenticationFailed(String),
38
39    #[error("Configuration error: {0}")]
40    ConfigError(String),
41
42    #[error("Signing error: {0}")]
43    SigningError(String),
44
45    #[error("Serialization error: {0}")]
46    SerializationError(String),
47
48    #[error("Invalid signature: {0}")]
49    SignatureError(String),
50
51    #[error("Other error: {0}")]
52    OtherError(String),
53}
54
55/// Result type for CDP operations
56pub type CdpResult<T> = Result<T, CdpError>;
57
58#[cfg(test)]
59use mockall::automock;
60
61#[async_trait]
62#[cfg_attr(test, automock)]
63pub trait CdpServiceTrait: Send + Sync {
64    /// Returns the EVM or Solana address for the configured account
65    async fn account_address(&self) -> Result<Address, CdpError>;
66
67    /// Signs a message using the EVM signing scheme
68    async fn sign_evm_message(&self, message: String) -> Result<Vec<u8>, CdpError>;
69
70    /// Signs an EVM transaction using the CDP API
71    async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, CdpError>;
72
73    /// Signs a message using Solana signing scheme
74    async fn sign_solana_message(&self, message: &[u8]) -> Result<Vec<u8>, CdpError>;
75
76    /// Signs a transaction using Solana signing scheme
77    async fn sign_solana_transaction(&self, message: String) -> Result<Vec<u8>, CdpError>;
78}
79
80#[derive(Clone, Debug)]
81pub struct CdpService {
82    pub config: CdpSignerConfig,
83    pub client: Client,
84}
85
86impl CdpService {
87    pub fn new(config: CdpSignerConfig) -> Result<Self, CdpError> {
88        // Initialize the CDP client with WalletAuth middleware, which is required for signing operations
89        let wallet_auth = WalletAuth::builder()
90            .api_key_id(config.api_key_id.clone())
91            .api_key_secret(config.api_key_secret.to_str().to_string())
92            .wallet_secret(config.wallet_secret.to_str().to_string())
93            .source("openzeppelin-relayer".to_string())
94            .source_version(env!("CARGO_PKG_VERSION").to_string())
95            .build()
96            .map_err(|e| CdpError::ConfigError(format!("Invalid CDP configuration: {e}")))?;
97
98        let inner = reqwest::Client::builder()
99            .connect_timeout(Duration::from_secs(5))
100            .timeout(Duration::from_secs(10))
101            .build()
102            .map_err(|e| CdpError::ConfigError(format!("Failed to build HTTP client: {e}")))?;
103        let wallet_client = ClientBuilder::new(inner).with(wallet_auth).build();
104        let client = Client::new_with_client(CDP_BASE_URL, wallet_client);
105        Ok(Self { config, client })
106    }
107
108    /// Get the configured account address
109    fn get_account_address(&self) -> &str {
110        &self.config.account_address
111    }
112
113    /// Check if the configured address is an EVM address (0x-prefixed hex)
114    fn is_evm_address(&self) -> bool {
115        self.config.account_address.starts_with("0x")
116    }
117
118    /// Check if the configured address is a Solana address (Base58)
119    fn is_solana_address(&self) -> bool {
120        !self.config.account_address.starts_with("0x")
121    }
122
123    /// Converts a CDP address to our Address type, auto-detecting format
124    fn address_from_string(&self, address_str: &str) -> Result<Address, CdpError> {
125        if address_str.starts_with("0x") {
126            // EVM address (hex)
127            let hex_str = address_str.strip_prefix("0x").unwrap();
128
129            // Decode hex string to bytes
130            let bytes = hex::decode(hex_str)
131                .map_err(|e| CdpError::ConfigError(format!("Invalid hex address: {e}")))?;
132
133            if bytes.len() != 20 {
134                return Err(CdpError::ConfigError(format!(
135                    "EVM address should be 20 bytes, got {} bytes",
136                    bytes.len()
137                )));
138            }
139
140            let mut array = [0u8; 20];
141            array.copy_from_slice(&bytes);
142
143            Ok(Address::Evm(array))
144        } else {
145            // Solana address (Base58)
146            Ok(Address::Solana(address_str.to_string()))
147        }
148    }
149}
150
151#[async_trait]
152impl CdpServiceTrait for CdpService {
153    async fn account_address(&self) -> Result<Address, CdpError> {
154        let address_str = self.get_account_address();
155        self.address_from_string(address_str)
156    }
157
158    async fn sign_evm_message(&self, message: String) -> Result<Vec<u8>, CdpError> {
159        if !self.is_evm_address() {
160            return Err(CdpError::ConfigError(
161                "Account address is not an EVM address (must start with 0x)".to_string(),
162            ));
163        }
164        let address = self.get_account_address();
165
166        let message_body = types::SignEvmMessageBody::builder().message(message);
167
168        let response = self
169            .client
170            .sign_evm_message()
171            .address(address)
172            .x_wallet_auth("") // Added by WalletAuth middleware.
173            .body(message_body)
174            .send()
175            .await
176            .map_err(|e| CdpError::SigningError(format!("Failed to sign message: {e}")))?;
177
178        let result = response.into_inner();
179
180        // Parse the signature hex string to bytes
181        let signature_bytes = hex::decode(
182            result
183                .signature
184                .strip_prefix("0x")
185                .unwrap_or(&result.signature),
186        )
187        .map_err(|e| CdpError::SigningError(format!("Invalid signature hex: {e}")))?;
188
189        Ok(signature_bytes)
190    }
191
192    async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, CdpError> {
193        if !self.is_evm_address() {
194            return Err(CdpError::ConfigError(
195                "Account address is not an EVM address (must start with 0x)".to_string(),
196            ));
197        }
198        let address = self.get_account_address();
199
200        // Convert transaction bytes to hex string for CDP API
201        let hex_encoded = hex::encode(message);
202
203        let tx_body =
204            types::SignEvmTransactionBody::builder().transaction(format!("0x{hex_encoded}"));
205
206        let response = self
207            .client
208            .sign_evm_transaction()
209            .address(address)
210            .x_wallet_auth("")
211            .body(tx_body)
212            .send()
213            .await
214            .map_err(|e| CdpError::SigningError(format!("Failed to sign transaction: {e}")))?;
215
216        let result = response.into_inner();
217
218        // Parse the signed transaction hex string to bytes
219        let signed_tx_bytes = hex::decode(
220            result
221                .signed_transaction
222                .strip_prefix("0x")
223                .unwrap_or(&result.signed_transaction),
224        )
225        .map_err(|e| CdpError::SigningError(format!("Invalid signed transaction hex: {e}")))?;
226
227        Ok(signed_tx_bytes)
228    }
229
230    async fn sign_solana_message(&self, message: &[u8]) -> Result<Vec<u8>, CdpError> {
231        if !self.is_solana_address() {
232            return Err(CdpError::ConfigError(
233                "Account address is not a Solana address (must not start with 0x)".to_string(),
234            ));
235        }
236        let address = self.get_account_address();
237        let encoded_message = str::from_utf8(message)
238            .map_err(|e| CdpError::SerializationError(format!("Invalid UTF-8 message: {e}")))?
239            .to_string();
240
241        let message_body = types::SignSolanaMessageBody::builder().message(encoded_message);
242
243        let response = self
244            .client
245            .sign_solana_message()
246            .address(address)
247            .x_wallet_auth("") // Added by WalletAuth middleware.
248            .body(message_body)
249            .send()
250            .await
251            .map_err(|e| CdpError::SigningError(format!("Failed to sign Solana message: {e}")))?;
252
253        let result = response.into_inner();
254
255        // Parse the signature base58 string to bytes
256        let signature_bytes = bs58::decode(result.signature)
257            .into_vec()
258            .map_err(|e| CdpError::SigningError(format!("Invalid Solana signature base58: {e}")))?;
259
260        Ok(signature_bytes)
261    }
262
263    async fn sign_solana_transaction(&self, transaction: String) -> Result<Vec<u8>, CdpError> {
264        if !self.is_solana_address() {
265            return Err(CdpError::ConfigError(
266                "Account address is not a Solana address (must not start with 0x)".to_string(),
267            ));
268        }
269        let address = self.get_account_address();
270
271        let message_body = types::SignSolanaTransactionBody::builder().transaction(transaction);
272
273        let response = self
274            .client
275            .sign_solana_transaction()
276            .address(address)
277            .x_wallet_auth("") // Added by WalletAuth middleware.
278            .body(message_body)
279            .send()
280            .await
281            .map_err(|e| CdpError::SigningError(format!("Failed to sign Solana transaction: {e}")))?;
282
283        let result = response.into_inner();
284
285        // Parse the signed transaction base64 string to bytes
286        let signature_bytes = general_purpose::STANDARD
287            .decode(result.signed_transaction)
288            .map_err(|e| {
289                CdpError::SigningError(format!("Invalid Solana signed transaction base64: {e}"))
290            })?;
291
292        Ok(signature_bytes)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::models::SecretString;
300    use mockito;
301    use serde_json::json;
302
303    fn create_test_config_evm() -> CdpSignerConfig {
304        CdpSignerConfig {
305            api_key_id: "test-api-key-id".to_string(),
306            api_key_secret: SecretString::new("test-api-key-secret"),
307            wallet_secret: SecretString::new("test-wallet-secret"),
308            account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
309        }
310    }
311
312    fn create_test_config_solana() -> CdpSignerConfig {
313        CdpSignerConfig {
314            api_key_id: "test-api-key-id".to_string(),
315            api_key_secret: SecretString::new("test-api-key-secret"),
316            wallet_secret: SecretString::new("test-wallet-secret"),
317            account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
318        }
319    }
320
321    // Helper function to create a test client with middleware
322    fn create_test_client() -> reqwest_middleware::ClientWithMiddleware {
323        let inner = reqwest::ClientBuilder::new()
324            .redirect(reqwest::redirect::Policy::none())
325            .build()
326            .unwrap();
327        reqwest_middleware::ClientBuilder::new(inner).build()
328    }
329
330    // Setup mock for EVM message signing
331    async fn setup_mock_sign_evm_message(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
332        mock_server
333            .mock("POST", mockito::Matcher::Regex(r".*/v2/evm/accounts/.*/sign/message".to_string()))
334            .match_header("Content-Type", "application/json")
335            .with_status(200)
336            .with_header("content-type", "application/json")
337            .with_body(serde_json::to_string(&json!({
338                "signature": "0x3045022100abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456789002201234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
339            })).unwrap())
340            .expect(1)
341            .create_async()
342            .await
343    }
344
345    // Setup mock for EVM transaction signing
346    async fn setup_mock_sign_evm_transaction(
347        mock_server: &mut mockito::ServerGuard,
348    ) -> mockito::Mock {
349        mock_server
350            .mock("POST", mockito::Matcher::Regex(r".*/v2/evm/accounts/.*/sign/transaction".to_string()))
351            .match_header("Content-Type", "application/json")
352            .with_status(200)
353            .with_header("content-type", "application/json")
354            .with_body(serde_json::to_string(&json!({
355                "signedTransaction": "0x02f87001020304050607080910111213141516171819202122232425262728293031"
356            })).unwrap())
357            .expect(1)
358            .create_async()
359            .await
360    }
361
362    // Setup mock for Solana message signing
363    async fn setup_mock_sign_solana_message(
364        mock_server: &mut mockito::ServerGuard,
365    ) -> mockito::Mock {
366        mock_server
367            .mock("POST", mockito::Matcher::Regex(r".*/v2/solana/accounts/.*/sign/message".to_string()))
368            .match_header("Content-Type", "application/json")
369            .with_status(200)
370            .with_header("content-type", "application/json")
371            .with_body(serde_json::to_string(&json!({
372                "signature": "5VERuXP42jC4Uxo1Rc3eLQgFaQGYdM9ZJvqK3JmZ6vxGz4s8FJ7KHkQpE3cN8RuQ2mW6tX9Y5K2P1VcZqL8TfABC3X"
373            })).unwrap())
374            .expect(1)
375            .create_async()
376            .await
377    }
378
379    // Setup mock for Solana transaction signing
380    async fn setup_mock_sign_solana_transaction(
381        mock_server: &mut mockito::ServerGuard,
382    ) -> mockito::Mock {
383        mock_server
384            .mock(
385                "POST",
386                mockito::Matcher::Regex(r".*/v2/solana/accounts/.*/sign/transaction".to_string()),
387            )
388            .match_header("Content-Type", "application/json")
389            .with_status(200)
390            .with_header("content-type", "application/json")
391            .with_body(
392                serde_json::to_string(&json!({
393                    "signedTransaction": "SGVsbG8gV29ybGQh"  // Base64 encoded test data
394                }))
395                .unwrap(),
396            )
397            .expect(1)
398            .create_async()
399            .await
400    }
401
402    // Setup mock for error responses - 400 Bad Request
403    async fn setup_mock_error_400_malformed_transaction(
404        mock_server: &mut mockito::ServerGuard,
405        path_pattern: &str,
406    ) -> mockito::Mock {
407        mock_server
408            .mock("POST", mockito::Matcher::Regex(path_pattern.to_string()))
409            .match_header("Content-Type", "application/json")
410            .with_status(400)
411            .with_header("content-type", "application/json")
412            .with_body(
413                serde_json::to_string(&json!({
414                    "errorType": "malformed_transaction",
415                    "errorMessage": "Malformed unsigned transaction."
416                }))
417                .unwrap(),
418            )
419            .expect(1)
420            .create_async()
421            .await
422    }
423
424    // Setup mock for error responses - 401 Unauthorized
425    async fn setup_mock_error_401_unauthorized(
426        mock_server: &mut mockito::ServerGuard,
427        path_pattern: &str,
428    ) -> mockito::Mock {
429        mock_server
430            .mock("POST", mockito::Matcher::Regex(path_pattern.to_string()))
431            .match_header("Content-Type", "application/json")
432            .with_status(401)
433            .with_header("content-type", "application/json")
434            .with_body(
435                serde_json::to_string(&json!({
436                    "errorType": "unauthorized",
437                    "errorMessage": "Invalid API credentials."
438                }))
439                .unwrap(),
440            )
441            .expect(1)
442            .create_async()
443            .await
444    }
445
446    // Setup mock for error responses - 500 Internal Server Error
447    async fn setup_mock_error_500_internal_error(
448        mock_server: &mut mockito::ServerGuard,
449        path_pattern: &str,
450    ) -> mockito::Mock {
451        mock_server
452            .mock("POST", mockito::Matcher::Regex(path_pattern.to_string()))
453            .match_header("Content-Type", "application/json")
454            .with_status(500)
455            .with_header("content-type", "application/json")
456            .with_body(
457                serde_json::to_string(&json!({
458                    "errorType": "internal_error",
459                    "errorMessage": "Internal server error occurred."
460                }))
461                .unwrap(),
462            )
463            .expect(1)
464            .create_async()
465            .await
466    }
467
468    // Setup mock for error responses - 422 Unprocessable Entity
469    async fn setup_mock_error_422_invalid_signature(
470        mock_server: &mut mockito::ServerGuard,
471        path_pattern: &str,
472    ) -> mockito::Mock {
473        mock_server
474            .mock("POST", mockito::Matcher::Regex(path_pattern.to_string()))
475            .match_header("Content-Type", "application/json")
476            .with_status(422)
477            .with_header("content-type", "application/json")
478            .with_body(
479                serde_json::to_string(&json!({
480                    "errorType": "invalid_signature_request",
481                    "errorMessage": "Unable to process signature request."
482                }))
483                .unwrap(),
484            )
485            .expect(1)
486            .create_async()
487            .await
488    }
489
490    #[test]
491    fn test_new_cdp_service_valid_config() {
492        let config = create_test_config_evm();
493        let result = CdpService::new(config);
494
495        // Service creation should succeed with valid config
496        assert!(result.is_ok());
497    }
498
499    #[test]
500    fn test_get_account_address() {
501        let config = create_test_config_evm();
502        let service = CdpService::new(config).unwrap();
503
504        let address = service.get_account_address();
505        assert_eq!(address, "0x742d35Cc6634C0532925a3b844Bc454e4438f44f");
506    }
507
508    #[test]
509    fn test_is_evm_address() {
510        let config = create_test_config_evm();
511        let service = CdpService::new(config).unwrap();
512        assert!(service.is_evm_address());
513        assert!(!service.is_solana_address());
514    }
515
516    #[test]
517    fn test_is_solana_address() {
518        let config = create_test_config_solana();
519        let service = CdpService::new(config).unwrap();
520        assert!(service.is_solana_address());
521        assert!(!service.is_evm_address());
522    }
523
524    #[tokio::test]
525    async fn test_address_evm_success() {
526        let config = create_test_config_evm();
527        let service = CdpService::new(config).unwrap();
528        let result = service.account_address().await;
529
530        assert!(result.is_ok());
531        match result.unwrap() {
532            Address::Evm(addr) => {
533                // Verify the address bytes match expected values
534                let expected = [
535                    0x74, 0x2d, 0x35, 0xcc, 0x66, 0x34, 0xC0, 0x53, 0x29, 0x25, 0xa3, 0xb8, 0x44,
536                    0xbc, 0x45, 0x4e, 0x44, 0x38, 0xf4, 0x4f,
537                ];
538                assert_eq!(addr, expected);
539            }
540            _ => panic!("Expected EVM address"),
541        }
542    }
543
544    #[tokio::test]
545    async fn test_address_solana_success() {
546        let config = create_test_config_solana();
547        let service = CdpService::new(config).unwrap();
548        let result = service.account_address().await;
549
550        assert!(result.is_ok());
551        match result.unwrap() {
552            Address::Solana(addr) => {
553                assert_eq!(addr, "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2");
554            }
555            _ => panic!("Expected Solana address"),
556        }
557    }
558
559    #[test]
560    fn test_address_from_string_valid_evm_address() {
561        let config = create_test_config_evm();
562        let service = CdpService::new(config).unwrap();
563
564        let test_address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44f";
565        let result = service.address_from_string(test_address);
566
567        assert!(result.is_ok());
568        match result.unwrap() {
569            Address::Evm(addr) => {
570                let expected = [
571                    0x74, 0x2d, 0x35, 0xcc, 0x66, 0x34, 0xC0, 0x53, 0x29, 0x25, 0xa3, 0xb8, 0x44,
572                    0xbc, 0x45, 0x4e, 0x44, 0x38, 0xf4, 0x4f,
573                ];
574                assert_eq!(addr, expected);
575            }
576            _ => panic!("Expected EVM address"),
577        }
578    }
579
580    #[test]
581    fn test_address_from_string_valid_solana_address() {
582        let config = create_test_config_solana();
583        let service = CdpService::new(config).unwrap();
584
585        let test_address = "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2";
586        let result = service.address_from_string(test_address);
587
588        assert!(result.is_ok());
589        match result.unwrap() {
590            Address::Solana(addr) => {
591                assert_eq!(addr, "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2");
592            }
593            _ => panic!("Expected Solana address"),
594        }
595    }
596
597    #[test]
598    fn test_address_from_string_without_0x_prefix() {
599        let config = create_test_config_evm();
600        let service = CdpService::new(config).unwrap();
601
602        let test_address = "742d35Cc6634C0532925a3b844Bc454e4438f44f";
603        let result = service.address_from_string(test_address);
604
605        // Without 0x prefix, it should be treated as Solana address
606        assert!(result.is_ok());
607        match result.unwrap() {
608            Address::Solana(addr) => {
609                assert_eq!(addr, "742d35Cc6634C0532925a3b844Bc454e4438f44f");
610            }
611            _ => panic!("Expected Solana address"),
612        }
613    }
614
615    #[test]
616    fn test_address_from_string_invalid_hex() {
617        let config = create_test_config_evm();
618        let service = CdpService::new(config).unwrap();
619
620        let test_address = "0xnot_valid_hex";
621        let result = service.address_from_string(test_address);
622
623        assert!(result.is_err());
624        match result {
625            Err(CdpError::ConfigError(msg)) => {
626                assert!(msg.contains("Invalid hex address"));
627            }
628            _ => panic!("Expected ConfigError for invalid hex"),
629        }
630    }
631
632    #[test]
633    fn test_address_from_string_wrong_length() {
634        let config = create_test_config_evm();
635        let service = CdpService::new(config).unwrap();
636
637        let test_address = "0x742d35Cc"; // Too short
638        let result = service.address_from_string(test_address);
639
640        assert!(result.is_err());
641        match result {
642            Err(CdpError::ConfigError(msg)) => {
643                assert!(msg.contains("EVM address should be 20 bytes"));
644            }
645            _ => panic!("Expected ConfigError for wrong length"),
646        }
647    }
648
649    #[test]
650    fn test_cdp_error_display() {
651        let errors = [
652            CdpError::HttpError("HTTP error".to_string()),
653            CdpError::AuthenticationFailed("Auth failed".to_string()),
654            CdpError::ConfigError("Config error".to_string()),
655            CdpError::SigningError("Signing error".to_string()),
656            CdpError::SerializationError("Serialization error".to_string()),
657            CdpError::SignatureError("Signature error".to_string()),
658            CdpError::OtherError("Other error".to_string()),
659        ];
660
661        for error in errors {
662            let error_str = error.to_string();
663            assert!(!error_str.is_empty());
664        }
665    }
666
667    #[tokio::test]
668    async fn test_sign_evm_message_success() {
669        let mut mock_server = mockito::Server::new_async().await;
670        let _mock = setup_mock_sign_evm_message(&mut mock_server).await;
671
672        let config = create_test_config_evm();
673        let client = Client::new_with_client(&mock_server.url(), create_test_client());
674
675        let service = CdpService { config, client };
676
677        let message = "Hello World!".to_string();
678        let result = service.sign_evm_message(message).await;
679
680        match result {
681            Ok(signature) => {
682                assert!(!signature.is_empty());
683            }
684            Err(e) => {
685                panic!("Expected success but got error: {:?}", e);
686            }
687        }
688    }
689
690    #[tokio::test]
691    async fn test_sign_evm_message_wrong_address_type() {
692        let config = create_test_config_solana(); // Solana address for EVM signing
693        let client = Client::new_with_client("http://test", create_test_client());
694        let service = CdpService { config, client };
695
696        let message = "Hello World!".to_string();
697        let result = service.sign_evm_message(message).await;
698
699        assert!(result.is_err());
700        match result {
701            Err(CdpError::ConfigError(msg)) => {
702                assert!(msg.contains("Account address is not an EVM address"));
703            }
704            _ => panic!("Expected ConfigError for wrong address type"),
705        }
706    }
707
708    #[tokio::test]
709    async fn test_sign_evm_transaction_success() {
710        let mut mock_server = mockito::Server::new_async().await;
711        let _mock = setup_mock_sign_evm_transaction(&mut mock_server).await;
712
713        let config = create_test_config_evm();
714        let client = Client::new_with_client(&mock_server.url(), create_test_client());
715
716        let service = CdpService { config, client };
717
718        let transaction_bytes = b"test transaction data";
719        let result = service.sign_evm_transaction(transaction_bytes).await;
720
721        match result {
722            Ok(signed_tx) => {
723                assert!(!signed_tx.is_empty());
724            }
725            Err(e) => {
726                panic!("Expected success but got error: {:?}", e);
727            }
728        }
729    }
730
731    #[tokio::test]
732    async fn test_sign_evm_transaction_wrong_address_type() {
733        let config = create_test_config_solana(); // Solana address for EVM signing
734        let client = Client::new_with_client("http://test", create_test_client());
735        let service = CdpService { config, client };
736
737        let transaction_bytes = b"test transaction data";
738        let result = service.sign_evm_transaction(transaction_bytes).await;
739
740        assert!(result.is_err());
741        match result {
742            Err(CdpError::ConfigError(msg)) => {
743                assert!(msg.contains("Account address is not an EVM address"));
744            }
745            _ => panic!("Expected ConfigError for wrong address type"),
746        }
747    }
748
749    #[tokio::test]
750    async fn test_sign_solana_message_success() {
751        let mut mock_server = mockito::Server::new_async().await;
752        let _mock = setup_mock_sign_solana_message(&mut mock_server).await;
753
754        let config = create_test_config_solana();
755        let client = Client::new_with_client(&mock_server.url(), create_test_client());
756
757        let service = CdpService { config, client };
758
759        let message_bytes = b"Hello Solana!";
760        let result = service.sign_solana_message(message_bytes).await;
761
762        assert!(result.is_ok());
763        let signature = result.unwrap();
764        assert!(!signature.is_empty());
765    }
766
767    #[tokio::test]
768    async fn test_sign_solana_message_wrong_address_type() {
769        let config = create_test_config_evm(); // EVM address for Solana signing
770        let client = Client::new_with_client("http://test", create_test_client());
771        let service = CdpService { config, client };
772
773        let message_bytes = b"Hello Solana!";
774        let result = service.sign_solana_message(message_bytes).await;
775
776        assert!(result.is_err());
777        match result {
778            Err(CdpError::ConfigError(msg)) => {
779                assert!(msg.contains("Account address is not a Solana address"));
780            }
781            _ => panic!("Expected ConfigError for wrong address type"),
782        }
783    }
784
785    #[tokio::test]
786    async fn test_sign_solana_transaction_success() {
787        let mut mock_server = mockito::Server::new_async().await;
788        let _mock = setup_mock_sign_solana_transaction(&mut mock_server).await;
789
790        let config = create_test_config_solana();
791        let client = Client::new_with_client(&mock_server.url(), create_test_client());
792
793        let service = CdpService { config, client };
794
795        let transaction = "test-transaction-string".to_string();
796        let result = service.sign_solana_transaction(transaction).await;
797
798        match result {
799            Ok(signed_tx) => {
800                assert!(!signed_tx.is_empty());
801            }
802            Err(e) => {
803                panic!("Expected success but got error: {:?}", e);
804            }
805        }
806    }
807
808    #[tokio::test]
809    async fn test_sign_solana_transaction_wrong_address_type() {
810        let config = create_test_config_evm(); // EVM address for Solana signing
811        let client = Client::new_with_client("http://test", create_test_client());
812        let service = CdpService { config, client };
813
814        let transaction = "test-transaction-string".to_string();
815        let result = service.sign_solana_transaction(transaction).await;
816
817        assert!(result.is_err());
818        match result {
819            Err(CdpError::ConfigError(msg)) => {
820                assert!(msg.contains("Account address is not a Solana address"));
821            }
822            _ => panic!("Expected ConfigError for wrong address type"),
823        }
824    }
825
826    // Error handling tests
827    #[tokio::test]
828    async fn test_sign_evm_message_error_400_malformed_transaction() {
829        let mut mock_server = mockito::Server::new_async().await;
830        let _mock = setup_mock_error_400_malformed_transaction(
831            &mut mock_server,
832            r".*/v2/evm/accounts/.*/sign/message",
833        )
834        .await;
835
836        let config = create_test_config_evm();
837        let client = Client::new_with_client(&mock_server.url(), create_test_client());
838        let service = CdpService { config, client };
839
840        let message = "Hello World!".to_string();
841        let result = service.sign_evm_message(message).await;
842
843        assert!(result.is_err());
844        match result {
845            Err(CdpError::SigningError(msg)) => {
846                assert!(msg.contains("Failed to sign message"));
847            }
848            _ => panic!("Expected SigningError for malformed transaction"),
849        }
850    }
851
852    #[tokio::test]
853    async fn test_sign_evm_message_error_401_unauthorized() {
854        let mut mock_server = mockito::Server::new_async().await;
855        let _mock = setup_mock_error_401_unauthorized(
856            &mut mock_server,
857            r".*/v2/evm/accounts/.*/sign/message",
858        )
859        .await;
860
861        let config = create_test_config_evm();
862        let client = Client::new_with_client(&mock_server.url(), create_test_client());
863        let service = CdpService { config, client };
864
865        let message = "Hello World!".to_string();
866        let result = service.sign_evm_message(message).await;
867
868        assert!(result.is_err());
869        match result {
870            Err(CdpError::SigningError(msg)) => {
871                assert!(msg.contains("Failed to sign message"));
872            }
873            _ => panic!("Expected SigningError for unauthorized"),
874        }
875    }
876
877    #[tokio::test]
878    async fn test_sign_evm_message_error_500_internal_error() {
879        let mut mock_server = mockito::Server::new_async().await;
880        let _mock = setup_mock_error_500_internal_error(
881            &mut mock_server,
882            r".*/v2/evm/accounts/.*/sign/message",
883        )
884        .await;
885
886        let config = create_test_config_evm();
887        let client = Client::new_with_client(&mock_server.url(), create_test_client());
888        let service = CdpService { config, client };
889
890        let message = "Hello World!".to_string();
891        let result = service.sign_evm_message(message).await;
892
893        assert!(result.is_err());
894        match result {
895            Err(CdpError::SigningError(msg)) => {
896                assert!(msg.contains("Failed to sign message"));
897            }
898            _ => panic!("Expected SigningError for internal error"),
899        }
900    }
901
902    #[tokio::test]
903    async fn test_sign_evm_transaction_error_400_malformed_transaction() {
904        let mut mock_server = mockito::Server::new_async().await;
905        let _mock = setup_mock_error_400_malformed_transaction(
906            &mut mock_server,
907            r".*/v2/evm/accounts/.*/sign/transaction",
908        )
909        .await;
910
911        let config = create_test_config_evm();
912        let client = Client::new_with_client(&mock_server.url(), create_test_client());
913        let service = CdpService { config, client };
914
915        let transaction_bytes = b"invalid transaction data";
916        let result = service.sign_evm_transaction(transaction_bytes).await;
917
918        assert!(result.is_err());
919        match result {
920            Err(CdpError::SigningError(msg)) => {
921                assert!(msg.contains("Failed to sign transaction"));
922            }
923            _ => panic!("Expected SigningError for malformed transaction"),
924        }
925    }
926
927    #[tokio::test]
928    async fn test_sign_evm_transaction_error_422_invalid_signature() {
929        let mut mock_server = mockito::Server::new_async().await;
930        let _mock = setup_mock_error_422_invalid_signature(
931            &mut mock_server,
932            r".*/v2/evm/accounts/.*/sign/transaction",
933        )
934        .await;
935
936        let config = create_test_config_evm();
937        let client = Client::new_with_client(&mock_server.url(), create_test_client());
938        let service = CdpService { config, client };
939
940        let transaction_bytes = b"test transaction data";
941        let result = service.sign_evm_transaction(transaction_bytes).await;
942
943        assert!(result.is_err());
944        match result {
945            Err(CdpError::SigningError(msg)) => {
946                assert!(msg.contains("Failed to sign transaction"));
947            }
948            _ => panic!("Expected SigningError for invalid signature request"),
949        }
950    }
951
952    #[tokio::test]
953    async fn test_sign_solana_message_error_400_malformed_transaction() {
954        let mut mock_server = mockito::Server::new_async().await;
955        let _mock = setup_mock_error_400_malformed_transaction(
956            &mut mock_server,
957            r".*/v2/solana/accounts/.*/sign/message",
958        )
959        .await;
960
961        let config = create_test_config_solana();
962        let client = Client::new_with_client(&mock_server.url(), create_test_client());
963        let service = CdpService { config, client };
964
965        let message_bytes = b"Hello Solana!";
966        let result = service.sign_solana_message(message_bytes).await;
967
968        assert!(result.is_err());
969        match result {
970            Err(CdpError::SigningError(msg)) => {
971                assert!(msg.contains("Failed to sign Solana message"));
972            }
973            _ => panic!("Expected SigningError for malformed transaction"),
974        }
975    }
976
977    #[tokio::test]
978    async fn test_sign_solana_message_error_401_unauthorized() {
979        let mut mock_server = mockito::Server::new_async().await;
980        let _mock = setup_mock_error_401_unauthorized(
981            &mut mock_server,
982            r".*/v2/solana/accounts/.*/sign/message",
983        )
984        .await;
985
986        let config = create_test_config_solana();
987        let client = Client::new_with_client(&mock_server.url(), create_test_client());
988        let service = CdpService { config, client };
989
990        let message_bytes = b"Hello Solana!";
991        let result = service.sign_solana_message(message_bytes).await;
992
993        assert!(result.is_err());
994        match result {
995            Err(CdpError::SigningError(msg)) => {
996                assert!(msg.contains("Failed to sign Solana message"));
997            }
998            _ => panic!("Expected SigningError for unauthorized"),
999        }
1000    }
1001
1002    #[tokio::test]
1003    async fn test_sign_solana_transaction_error_400_malformed_transaction() {
1004        let mut mock_server = mockito::Server::new_async().await;
1005        let _mock = setup_mock_error_400_malformed_transaction(
1006            &mut mock_server,
1007            r".*/v2/solana/accounts/.*/sign/transaction",
1008        )
1009        .await;
1010
1011        let config = create_test_config_solana();
1012        let client = Client::new_with_client(&mock_server.url(), create_test_client());
1013        let service = CdpService { config, client };
1014
1015        let transaction = "invalid-transaction-string".to_string();
1016        let result = service.sign_solana_transaction(transaction).await;
1017
1018        assert!(result.is_err());
1019        match result {
1020            Err(CdpError::SigningError(msg)) => {
1021                assert!(msg.contains("Failed to sign Solana transaction"));
1022            }
1023            _ => panic!("Expected SigningError for malformed transaction"),
1024        }
1025    }
1026
1027    #[tokio::test]
1028    async fn test_sign_solana_transaction_error_500_internal_error() {
1029        let mut mock_server = mockito::Server::new_async().await;
1030        let _mock = setup_mock_error_500_internal_error(
1031            &mut mock_server,
1032            r".*/v2/solana/accounts/.*/sign/transaction",
1033        )
1034        .await;
1035
1036        let config = create_test_config_solana();
1037        let client = Client::new_with_client(&mock_server.url(), create_test_client());
1038        let service = CdpService { config, client };
1039
1040        let transaction = "test-transaction-string".to_string();
1041        let result = service.sign_solana_transaction(transaction).await;
1042
1043        assert!(result.is_err());
1044        match result {
1045            Err(CdpError::SigningError(msg)) => {
1046                assert!(msg.contains("Failed to sign Solana transaction"));
1047            }
1048            _ => panic!("Expected SigningError for internal error"),
1049        }
1050    }
1051}