openzeppelin_relayer/services/turnkey/
mod.rs

1//! # Turnkey Service Module
2//!
3//! This module provides integration with Turnkey API for secure wallet management
4//! and cryptographic operations.
5//!
6//! ## Features
7//!
8//! - API key-based authentication
9//! - Digital signature generation
10//! - Message signing via Turnkey API
11//! - Secure transaction signing for blockchain operations
12//!
13//! ## Architecture
14//!
15//! ```text
16//! TurnkeyService (implements TurnkeyServiceTrait)
17//!   ├── Authentication (API key-based)
18//!   ├── Digital Stamping
19//!   ├── Transaction Signing
20//!   └── Raw Payload Signing
21//! ```
22use std::str::FromStr;
23
24use alloy::primitives::keccak256;
25use async_trait::async_trait;
26use chrono;
27use p256::{
28    ecdsa::{signature::Signer, Signature as P256Signature, SigningKey},
29    FieldBytes,
30};
31use reqwest::Client;
32use serde::{Deserialize, Serialize};
33use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
34use stellar_strkey;
35use thiserror::Error;
36use tracing::{debug, info};
37
38use crate::models::{Address, SecretString, TurnkeySignerConfig};
39use crate::utils::base64_url_encode;
40
41#[derive(Error, Debug, Serialize)]
42pub enum TurnkeyError {
43    #[error("HTTP error: {0}")]
44    HttpError(String),
45
46    #[error("API method error: {0:?}")]
47    MethodError(TurnkeyResponseError),
48
49    #[error("Authentication failed: {0}")]
50    AuthenticationFailed(String),
51
52    #[error("Configuration error: {0}")]
53    ConfigError(String),
54
55    #[error("Signing error: {0}")]
56    SigningError(String),
57
58    #[error("Serialization error: {0}")]
59    SerializationError(String),
60
61    #[error("Invalid signature: {0}")]
62    SignatureError(String),
63
64    #[error("Invalid pubkey: {0}")]
65    PubkeyError(#[from] solana_sdk::pubkey::PubkeyError),
66
67    #[error("Other error: {0}")]
68    OtherError(String),
69}
70
71/// Error response from Turnkey API
72#[derive(Debug, Deserialize, Serialize)]
73pub struct TurnkeyResponseError {
74    pub error: TurnkeyErrorDetails,
75}
76
77/// Error details from Turnkey API
78#[derive(Debug, Deserialize, Serialize)]
79pub struct TurnkeyErrorDetails {
80    pub code: i32,
81    pub message: String,
82}
83
84/// Result type for Turnkey operations
85pub type TurnkeyResult<T> = Result<T, TurnkeyError>;
86
87/// Digital stamp for API authentication
88#[derive(Serialize)]
89struct ApiStamp {
90    pub public_key: String,
91    pub signature: String,
92    pub scheme: String,
93}
94
95/// Request to sign raw payload
96#[derive(Serialize)]
97#[serde(rename_all = "camelCase")]
98struct SignRawPayloadRequest {
99    #[serde(rename = "type")]
100    activity_type: String,
101    timestamp_ms: String,
102    organization_id: String,
103    parameters: SignRawPayloadIntentV2Parameters,
104}
105
106/// Parameters for signing transaction payload
107#[derive(Serialize)]
108#[serde(rename_all = "camelCase")]
109struct SignEvmTransactionRequest {
110    #[serde(rename = "type")]
111    activity_type: String,
112    timestamp_ms: String,
113    organization_id: String,
114    parameters: SignEvmTransactionV2Parameters,
115}
116
117/// Parameters for signing raw payload
118#[derive(Serialize)]
119#[serde(rename_all = "camelCase")]
120struct SignRawPayloadIntentV2Parameters {
121    sign_with: String,
122    payload: String,
123    encoding: String,
124    hash_function: String,
125}
126
127/// Parameters for signing raw payload
128#[derive(Serialize)]
129#[serde(rename_all = "camelCase")]
130struct SignEvmTransactionV2Parameters {
131    sign_with: String,
132    #[serde(rename = "type")]
133    sign_type: String,
134    unsigned_transaction: String,
135}
136
137/// Response from activity API
138#[derive(Deserialize, Serialize)]
139struct ActivityResponse {
140    activity: Activity,
141}
142
143/// Activity details
144#[derive(Deserialize, Serialize)]
145#[serde(rename_all = "camelCase")]
146struct Activity {
147    id: Option<String>,
148    status: Option<String>,
149    result: Option<ActivityResult>,
150}
151
152/// Activity result
153#[derive(Deserialize, Serialize)]
154#[serde(rename_all = "camelCase")]
155struct ActivityResult {
156    sign_raw_payload_result: Option<SignRawPayloadResult>,
157    sign_transaction_result: Option<SignTransactionResult>,
158}
159
160/// Sign raw payload result
161#[derive(Deserialize, Serialize)]
162#[serde(rename_all = "camelCase")]
163struct SignRawPayloadResult {
164    r: String,
165    s: String,
166    v: String,
167}
168
169#[derive(Deserialize, Serialize)]
170#[serde(rename_all = "camelCase")]
171struct SignTransactionResult {
172    signed_transaction: String,
173}
174
175#[cfg(test)]
176use mockall::automock;
177
178#[async_trait]
179#[cfg_attr(test, automock)]
180pub trait TurnkeyServiceTrait: Send + Sync {
181    /// Returns the Solana address derived from the configured public key
182    fn address_solana(&self) -> Result<Address, TurnkeyError>;
183
184    /// Returns the EVM address derived from the configured public key
185    fn address_evm(&self) -> Result<Address, TurnkeyError>;
186
187    /// Returns the Stellar address derived from the configured public key
188    fn address_stellar(&self) -> Result<Address, TurnkeyError>;
189
190    /// Signs a message using the Solana signing scheme
191    async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
192
193    /// Signs a message using the EVM signing scheme
194    async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
195
196    /// Signs a message using the Stellar signing scheme (Ed25519)
197    async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
198
199    /// Signs an EVM transaction using the Turnkey API
200    async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
201
202    /// Signs a Solana transaction and returns both the transaction and signature
203    async fn sign_solana_transaction(
204        &self,
205        transaction: &mut Transaction,
206    ) -> TurnkeyResult<(Transaction, Signature)>;
207}
208
209#[derive(Clone, Debug)]
210pub struct TurnkeyService {
211    pub api_public_key: String,
212    pub api_private_key: SecretString,
213    pub organization_id: String,
214    pub private_key_id: String,
215    pub public_key: String,
216    pub base_url: String,
217    client: Client,
218}
219
220impl TurnkeyService {
221    pub fn new(config: TurnkeySignerConfig) -> Result<Self, TurnkeyError> {
222        Ok(Self {
223            api_public_key: config.api_public_key.clone(),
224            api_private_key: config.api_private_key,
225            organization_id: config.organization_id.clone(),
226            private_key_id: config.private_key_id.clone(),
227            public_key: config.public_key.clone(),
228            base_url: String::from("https://api.turnkey.com"),
229            client: Client::new(),
230        })
231    }
232
233    /// Converts the public key to a Solana address
234    pub fn address_solana(&self) -> Result<Address, TurnkeyError> {
235        if self.public_key.is_empty() {
236            return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
237        }
238
239        let raw_pubkey = hex::decode(&self.public_key)
240            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
241
242        let pubkey_bs58 = bs58::encode(&raw_pubkey).into_string();
243
244        Ok(Address::Solana(pubkey_bs58))
245    }
246
247    /// Converts the public key to an EVM address
248    pub fn address_evm(&self) -> Result<Address, TurnkeyError> {
249        let public_key = hex::decode(&self.public_key)
250            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
251
252        // Remove the first byte (0x04 prefix)
253        let pub_key_no_prefix = &public_key[1..];
254
255        let hash = keccak256(pub_key_no_prefix);
256
257        // Ethereum addresses are the last 20 bytes of the Keccak-256 hash.
258        // Since the hash is 32 bytes, the address is bytes 12..32.
259        let address_bytes = &hash[12..];
260
261        if address_bytes.len() != 20 {
262            return Err(TurnkeyError::ConfigError(format!(
263                "EVM address should be 20 bytes, got {} bytes",
264                address_bytes.len()
265            )));
266        }
267
268        let mut array = [0u8; 20];
269        array.copy_from_slice(address_bytes);
270
271        Ok(Address::Evm(array))
272    }
273
274    /// Converts the public key to a Stellar address
275    pub fn address_stellar(&self) -> Result<Address, TurnkeyError> {
276        if self.public_key.is_empty() {
277            return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
278        }
279
280        // For Stellar, we expect Ed25519 public key in hex format
281        let raw_pubkey = hex::decode(&self.public_key)
282            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
283
284        // Stellar uses StrKey encoding with 'G' prefix for account addresses
285        let stellar_address = stellar_strkey::ed25519::PublicKey::from_payload(&raw_pubkey)
286            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid Ed25519 public key: {e}")))?
287            .to_string();
288
289        Ok(Address::Stellar(stellar_address))
290    }
291
292    /// Creates a digital stamp for API authentication
293    fn stamp(&self, message: &str) -> TurnkeyResult<String> {
294        let private_api_key_bytes = hex::decode(self.api_private_key.to_str().as_str())
295            .map_err(|e| TurnkeyError::ConfigError(format!("Failed to decode private key: {e}")))?;
296
297        let key_array: [u8; 32] = private_api_key_bytes
298            .as_slice()
299            .try_into()
300            .map_err(|_| TurnkeyError::ConfigError("Invalid private key length".to_string()))?;
301        let signing_key: SigningKey = SigningKey::from_bytes(&FieldBytes::from(key_array))
302            .map_err(|e| TurnkeyError::SigningError(format!("Turnkey stamp error: {e}")))?;
303
304        let signature: P256Signature = signing_key.sign(message.as_bytes());
305
306        let stamp = ApiStamp {
307            public_key: self.api_public_key.clone(),
308            signature: hex::encode(signature.to_der()),
309            scheme: "SIGNATURE_SCHEME_TK_API_P256".into(),
310        };
311
312        let json_stamp = serde_json::to_string(&stamp).map_err(|e| {
313            TurnkeyError::SerializationError(format!("Serialization stamp error: {e}"))
314        })?;
315        let encoded_stamp = base64_url_encode(json_stamp.as_bytes());
316
317        Ok(encoded_stamp)
318    }
319
320    /// Helper method to make Turnkey API requests
321    async fn make_turnkey_request<T, R>(&self, endpoint: &str, request_body: &T) -> TurnkeyResult<R>
322    where
323        T: Serialize,
324        R: for<'de> Deserialize<'de> + 'static,
325    {
326        // Serialize the request body
327        let body = serde_json::to_string(request_body).map_err(|e| {
328            TurnkeyError::SerializationError(format!("Request serialization error: {e}"))
329        })?;
330
331        // Create the authentication stamp
332        let x_stamp = self.stamp(&body)?;
333
334        debug!(endpoint = %endpoint, "sending request to turnkey api");
335        let response = self
336            .client
337            .post(format!("{}/public/v1/submit/{}", self.base_url, endpoint))
338            .header("Content-Type", "application/json")
339            .header("X-Stamp", x_stamp)
340            .body(body)
341            .send()
342            .await;
343
344        self.process_response::<R>(response).await
345    }
346
347    /// Helper method to sign raw payloads with configurable hash function and v inclusion
348    async fn sign_raw_payload(
349        &self,
350        payload: &[u8],
351        hash_function: &str,
352        include_v: bool,
353    ) -> TurnkeyResult<Vec<u8>> {
354        let encoded_payload = hex::encode(payload);
355
356        let sign_raw_payload_body = SignRawPayloadRequest {
357            activity_type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2".to_string(),
358            timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
359            organization_id: self.organization_id.clone(),
360            parameters: SignRawPayloadIntentV2Parameters {
361                sign_with: self.private_key_id.clone(),
362                payload: encoded_payload,
363                encoding: "PAYLOAD_ENCODING_HEXADECIMAL".to_string(),
364                hash_function: hash_function.to_string(),
365            },
366        };
367
368        let response_body = self
369            .make_turnkey_request::<_, ActivityResponse>("sign_raw_payload", &sign_raw_payload_body)
370            .await?;
371
372        if let Some(result) = response_body.activity.result {
373            if let Some(result) = result.sign_raw_payload_result {
374                let concatenated_hex = if include_v {
375                    format!("{}{}{}", result.r, result.s, result.v)
376                } else {
377                    format!("{}{}", result.r, result.s)
378                };
379
380                let signature_bytes = hex::decode(&concatenated_hex).map_err(|e| {
381                    TurnkeyError::SigningError(format!("Turnkey signing error {e}"))
382                })?;
383
384                return Ok(signature_bytes);
385            }
386        }
387
388        Err(TurnkeyError::OtherError(
389            "Missing SIGN_RAW_PAYLOAD result".into(),
390        ))
391    }
392
393    /// Signs raw bytes using the Turnkey API (for Solana)
394    async fn sign_bytes_solana(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
395        self.sign_raw_payload(bytes, "HASH_FUNCTION_NOT_APPLICABLE", false)
396            .await
397    }
398
399    /// Signs raw bytes using the Turnkey API (for EVM)
400    async fn sign_bytes_evm(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
401        let result = self
402            .sign_raw_payload(bytes, "HASH_FUNCTION_NO_OP", true)
403            .await?;
404        debug!(signature_length = %result.len(), "evm signature length");
405        Ok(result)
406    }
407
408    /// Signs raw bytes using the Turnkey API (for Stellar)
409    async fn sign_bytes_stellar(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
410        use sha2::{Digest, Sha256};
411        let hash = Sha256::digest(bytes);
412
413        self.sign_raw_payload(&hash, "HASH_FUNCTION_NOT_APPLICABLE", false)
414            .await
415    }
416
417    /// Signs an EVM transaction using the Turnkey API
418    async fn sign_evm_transaction_impl(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
419        let encoded_bytes = hex::encode(bytes);
420
421        // Create the request body
422        let sign_transaction_body = SignEvmTransactionRequest {
423            activity_type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2".to_string(),
424            timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
425            organization_id: self.organization_id.clone(),
426            parameters: SignEvmTransactionV2Parameters {
427                sign_with: self.private_key_id.clone(),
428                sign_type: "TRANSACTION_TYPE_ETHEREUM".to_string(),
429                unsigned_transaction: encoded_bytes,
430            },
431        };
432
433        // Make the API request and get the response
434        let response_body = self
435            .make_turnkey_request::<_, ActivityResponse>("sign_transaction", &sign_transaction_body)
436            .await?;
437
438        // Extract the signed transaction
439        response_body
440            .activity
441            .result
442            .and_then(|result| result.sign_transaction_result)
443            .map(|tx_result| hex::decode(&tx_result.signed_transaction))
444            .transpose()
445            .map_err(|e| TurnkeyError::SigningError(format!("Failed to decode transaction: {e}")))?
446            .ok_or_else(|| TurnkeyError::OtherError("Missing transaction result".into()))
447    }
448
449    async fn process_response<T>(
450        &self,
451        response: Result<reqwest::Response, reqwest::Error>,
452    ) -> TurnkeyResult<T>
453    where
454        T: for<'de> Deserialize<'de> + 'static,
455    {
456        match response {
457            Ok(res) => {
458                let status = res.status();
459                let headers = res.headers().clone();
460                let content_type = headers
461                    .get("content-type")
462                    .and_then(|v| v.to_str().ok())
463                    .unwrap_or("unknown");
464
465                if res.status().is_success() {
466                    // On success, deserialize the response into the expected type T
467                    res.json::<T>()
468                        .await
469                        .map_err(|e| TurnkeyError::HttpError(e.to_string()))
470                } else {
471                    // For error responses, try to get the body text first
472                    match res.text().await {
473                        Ok(body_text) => {
474                            debug!(status = %status, body_text = %body_text, "error response");
475
476                            if content_type.contains("application/json") {
477                                match serde_json::from_str::<TurnkeyResponseError>(&body_text) {
478                                    Ok(error) => Err(TurnkeyError::MethodError(error)),
479                                    Err(e) => {
480                                        debug!(error = %e, "failed to parse error response as json");
481                                        Err(TurnkeyError::HttpError(format!(
482                                            "HTTP {status} error: {body_text}"
483                                        )))
484                                    }
485                                }
486                            } else {
487                                Err(TurnkeyError::HttpError(format!(
488                                    "HTTP {status} error: {body_text}"
489                                )))
490                            }
491                        }
492                        Err(e) => {
493                            info!(error = %e, "failed to read error response body");
494                            Err(TurnkeyError::HttpError(format!(
495                                "HTTP {status} error (failed to read body): {e}"
496                            )))
497                        }
498                    }
499                }
500            }
501            Err(e) => {
502                debug!(error = ?e, "turnkey api request error");
503                // On a reqwest error, convert it into a TurnkeyError::HttpError
504                Err(TurnkeyError::HttpError(e.to_string()))
505            }
506        }
507    }
508}
509
510#[async_trait]
511impl TurnkeyServiceTrait for TurnkeyService {
512    fn address_solana(&self) -> Result<Address, TurnkeyError> {
513        if self.public_key.is_empty() {
514            return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
515        }
516
517        let raw_pubkey = hex::decode(&self.public_key)
518            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
519
520        let pubkey_bs58 = bs58::encode(&raw_pubkey).into_string();
521
522        Ok(Address::Solana(pubkey_bs58))
523    }
524
525    fn address_evm(&self) -> Result<Address, TurnkeyError> {
526        let public_key = hex::decode(&self.public_key)
527            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
528
529        // Remove the first byte (0x04 prefix)
530        let pub_key_no_prefix = &public_key[1..];
531
532        let hash = keccak256(pub_key_no_prefix);
533
534        // Ethereum addresses are the last 20 bytes of the Keccak-256 hash.
535        // Since the hash is 32 bytes, the address is bytes 12..32.
536        let address_bytes = &hash[12..];
537
538        if address_bytes.len() != 20 {
539            return Err(TurnkeyError::ConfigError(format!(
540                "EVM address should be 20 bytes, got {} bytes",
541                address_bytes.len()
542            )));
543        }
544
545        let mut array = [0u8; 20];
546        array.copy_from_slice(address_bytes);
547
548        Ok(Address::Evm(array))
549    }
550
551    fn address_stellar(&self) -> Result<Address, TurnkeyError> {
552        self.address_stellar()
553    }
554
555    async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
556        let signature_bytes = self.sign_bytes_solana(message).await?;
557        Ok(signature_bytes)
558    }
559
560    async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
561        let signature_bytes = self.sign_bytes_evm(message).await?;
562        Ok(signature_bytes)
563    }
564
565    async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
566        let signature_bytes = self.sign_bytes_stellar(message).await?;
567        Ok(signature_bytes)
568    }
569
570    async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
571        self.sign_evm_transaction_impl(message).await
572    }
573
574    async fn sign_solana_transaction(
575        &self,
576        transaction: &mut Transaction,
577    ) -> TurnkeyResult<(Transaction, Signature)> {
578        let serialized_message = transaction.message_data();
579
580        let public_key = Pubkey::from_str(&self.address_solana()?.to_string())
581            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid pubkey: {e}")))?;
582
583        let signature_bytes = self.sign_bytes_solana(&serialized_message).await?;
584
585        let signature = Signature::try_from(signature_bytes.as_slice())
586            .map_err(|e| TurnkeyError::SignatureError(format!("Invalid signature: {e}")))?;
587
588        let index = transaction
589            .message
590            .account_keys
591            .iter()
592            .position(|key| key == &public_key);
593
594        match index {
595            Some(i) if i < transaction.signatures.len() => {
596                transaction.signatures[i] = signature;
597                Ok((transaction.clone(), signature))
598            }
599            _ => Err(TurnkeyError::OtherError(
600                "Unknown signer or index out of bounds".into(),
601            )),
602        }
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609    use mockito;
610    use serde_json::json;
611
612    fn create_solana_test_config() -> TurnkeySignerConfig {
613        TurnkeySignerConfig {
614            api_public_key: "test-api-public-key".to_string(),
615            api_private_key: SecretString::new(
616                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
617            ),
618            organization_id: "test-org-id".to_string(),
619            private_key_id: "test-private-key-id".to_string(),
620            public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
621                .to_string(),
622        }
623    }
624
625    fn create_evm_test_config() -> TurnkeySignerConfig {
626        TurnkeySignerConfig {
627            api_public_key: "test-api-public-key".to_string(),
628            api_private_key: SecretString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
629            organization_id: "test-org-id".to_string(),
630            private_key_id: "test-private-key-id".to_string(),
631            public_key: "047d3bb8e0317927700cf19fed34e0627367be1390ec247dddf8c239e4b4321a49aea80090e49b206b6a3e577a4f11d721ab063482001ee10db40d6f2963233eec".to_string(),
632        }
633    }
634
635    #[test]
636    fn test_new_turnkey_service() {
637        let config = create_evm_test_config();
638        let service = TurnkeyService::new(config);
639
640        assert!(service.is_ok());
641        let service = service.unwrap();
642        assert_eq!(service.api_public_key, "test-api-public-key");
643        assert_eq!(service.organization_id, "test-org-id");
644        assert_eq!(service.private_key_id, "test-private-key-id");
645    }
646
647    #[test]
648    fn test_address_evm() {
649        let config = create_evm_test_config();
650        let service = TurnkeyService::new(config).unwrap();
651
652        let address = service.address_evm();
653        assert!(address.is_ok());
654
655        let address = address.unwrap();
656
657        assert_eq!(
658            address.to_string(),
659            "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a"
660        );
661    }
662
663    #[test]
664    fn test_address_solana() {
665        let config = create_solana_test_config();
666        let service = TurnkeyService::new(config).unwrap();
667
668        let address = service.address_solana();
669        assert!(address.is_ok());
670
671        let address_str = address.unwrap().to_string();
672        assert_eq!(address_str, "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2");
673    }
674
675    #[test]
676    fn test_address_with_empty_pubkey() {
677        let mut config = create_solana_test_config();
678        config.public_key = "".to_string();
679        let service = TurnkeyService::new(config).unwrap();
680
681        let result = service.address_solana();
682        assert!(result.is_err());
683        if let Err(e) = result {
684            assert!(matches!(e, TurnkeyError::ConfigError(_)));
685            assert_eq!(e.to_string(), "Configuration error: Public key is empty");
686        }
687    }
688
689    #[test]
690    fn test_address_with_invalid_pubkey() {
691        let mut config = create_solana_test_config();
692        config.public_key = "invalid-hex".to_string();
693        let service = TurnkeyService::new(config).unwrap();
694
695        let result = service.address_evm();
696        assert!(result.is_err());
697        if let Err(e) = result {
698            assert!(matches!(e, TurnkeyError::ConfigError(_)));
699            assert!(e.to_string().contains("Invalid public key hex"));
700        }
701    }
702
703    // Setup mock for signing raw payload
704    async fn setup_mock_sign_raw_payload(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
705        mock_server
706            .mock("POST", "/public/v1/submit/sign_raw_payload")
707            .match_header("Content-Type", "application/json")
708            .match_header("X-Stamp", mockito::Matcher::Any)
709            .with_status(200)
710            .with_header("content-type", "application/json")
711            .with_body(serde_json::to_string(&json!({
712                "activity": {
713                    "id": "test-activity-id",
714                    "status": "ACTIVITY_STATUS_COMPLETE",
715                    "result": {
716                        "signRawPayloadResult": {
717                            "r": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
718                            "s": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
719                            "v": "1b"
720                        }
721                    }
722                }
723            })).unwrap())
724            .expect(1)
725            .create_async()
726            .await
727    }
728
729    // Setup mock for signing EVM transaction
730    async fn setup_mock_sign_evm_transaction(
731        mock_server: &mut mockito::ServerGuard,
732    ) -> mockito::Mock {
733        mock_server
734            .mock("POST", "/public/v1/submit/sign_transaction")
735            .match_header("Content-Type", "application/json")
736            .match_header("X-Stamp", mockito::Matcher::Any)
737            .with_status(200)
738            .with_header("content-type", "application/json")
739            .with_body(
740                serde_json::to_string(&json!({
741                    "activity": {
742                        "id": "test-activity-id",
743                        "status": "ACTIVITY_STATUS_COMPLETE",
744                        "result": {
745                            "signTransactionResult": {
746                                "signedTransaction": "02f1010203050607080910" // Example signed transaction hex
747                            }
748                        }
749                    }
750                }))
751                .unwrap(),
752            )
753            .expect(1)
754            .create_async()
755            .await
756    }
757
758    // Setup mock for error response
759    async fn setup_mock_error_response(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
760        mock_server
761            .mock("POST", "/public/v1/submit/sign_raw_payload")
762            .match_header("Content-Type", "application/json")
763            .match_header("X-Stamp", mockito::Matcher::Any)
764            .with_status(400)
765            .with_header("content-type", "application/json")
766            .with_body(
767                serde_json::to_string(&json!({
768                    "error": {
769                        "code": 400,
770                        "message": "Invalid payload format"
771                    }
772                }))
773                .unwrap(),
774            )
775            .expect(1)
776            .create_async()
777            .await
778    }
779
780    // Helper function to create a modified client for testing
781    fn create_test_client() -> Client {
782        reqwest::ClientBuilder::new()
783            .redirect(reqwest::redirect::Policy::none())
784            .build()
785            .unwrap()
786    }
787
788    #[tokio::test]
789    async fn test_sign_solana() {
790        let mut mock_server = mockito::Server::new_async().await;
791        let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
792
793        let config = create_solana_test_config();
794
795        let service = TurnkeyService {
796            api_public_key: config.api_public_key,
797            api_private_key: config.api_private_key,
798            organization_id: config.organization_id,
799            private_key_id: config.private_key_id,
800            public_key: config.public_key,
801            base_url: mock_server.url(),
802            client: create_test_client(),
803        };
804
805        let message = b"test message";
806        let result = service.sign_solana(message).await;
807
808        assert!(result.is_ok());
809    }
810
811    #[tokio::test]
812    async fn test_sign_evm() {
813        let mut mock_server = mockito::Server::new_async().await;
814        let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
815
816        let config = create_evm_test_config();
817        let service = TurnkeyService {
818            api_public_key: config.api_public_key,
819            api_private_key: config.api_private_key,
820            organization_id: config.organization_id,
821            private_key_id: config.private_key_id,
822            public_key: config.public_key,
823            base_url: mock_server.url(),
824            client: create_test_client(),
825        };
826
827        let message = b"test message";
828        let result = service.sign_evm(message).await;
829
830        assert!(result.is_ok());
831    }
832
833    #[tokio::test]
834    async fn test_sign_evm_transaction() {
835        let mut mock_server = mockito::Server::new_async().await;
836        let _mock = setup_mock_sign_evm_transaction(&mut mock_server).await;
837
838        let config = create_evm_test_config();
839        let service = TurnkeyService {
840            api_public_key: config.api_public_key,
841            api_private_key: config.api_private_key,
842            organization_id: config.organization_id,
843            private_key_id: config.private_key_id,
844            public_key: config.public_key,
845            base_url: mock_server.url(),
846            client: create_test_client(),
847        };
848
849        let message = b"test transaction";
850        let result = service.sign_evm_transaction(message).await;
851
852        assert!(result.is_ok());
853        let result = result.unwrap();
854        let expected = hex::decode("02f1010203050607080910").unwrap();
855        assert_eq!(result, expected)
856    }
857
858    #[tokio::test]
859    async fn test_error_handling() {
860        let mut mock_server = mockito::Server::new_async().await;
861        let _mock = setup_mock_error_response(&mut mock_server).await;
862
863        let config = create_solana_test_config();
864        let service = TurnkeyService {
865            api_public_key: config.api_public_key,
866            api_private_key: config.api_private_key,
867            organization_id: config.organization_id,
868            private_key_id: config.private_key_id,
869            public_key: config.public_key,
870            base_url: mock_server.url(),
871            client: create_test_client(),
872        };
873
874        let message = b"test message";
875        let result = service.sign_solana(message).await;
876        assert!(result.is_err());
877        match result {
878            Err(TurnkeyError::MethodError(e)) => {
879                assert!(e.error.message.contains("Invalid payload format"));
880            }
881            _ => panic!("Expected MethodError for Solana signing"),
882        }
883    }
884}