kms_secp256k1_api/services/
cosmos_keys_service.rs

1use crate::config::Config;
2use crate::constants::{COSMOS_SECP_LEN, DEFAULT_COSMOS_HRP};
3use crate::services::crypto_service::CryptoService;
4use crate::services::keys_service::{KeyEntry, KeysService, KeysServiceTrait, SigEntry};
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD;
7use cosmrs::{
8    Any,
9    crypto::PublicKey as CosmosSecp256k1PublicKey,
10    proto::cosmos::tx::v1beta1::TxRaw,
11    tendermint::{block, chain::Id},
12    tx::{AuthInfo, Body, Fee, MessageExt, SignDoc, SignerInfo},
13};
14use k256::ecdsa::VerifyingKey;
15use k256::{
16    PublicKey,
17    ecdsa::Signature,
18    sha2::{Digest, Sha256},
19};
20use reqwest::Client;
21use serde::Deserialize;
22use serde_json::json;
23use std::str::FromStr;
24use tracing::error;
25
26pub struct CosmosKeysService {
27    keys_service: KeysService,
28    hrp: String,
29}
30
31#[async_trait::async_trait]
32impl KeysServiceTrait for CosmosKeysService {
33    /// Creates a new KMS key, derives its public key,
34    /// registers an alias for it, and returns the formatted public key.
35    ///
36    /// # Arguments
37    ///
38    /// * `config` - Reference to the config struct used to decide prefixing.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if key creation, public key conversion, or alias creation fails.
43    async fn create_key(&mut self, _config: &Config) -> Result<KeyEntry, String> {
44        let (key_id, public_key_base64) = self
45            .keys_service
46            .kms_client_service
47            .create_key()
48            .await
49            .map_err(|e| {
50            let msg = format!("Failed to create_key in KmsClientService: {e}");
51            error!("{}", &msg);
52            msg
53        })?;
54
55        let public_key = self
56            .keys_service
57            .crypto_service
58            .public_key(&public_key_base64)
59            .map_err(|e| {
60                let msg = format!("public_key conversion failed: {e:?}");
61                error!("{}", &msg);
62                msg
63            })?;
64
65        if public_key.is_empty() {
66            let msg = "No public key generated".to_string();
67            error!("{}", &msg);
68            return Err(msg);
69        }
70
71        let key = self.resolve_key(&public_key)?;
72
73        // Create alias for the key
74        self.keys_service
75            .kms_client_service
76            .create_alias(&key_id, &key)
77            .await
78            .map_err(|e| {
79                let msg = format!("Error creating alias: {e:?}");
80                error!("{}", &msg);
81                msg
82            })?;
83
84        // info!("{}", &public_key_base64);
85
86        Ok(KeyEntry {
87            public_key: Some(public_key.clone()).into(),
88            address: key.clone().into(),
89            public_key_base64: public_key_base64.into(),
90            key_id: key_id.into(),
91        })
92    }
93
94    /// Signs a transaction hash using the provided public key and configuration mode.
95    ///
96    /// Delegates signing to the internal `sign` method.
97    ///
98    /// # Arguments
99    ///
100    /// * `config` - Reference to the application configuration to determine signing mode.
101    /// * `transaction_hash` - The hash of the transaction to be signed.
102    /// * `key` - The key alias corresponding to the private key for signing.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the signing operation fails.
107    async fn sign_transaction_hash(
108        &mut self,
109        config: &Config,
110        transaction_hash: &str,
111        key: &str,
112    ) -> Result<SigEntry, String> {
113        Self::ensure_cosmos_mode(config)?;
114
115        Self::validate_transaction_hash(transaction_hash)?;
116
117        let key = self.resolve_key(key)?;
118        let public_key = self.resolve_public_key(&key).await?;
119
120        Self::validate_public_key(&public_key, "sign_transaction_hash")?;
121
122        // Perform signing
123        let signature = self.sign(transaction_hash, &key, &public_key).await?;
124
125        Ok(SigEntry {
126            address: key.into(),
127            public_key: public_key.into(),
128            signature: signature.into(),
129        })
130    }
131
132    /// Signs a serialized Cosmos transaction using the provided public key.
133    ///
134    /// Parses the input transaction string, verifies the transaction hash and public key,
135    /// signs the hash, attaches the signature to the transaction, and returns the signed
136    /// transaction as a JSON string.
137    ///
138    /// # Arguments
139    ///
140    /// * `config` - Reference to the application configuration. Only Cosmos mode is supported.
141    /// * `transaction_str` - A JSON string representing the transaction to be signed.
142    /// * `key` - The key alias corresponding to the signing key.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - The application is not in Cosmos mode.
148    /// - The transaction JSON is invalid.
149    /// - The transaction hash or public key is invalid.
150    /// - The signing process fails.
151    /// - The final transaction serialization fails.
152    async fn sign_transaction(
153        &mut self,
154        config: &Config,
155        transaction_str: &str,
156        key: &str,
157    ) -> Result<String, String> {
158        Self::ensure_cosmos_mode(config)?;
159        let tx_json: serde_json::Value = serde_json::from_str(transaction_str)
160            .map_err(|e| format!("Failed to parse JSON: {e}"))?;
161
162        // Extract chain_id and address
163        let chain_id = Id::from_str(&config.get_cosmos_chain_id())
164            .map_err(|e| format!("Failed to fetch chain_id: {e}"))?;
165
166        let key = self.resolve_key(key)?;
167        let public_key = self.resolve_public_key(&key).await?;
168
169        // Decode and validate public key bytes
170        Self::validate_public_key(&public_key, "sign_transaction")?;
171
172        // Deserialize TxBody
173        let helper: BodyHelper = serde_json::from_value(tx_json["body"].clone())
174            .map_err(|e| format!("Invalid TxBody: {e}"))?;
175
176        let tx_body = helper.into_body()?;
177
178        // Deserialize Fee
179        let fee: Fee = serde_json::from_value(tx_json["auth_info"]["fee"].clone())
180            .map_err(|e| format!("Invalid Fee: {e}"))?;
181
182        let public_key_bytes =
183            hex::decode(&public_key).map_err(|e| format!("Invalid hex public key: {e}"))?;
184
185        if public_key_bytes.len() != 33 {
186            return Err(format!(
187                "Invalid public key length: expected 33 bytes, got {}",
188                public_key_bytes.len()
189            ));
190        }
191
192        let pubkey_base64 = STANDARD.encode(public_key_bytes.clone());
193
194        // Fetch account_number and sequence
195        let mut account = fetch_account_info(&key, &pubkey_base64, config)
196            .await
197            .map_err(|e| format!("Failed to fetch account info: {e}"))?;
198
199        let Some(fetched_pub_key) = account.pub_key.take() else {
200            return Err("Account info is missing pub_key".to_string());
201        };
202
203        if fetched_pub_key.key != pubkey_base64 {
204            return Err(format!(
205                "Invalid fetched public key: got {}",
206                fetched_pub_key.key
207            ));
208        }
209        let auth_info = build_auth_info(&public_key_bytes, account.sequence, &fee)?;
210
211        // Build SignDoc
212        let sign_doc = SignDoc::new(&tx_body, &auth_info, &chain_id, account.sequence)
213            .map_err(|e| format!("SignDoc error: {e}"))?;
214
215        let sign_doc_bytes = sign_doc
216            .into_bytes()
217            .map_err(|e| format!("SignDoc encode error: {e}"))?;
218
219        // Hash and sign
220        let transaction_hash_bytes = Sha256::digest(&sign_doc_bytes);
221        let transaction_hash = hex::encode(transaction_hash_bytes);
222
223        Self::validate_transaction_hash(&transaction_hash)?;
224
225        // Perform signing
226        let signature_hex = self.sign(&transaction_hash, &key, &public_key).await?;
227
228        let body_bytes = tx_body
229            .into_bytes()
230            .map_err(|e| format!("Failed to encode body: {e}"))?;
231        let auth_info_bytes = auth_info
232            .into_bytes()
233            .map_err(|e| format!("Failed to encode auth_info: {e}"))?;
234
235        let sig_bytes = hex::decode(signature_hex).map_err(|e| format!("Invalid hex: {e}"))?;
236
237        let signature = Signature::from_slice(&sig_bytes)
238            .map_err(|e| format!("Invalid compact signature: {e}"))?;
239
240        let tx_raw = TxRaw {
241            body_bytes,
242            auth_info_bytes,
243            signatures: vec![signature.to_bytes().to_vec()],
244        };
245
246        let tx_raw_bytes = tx_raw
247            .to_bytes()
248            .map_err(|e| format!("Failed to encode TxRaw: {e}"))?;
249
250        let base64_tx = STANDARD.encode(tx_raw_bytes);
251
252        let broadcast_request = json!({
253            "tx_bytes": base64_tx,
254            "mode": "BROADCAST_MODE_SYNC"  // or "BLOCK" or "ASYNC"
255        });
256
257        let fee_amount_json = fee_amount_json(&fee);
258
259        // Existing signatures from the original transaction (if any)
260        let new_signature = signature_to_json(&key, &public_key, &signature, &transaction_hash);
261        let signatures_array = append_signature_to_transaction(&tx_json, new_signature);
262
263        // Prepare final JSON response
264        let result = json!({
265            "chain_id": chain_id,
266            "body": tx_json["body"],
267            "auth_info": {
268                "signer_infos": [
269                    {
270                        "public_key": {
271                            "@type": fetched_pub_key.key_type,
272                            "key": fetched_pub_key.key,
273                        },
274                        "mode_info": {
275                            "single": { "mode": "SIGN_MODE_DIRECT" }
276                        },
277                        "sequence": account.sequence,
278                        "account_number": account.account_number
279                    }
280                ],
281                "fee": {
282                    "amount": fee_amount_json,
283                    "gas_limit": fee.gas_limit
284                }
285            },
286            "broadcast_request": broadcast_request,
287            "signatures": signatures_array
288        });
289
290        // Return the wrapped transaction + signatures JSON
291        serde_json::to_string(&result)
292            .map_err(|e| format!("Failed to serialize final signed transaction: {e}"))
293    }
294
295    async fn verify(
296        &mut self,
297        transaction_hash_hex: &str,
298        signature_hex: &str,
299        key: &str,
300    ) -> Result<bool, String> {
301        let public_key = self.resolve_public_key(key).await?;
302        self.keys_service
303            .verify(transaction_hash_hex, signature_hex, &public_key)
304    }
305
306    async fn verify_via_kms(
307        &mut self,
308        transaction_hash_hex: &str,
309        signature_hex: &str,
310        key: &str,
311    ) -> Result<bool, String> {
312        let public_key = self.resolve_public_key(key).await?;
313        self.keys_service
314            .verify_via_kms(transaction_hash_hex, signature_hex, &public_key)
315            .await
316    }
317
318    async fn delete_key(&mut self, key: &str) -> Result<bool, String> {
319        let key = self.resolve_key(key)?;
320        self.keys_service.delete_key(&key).await
321    }
322
323    async fn list_keys(&mut self) -> Result<Vec<KeyEntry>, String> {
324        self.keys_service.list_keys().await
325    }
326}
327
328impl CosmosKeysService {
329    /// Creates a new `CosmosKeysService` instance.
330    ///
331    /// # Arguments
332    ///
333    /// * `config` - The configuration object used to initialize the service.
334    /// * `crypto_service` - The cryptographic service used for signing and key management.
335    ///
336    /// # Errors
337    ///
338    /// Returns an error string if the `KeysService` fails to initialize.
339    ///
340    pub async fn new(config: Config, crypto_service: CryptoService) -> Result<Self, String> {
341        let keys_service = KeysService::new(config.clone(), crypto_service).await?;
342        let cosmos_hrp = config.get_cosmos_hrp();
343        let hrp = match cosmos_hrp.as_str() {
344            "" => DEFAULT_COSMOS_HRP,
345            hrp => hrp,
346        }
347        .to_string();
348        Ok(Self { keys_service, hrp })
349    }
350
351    async fn sign(
352        &mut self,
353        transaction_hash: &str,
354        key: &str,
355        public_key: &str,
356    ) -> Result<String, String> {
357        let signature_hex = self
358            .keys_service
359            .sign(transaction_hash, key, None)
360            .await
361            .map_err(|e| format!("Signing failed: {e}"))?;
362
363        Self::validate_signature_length(&signature_hex)?;
364
365        let is_valid = self
366            .verify(transaction_hash, &signature_hex, public_key)
367            .await?;
368        if !is_valid {
369            return Err("Generated signature failed verification".to_string());
370        }
371        Ok(signature_hex)
372    }
373
374    /// Resolves the given key to a Cosmos address if it is a compressed public key.
375    ///
376    /// If the input is a valid 33-byte (compressed) secp256k1 public key, it is
377    /// converted to a bech32-encoded Cosmos address using the configured HRP.
378    /// Otherwise, the input is assumed to already be an address and returned as-is.
379    ///
380    /// # Arguments
381    ///
382    /// * `key` - A compressed secp256k1 public key (as a hex string) or a Cosmos address.
383    ///
384    /// # Errors
385    ///
386    /// Returns an error if the key is detected to be a public key and address conversion fails.
387    ///
388    fn resolve_key(&mut self, key: &str) -> Result<String, String> {
389        if key.len() == COSMOS_SECP_LEN {
390            self.keys_service
391                .crypto_service
392                .address_cosmos(key, &self.hrp)
393                .map_err(|e| {
394                    let msg = format!("Failed to convert public key to address: {e:?}");
395                    error!("{}", &msg);
396                    msg
397                })
398        } else {
399            Ok(key.to_string())
400        }
401    }
402
403    /// Resolves a Cosmos-compatible public key from a key identifier.
404    ///
405    /// If the provided `key` is already a valid 33-byte compressed public key in hex format
406    /// (expected length `COSMOS_SECP_LEN`), it is returned as-is. Otherwise, the key is
407    /// assumed to be an alias or identifier managed by the KMS, and the corresponding
408    /// public key is fetched from the KMS and serialized for use with Cosmos.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if:
413    /// - The KMS fails to return a public key for the given alias.
414    /// - The returned key cannot be converted into a valid Cosmos-compatible public key.
415    pub async fn resolve_public_key(&mut self, key: &str) -> Result<String, String> {
416        if key.len() == COSMOS_SECP_LEN {
417            Ok(key.to_string())
418        } else {
419            let public_key = self
420                .keys_service
421                .kms_client_service
422                .get_public_key(key)
423                .await
424                .map_err(|e| {
425                    let msg = format!("Failed to get public key from alias with KMS: {e}");
426                    error!("{}", msg);
427                    msg
428                })?;
429
430            self.keys_service
431                .crypto_service
432                .public_key(&public_key)
433                .map_err(|e| {
434                    let msg = format!("public_key conversion failed: {e:?}");
435                    error!("{}", &msg);
436                    msg
437                })
438        }
439    }
440
441    fn validate_public_key(public_key: &str, context: &str) -> Result<(), String> {
442        let public_key_bytes = hex::decode(public_key).map_err(|e| {
443            let msg = format!("Error decoding public key in {context}: {e:?}");
444            error!("{}", msg);
445            msg
446        })?;
447
448        PublicKey::from_sec1_bytes(&public_key_bytes).map_err(|e| {
449            let msg = format!("Invalid public key bytes in {context}: {e:?}");
450            error!("{}", msg);
451            msg
452        })?;
453
454        Ok(())
455    }
456
457    fn ensure_cosmos_mode(config: &Config) -> Result<(), String> {
458        if config.is_cosmos_mode() {
459            Ok(())
460        } else {
461            Err("Only Cosmos mode is supported".to_string())
462        }
463    }
464
465    fn validate_signature_length(hex: &str) -> Result<(), String> {
466        let bytes = hex::decode(hex).map_err(|_| "Invalid hex in signature".to_string())?;
467        if bytes.len() == 64 {
468            Ok(())
469        } else {
470            Err("Invalid signature length (expected 64 bytes)".to_string())
471        }
472    }
473
474    fn validate_transaction_hash(transaction_hash: &str) -> Result<(), String> {
475        let hash_bytes = hex::decode(transaction_hash)
476            .map_err(|e| format!("Invalid transaction hash hex: {e}"))?;
477
478        if hash_bytes.len() != 32 {
479            return Err("Transaction hash must be 32 bytes".to_string());
480        }
481        Ok(())
482    }
483}
484
485#[derive(Debug, Deserialize)]
486pub struct PubKey {
487    #[serde(rename = "@type")]
488    pub key_type: String,
489    pub key: String,
490}
491
492#[derive(Debug, Deserialize)]
493pub struct BaseAccount {
494    #[serde(rename = "@type")]
495    pub account_type: String,
496    pub address: String,
497    pub pub_key: Option<PubKey>,
498    pub account_number: u64,
499    pub sequence: u64,
500}
501
502#[derive(Debug, Deserialize)]
503struct AccountWrapper {
504    pub account: BaseAccount,
505}
506
507#[derive(Deserialize)]
508struct RawMsg {
509    #[serde(rename = "@type")]
510    pub type_url: String,
511    #[serde(flatten)]
512    pub value: serde_json::Value,
513}
514
515#[derive(Deserialize)]
516pub struct BodyHelper {
517    messages: Vec<RawMsg>,
518    memo: Option<String>,
519    timeout_height: Option<String>,
520}
521
522impl BodyHelper {
523    /// Converts the transaction request into a `Body` used for signing and broadcasting.
524    ///
525    /// This method transforms JSON-encoded Cosmos messages into protobuf `Any` messages,
526    /// parses the optional timeout height, and constructs the full `Body` structure for
527    /// the transaction.
528    ///
529    /// # Errors
530    ///
531    /// Returns an error if:
532    /// - The `timeout_height` is not a valid integer.
533    /// - Any message in the `messages` field fails to convert to a protobuf `Any`.
534    pub fn into_body(self) -> Result<Body, String> {
535        let height = self
536            .timeout_height
537            .as_deref()
538            .unwrap_or("0")
539            .parse::<u64>()
540            .map_err(|e| format!("Invalid timeout_height: {e}"))?;
541
542        let any_msgs: Result<Vec<Any>, String> =
543            self.messages.iter().map(Self::json_msg_to_any).collect();
544        let any_msgs = any_msgs?;
545
546        let height_u32 =
547            u32::try_from(height).map_err(|_| format!("timeout_height too large: {height}"))?;
548
549        Ok(Body::new(
550            any_msgs,
551            self.memo.unwrap_or_default(),
552            block::Height::from(height_u32),
553        ))
554    }
555
556    fn json_msg_to_any(raw: &RawMsg) -> Result<Any, String> {
557        let json_bytes =
558            serde_json::to_vec(&raw.value).map_err(|e| format!("JSON encode error: {e}"))?;
559
560        Ok(Any {
561            type_url: raw.type_url.clone(),
562            value: json_bytes,
563        })
564    }
565}
566
567/// Fetches Cosmos `BaseAccount` information from the REST endpoint.
568///
569/// This function attempts to retrieve account metadata (e.g., sequence and account number)
570/// from the configured Cosmos REST endpoint using the provided Bech32 address.
571///
572/// If the account does not exist on-chain (404 response), a default `BaseAccount` is returned
573/// using the provided public key in base64 format.
574///
575/// # Arguments
576///
577/// * `address` - The Bech32 Cosmos address to fetch account info for.
578/// * `pubkey_base64` - The base64-encoded public key used in case the account is not yet on-chain.
579/// * `config` - A reference to the configuration containing the Cosmos REST URL.
580///
581/// # Returns
582///
583/// A `Result` containing either:
584/// - `BaseAccount` with account metadata (either fetched or default), or
585/// - An error if the request fails or the response cannot be parsed.
586///
587/// # Errors
588///
589/// Returns an error if the HTTP request fails, if the REST endpoint returns an unexpected status,
590/// or if the response body cannot be parsed into an `AccountWrapper`.
591pub async fn fetch_account_info(
592    address: &str,
593    pubkey_base64: &str,
594    config: &Config,
595) -> Result<BaseAccount, Box<dyn std::error::Error>> {
596    let url = format!("{}{address}", config.get_cosmos_rest_url());
597    let client = Client::new();
598
599    let response = client.get(&url).send().await;
600
601    if let Ok(resp) = response
602        && resp.status().is_success()
603    {
604        let parsed: AccountWrapper = resp.json().await?;
605        return Ok(parsed.account);
606    }
607
608    // On any failure (non-2xx status, error, etc.), return a default BaseAccount
609    Ok(BaseAccount {
610        account_type: "cosmos.auth.v1beta1.BaseAccount".to_string(),
611        address: address.to_string(),
612        pub_key: Some(PubKey {
613            key_type: "/cosmos.crypto.secp256k1.PubKey".to_string(),
614            key: pubkey_base64.to_string(),
615        }),
616        account_number: 0,
617        sequence: 0,
618    })
619}
620
621#[must_use]
622pub fn signature_to_json(
623    address: &str,
624    public_key: &str,
625    signature: &Signature,
626    transaction_hash: &str,
627) -> serde_json::Value {
628    json!({
629        "address": address,
630        "signer": public_key,
631        "v": "00", // Cosmos doesn't use EIP-155 v
632        "r": format!("{:x}", signature.r()),
633        "s": format!("{:x}", signature.s()),
634        "hash": transaction_hash,
635        "signature": hex::encode(signature.to_bytes()),
636    })
637}
638
639#[must_use]
640pub fn fee_amount_json(fee: &Fee) -> Vec<serde_json::Value> {
641    fee.amount
642        .iter()
643        .map(|coin| {
644            json!({
645                "denom": coin.denom,
646                "amount": coin.amount
647            })
648        })
649        .collect()
650}
651
652/// Build an `AuthInfo` structure for signing a Cosmos transaction.
653///
654/// # Arguments
655///
656/// * `public_key_bytes` - A byte slice representing the compressed secp256k1 public key (33 bytes).
657/// * `sequence` - The account sequence number for the signer.
658/// * `fee` - A reference to the transaction fee.
659///
660/// # Returns
661///
662/// Returns `Ok(AuthInfo)` on success or an `Err(String)` describing the failure if the public key
663/// bytes are invalid.
664///
665/// # Errors
666///
667/// Returns an error if the provided public key bytes are not a valid secp256k1 public key.
668pub fn build_auth_info(
669    public_key_bytes: &[u8],
670    sequence: u64,
671    fee: &Fee,
672) -> Result<AuthInfo, String> {
673    let verifying_key = VerifyingKey::from_sec1_bytes(public_key_bytes)
674        .map_err(|e| format!("Invalid secp256k1 public key: {e}"))?;
675
676    let cosmos_pubkey = CosmosSecp256k1PublicKey::from(&verifying_key);
677    let signer_info = SignerInfo::single_direct(Some(cosmos_pubkey), sequence);
678
679    Ok(AuthInfo {
680        signer_infos: vec![signer_info],
681        fee: fee.clone(),
682    })
683}
684
685#[must_use]
686pub fn append_signature_to_transaction(
687    tx_json: &serde_json::Value,
688    new_signature: serde_json::Value,
689) -> Vec<serde_json::Value> {
690    match tx_json.get("signatures") {
691        Some(serde_json::Value::Array(existing)) => {
692            let mut updated = existing.clone();
693            updated.push(new_signature);
694            updated
695        }
696        _ => vec![new_signature],
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703    use crate::constants::{
704        COSMOS_PUBLIC_KEY, COSMOS_SIGNATURE, COSMOS_TRANSACTION, COSMOS_TRANSACTION_HASH,
705    };
706    use crate::{
707        config::ConfigBuilder, constants::WASM_PATH, services::crypto_service::CryptoService,
708        wasm_loader::WasmLoader,
709    };
710    use base64::Engine;
711    use base64::engine::general_purpose::STANDARD;
712    use serde_json::{Value, json};
713
714    #[tokio::test]
715    async fn test_create_key() {
716        let config = ConfigBuilder::new()
717            .with_cosmos_mode()
718            .with_aws_mode(false)
719            .build();
720
721        let wasm_loader = WasmLoader::new(WASM_PATH)
722            .await
723            .expect("Failed to load WASM module");
724
725        let crypto_service =
726            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
727
728        // Create the service via the real constructor, it will use MockKmsClientService internally
729        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
730            .await
731            .expect("Failed to create CosmosKeysService");
732
733        // Call create_key on the service under test
734        let result = service.create_key(&config).await;
735
736        // Assert the key is returned
737        assert!(result.is_ok(), "create_key failed: {result:?}");
738        let key = result.unwrap();
739        assert!(!key.public_key.as_deref().unwrap().to_string().is_empty());
740    }
741
742    #[tokio::test]
743    async fn test_verify_signature() {
744        let config = ConfigBuilder::new()
745            .with_cosmos_mode()
746            .with_aws_mode(false)
747            .build();
748
749        let wasm_loader = WasmLoader::new(WASM_PATH)
750            .await
751            .expect("Failed to load WASM module");
752
753        let crypto_service =
754            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
755
756        // Create CosmosKeysService (uses mocked KMS + real CryptoService)
757        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
758            .await
759            .expect("Failed to create CosmosKeysService");
760
761        // Valid signature
762        let result = service
763            .verify(COSMOS_TRANSACTION_HASH, COSMOS_SIGNATURE, COSMOS_PUBLIC_KEY)
764            .await
765            .unwrap();
766        assert!(result, "Expected signature to verify correctly");
767
768        // Invalid signature
769        let result = service
770            .verify("bad_hash", "bad_signature", "bad_key")
771            .await
772            .unwrap();
773        assert!(!result, "Expected signature verification to fail");
774    }
775
776    #[tokio::test]
777    async fn test_verify_via_kms_signature() {
778        let config = ConfigBuilder::new()
779            .with_cosmos_mode()
780            .with_aws_mode(false)
781            .build();
782
783        let wasm_loader = WasmLoader::new(WASM_PATH)
784            .await
785            .expect("Failed to load WASM module");
786
787        let crypto_service =
788            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
789
790        // Create CosmosKeysService (uses mocked KMS + real CryptoService)
791        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
792            .await
793            .expect("Failed to create CosmosKeysService");
794
795        // Valid signature
796        let result = service
797            .verify_via_kms(COSMOS_TRANSACTION_HASH, COSMOS_SIGNATURE, COSMOS_PUBLIC_KEY)
798            .await
799            .unwrap();
800        assert!(result, "Expected signature to verify correctly");
801
802        // Invalid signature
803        let result = service
804            .verify_via_kms("bad_hash", "bad_signature", "bad_key")
805            .await
806            .unwrap();
807        assert!(!result, "Expected signature verification to fail");
808    }
809
810    #[tokio::test]
811    async fn test_delete_key() {
812        let config = ConfigBuilder::new()
813            .with_cosmos_mode()
814            .with_aws_mode(false)
815            .build();
816
817        let wasm_loader = WasmLoader::new(WASM_PATH)
818            .await
819            .expect("Failed to load WASM module");
820
821        let crypto_service =
822            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
823
824        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
825            .await
826            .expect("Failed to create CosmosKeysService");
827
828        let result = service
829            .delete_key("known_public_key")
830            .await
831            .expect("Failed to delete known public key");
832        assert!(result, "Expected key to be deleted successfully");
833
834        let result = service
835            .delete_key("unknown_key")
836            .await
837            .expect("Failed to delete unknown public key");
838        assert!(!result, "Expected key deletion to fail for unknown key");
839    }
840
841    #[tokio::test]
842    async fn test_list_keys() {
843        let config = ConfigBuilder::new()
844            .with_cosmos_mode()
845            .with_aws_mode(false)
846            .build();
847
848        let wasm_loader = WasmLoader::new(WASM_PATH)
849            .await
850            .expect("Failed to load WASM module");
851
852        let crypto_service =
853            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
854
855        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
856            .await
857            .expect("Failed to create CosmosKeysService");
858
859        let result = service.list_keys().await;
860
861        assert!(result.is_ok(), "Expected list_keys to succeed");
862
863        let keys = result.unwrap();
864        assert_eq!(
865            keys[0],
866            KeyEntry {
867                address: "address_1".to_string().into(),
868                public_key_base64: STANDARD.encode("public_key_1_base64").into(),
869                public_key: Some("public_key_1".to_string()).into(),
870                key_id: "key_id_1".to_string().into(),
871            }
872        );
873        assert_eq!(
874            keys[1],
875            KeyEntry {
876                address: "address_2".to_string().into(),
877                public_key_base64: STANDARD.encode("public_key_2_base64").into(),
878                public_key: Some("public_key_2".to_string()).into(),
879                key_id: "key_id_2".to_string().into(),
880            }
881        );
882    }
883
884    #[tokio::test]
885    async fn test_sign_transaction_hash() {
886        let config = ConfigBuilder::new()
887            .with_cosmos_mode()
888            .with_aws_mode(false)
889            .build();
890
891        let wasm_loader = WasmLoader::new(WASM_PATH)
892            .await
893            .expect("Failed to load WASM module");
894
895        let crypto_service =
896            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
897
898        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
899            .await
900            .expect("Failed to create CosmosKeysService");
901
902        let result = service
903            .sign_transaction_hash(&config, COSMOS_TRANSACTION_HASH, COSMOS_PUBLIC_KEY)
904            .await;
905
906        assert!(result.is_ok(), "Expected signing to succeed");
907
908        let signed = result.unwrap();
909
910        let expected_sig_hex = COSMOS_SIGNATURE;
911
912        assert_eq!(
913            signed.signature.to_string(),
914            expected_sig_hex,
915            "Expected signature to match expected format"
916        );
917    }
918
919    #[tokio::test]
920    async fn test_sign_transaction() {
921        let config = ConfigBuilder::new()
922            .with_cosmos_mode()
923            .with_aws_mode(false)
924            .build();
925
926        let wasm_loader = WasmLoader::new(WASM_PATH)
927            .await
928            .expect("Failed to load WASM module");
929
930        let crypto_service =
931            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
932
933        // Create the CosmosKeysService with mocks + real crypto
934        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
935            .await
936            .expect("Failed to create CosmosKeysService");
937
938        let result = service
939            .sign_transaction(&config, COSMOS_TRANSACTION, COSMOS_PUBLIC_KEY)
940            .await;
941
942        assert!(result.is_ok(), "Expected signing to succeed");
943
944        let signed = result.unwrap();
945        let json: Value = serde_json::from_str(&signed).expect("Invalid JSON returned");
946
947        let signatures = json["signatures"]
948            .as_array()
949            .expect("Missing 'signatures' array");
950        let first = &signatures[0];
951
952        let signer = first["signer"].as_str().expect("Missing 'signer'");
953        let signature = first["signature"].as_str().expect("Missing 'signature'");
954
955        assert_eq!(
956            signer, COSMOS_PUBLIC_KEY,
957            "Expected signer to match COSMOS_PUBLIC_KEY"
958        );
959
960        assert_eq!(
961            signature, COSMOS_SIGNATURE,
962            "Expected signature to match expected format"
963        );
964    }
965
966    #[tokio::test]
967    async fn test_sign_transaction_malformed_json() {
968        let config = ConfigBuilder::new()
969            .with_cosmos_mode()
970            .with_aws_mode(false)
971            .build();
972
973        let wasm_loader = WasmLoader::new(WASM_PATH)
974            .await
975            .expect("Failed to load WASM");
976
977        let crypto_service =
978            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
979
980        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
981            .await
982            .expect("Failed to create service");
983
984        let bad_json = "{ this is not valid JSON }";
985
986        let result = service
987            .sign_transaction(&config, bad_json, COSMOS_PUBLIC_KEY)
988            .await;
989
990        assert!(result.is_err());
991        let err = result.unwrap_err();
992        assert!(
993            err.contains("Failed to parse JSON")
994                || err.contains("Failed to parse cosmos transaction"),
995            "Expected parsing error, got: {err}"
996        );
997    }
998
999    #[tokio::test]
1000    async fn test_sign_transaction_missing_transaction_field() {
1001        let config = ConfigBuilder::new()
1002            .with_cosmos_mode()
1003            .with_aws_mode(false)
1004            .build();
1005
1006        let wasm_loader = WasmLoader::new(WASM_PATH)
1007            .await
1008            .expect("Failed to load WASM");
1009
1010        let crypto_service =
1011            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
1012
1013        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
1014            .await
1015            .expect("Failed to create service");
1016
1017        let minimal_transaction = json!({
1018            "some": "value"
1019        })
1020        .to_string();
1021
1022        let result = service
1023            .sign_transaction(&config, &minimal_transaction, COSMOS_PUBLIC_KEY)
1024            .await;
1025
1026        assert!(
1027            result.is_err(),
1028            "Expected failure due to missing transaction fields"
1029        );
1030    }
1031
1032    #[tokio::test]
1033    async fn test_sign_transaction_hash_with_invalid_input() {
1034        let config = ConfigBuilder::new()
1035            .with_cosmos_mode()
1036            .with_aws_mode(false)
1037            .build();
1038
1039        let wasm_loader = WasmLoader::new(WASM_PATH)
1040            .await
1041            .expect("Failed to load WASM module");
1042
1043        let crypto_service =
1044            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
1045
1046        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
1047            .await
1048            .expect("Failed to create CosmosKeysService");
1049
1050        // Invalid public key and transaction hash
1051        let result = service
1052            .sign_transaction_hash(&config, "invalid_hash", "invalid_key")
1053            .await;
1054
1055        assert!(
1056            result.is_err(),
1057            "Expected signing to fail due to invalid input"
1058        );
1059        let error = result.unwrap_err();
1060        assert!(
1061            error.contains("Invalid transaction hash hex"),
1062            "Unexpected error: {error}"
1063        );
1064    }
1065
1066    #[tokio::test]
1067    async fn test_sign_transaction_malformed_approvals() {
1068        let config = ConfigBuilder::new()
1069            .with_cosmos_mode()
1070            .with_aws_mode(false)
1071            .build();
1072
1073        let wasm_loader = WasmLoader::new(WASM_PATH)
1074            .await
1075            .expect("Failed to load WASM");
1076
1077        let crypto_service =
1078            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
1079
1080        let mut service = CosmosKeysService::new(config.clone(), crypto_service)
1081            .await
1082            .expect("Failed to create service");
1083
1084        // approvals should be an array, here it's a string (malformed)
1085        let transaction = json!({
1086            "hash": COSMOS_TRANSACTION_HASH,
1087            "approvals": "not an array"
1088        })
1089        .to_string();
1090
1091        let result = service
1092            .sign_transaction(&config, &transaction, COSMOS_PUBLIC_KEY)
1093            .await;
1094
1095        assert!(
1096            result.is_err(),
1097            "Expected failure due to malformed approvals field"
1098        );
1099    }
1100}