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 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 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 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 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 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 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 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 Self::validate_public_key(&public_key, "sign_transaction")?;
171
172 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 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 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 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 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 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" });
256
257 let fee_amount_json = fee_amount_json(&fee);
258
259 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 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 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 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 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 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 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
567pub 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 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", "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
652pub 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 let mut service = CosmosKeysService::new(config.clone(), crypto_service)
730 .await
731 .expect("Failed to create CosmosKeysService");
732
733 let result = service.create_key(&config).await;
735
736 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 let mut service = CosmosKeysService::new(config.clone(), crypto_service)
758 .await
759 .expect("Failed to create CosmosKeysService");
760
761 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 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 let mut service = CosmosKeysService::new(config.clone(), crypto_service)
792 .await
793 .expect("Failed to create CosmosKeysService");
794
795 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 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 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 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 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}