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