diff --git a/packages/wasm-sdk/src/state_transitions/identity/mod.rs b/packages/wasm-sdk/src/state_transitions/identity/mod.rs index e72636445c..8d137fb758 100644 --- a/packages/wasm-sdk/src/state_transitions/identity/mod.rs +++ b/packages/wasm-sdk/src/state_transitions/identity/mod.rs @@ -20,6 +20,49 @@ use tracing::{debug, error}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; +/// Parse a KeyType from its string representation. +fn parse_key_type(s: Option<&str>) -> Result { + match s.ok_or_else(|| WasmSdkError::invalid_argument("keyType is required"))? { + "ECDSA_SECP256K1" => Ok(KeyType::ECDSA_SECP256K1), + "BLS12_381" => Ok(KeyType::BLS12_381), + "ECDSA_HASH160" => Ok(KeyType::ECDSA_HASH160), + "BIP13_SCRIPT_HASH" => Ok(KeyType::BIP13_SCRIPT_HASH), + "EDDSA_25519_HASH160" => Ok(KeyType::EDDSA_25519_HASH160), + s => Err(WasmSdkError::invalid_argument(format!( + "Unknown key type: {}", + s + ))), + } +} + +/// Parse a Purpose from its string representation. +fn parse_purpose(s: Option<&str>) -> Result { + match s.ok_or_else(|| WasmSdkError::invalid_argument("purpose is required"))? { + "AUTHENTICATION" => Ok(Purpose::AUTHENTICATION), + "ENCRYPTION" => Ok(Purpose::ENCRYPTION), + "DECRYPTION" => Ok(Purpose::DECRYPTION), + "TRANSFER" => Ok(Purpose::TRANSFER), + "SYSTEM" => Ok(Purpose::SYSTEM), + "VOTING" => Ok(Purpose::VOTING), + s => Err(WasmSdkError::invalid_argument(format!( + "Unknown purpose: {}", + s + ))), + } +} + +/// Parse a SecurityLevel from its string representation. +/// Defaults to HIGH for None or unknown values. +fn parse_security_level(s: Option<&str>) -> SecurityLevel { + match s { + Some("MASTER") => SecurityLevel::MASTER, + Some("CRITICAL") => SecurityLevel::CRITICAL, + Some("HIGH") => SecurityLevel::HIGH, + Some("MEDIUM") => SecurityLevel::MEDIUM, + _ => SecurityLevel::HIGH, + } +} + /// Check if an ECDSA-derived public key matches an identity's public key. /// Supports ECDSA_SECP256K1 (33-byte comparison) and ECDSA_HASH160 (20-byte comparison). /// Returns false for non-ECDSA key types (BLS, EdDSA, etc.) since they require different derivation. @@ -43,6 +86,207 @@ fn ecdsa_public_key_matches_identity_key( } } +/// Parse public keys from JSON for identity update operations. +/// +/// This function handles the key parsing logic for `identity_update`, supporting: +/// - ECDSA_SECP256K1: Requires `privateKeyHex` or `privateKeyWif`, adds to signer +/// - BLS12_381: Requires `privateKeyHex` (WIF not supported), adds to signer +/// - ECDSA_HASH160: Accepts `privateKeyHex` or `data` field, does NOT add to signer +/// - BIP13_SCRIPT_HASH, EDDSA_25519_HASH160: Requires `data` field, does NOT add to signer +/// +/// Signing key types (where `is_unique_key_type() == true`) are added to the signer +/// so they can sign for themselves during the identity update transition. +fn parse_keys_for_identity_update( + keys_json: &str, + starting_key_id: u32, + signer: &mut SimpleSigner, + network: dash_sdk::dpp::dashcore::Network, +) -> Result, WasmSdkError> { + let keys_data: serde_json::Value = serde_json::from_str(keys_json).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid JSON for add_public_keys: {}", e)) + })?; + + let keys_array = keys_data + .as_array() + .ok_or_else(|| WasmSdkError::invalid_argument("add_public_keys must be a JSON array"))?; + + let mut next_key_id = starting_key_id; + let mut keys_result = Vec::new(); + + for key_data in keys_array { + let key_type = parse_key_type(key_data["keyType"].as_str())?; + let purpose = parse_purpose(key_data["purpose"].as_str())?; + let security_level = parse_security_level(key_data["securityLevel"].as_str()); + + // Handle key data based on key type + // Signing key types (is_unique_key_type == true) require private key + // Non-signing key types use the data field + let (public_key_data, private_key_bytes_opt): (Vec, Option<[u8; 32]>) = match key_type { + KeyType::ECDSA_SECP256K1 => { + // For ECDSA signing keys, require private key (hex or WIF) + let private_key_bytes = if let Some(pk_hex) = key_data["privateKeyHex"].as_str() { + let bytes = hex::decode(pk_hex).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid private key hex: {}", e)) + })?; + bytes.as_slice().try_into().map_err(|_| { + WasmSdkError::invalid_argument(format!( + "Private key must be 32 bytes, got {}", + bytes.len() + )) + })? + } else if let Some(pk_wif) = key_data["privateKeyWif"].as_str() { + let pk = PrivateKey::from_wif(pk_wif).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid WIF private key: {}", e)) + })?; + pk.inner.secret_bytes() + } else { + return Err(WasmSdkError::invalid_argument( + "ECDSA_SECP256K1 keys require either privateKeyHex or privateKeyWif", + )); + }; + + // Derive public key from private key + let public_key_data = key_type + .public_key_data_from_private_key_data(&private_key_bytes, network) + .map_err(|e| { + WasmSdkError::generic(format!( + "Failed to derive ECDSA_SECP256K1 public key: {}", + e + )) + })?; + + (public_key_data, Some(private_key_bytes)) + } + KeyType::BLS12_381 => { + // BLS keys only support hex format (WIF is not valid for BLS) + if key_data["privateKeyWif"].is_string() { + return Err(WasmSdkError::invalid_argument( + "BLS12_381 keys do not support WIF format, use privateKeyHex only", + )); + } + + let private_key_bytes = if let Some(pk_hex) = key_data["privateKeyHex"].as_str() { + let bytes = hex::decode(pk_hex).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid private key hex: {}", e)) + })?; + bytes.as_slice().try_into().map_err(|_| { + WasmSdkError::invalid_argument(format!( + "Private key must be 32 bytes, got {}", + bytes.len() + )) + })? + } else { + return Err(WasmSdkError::invalid_argument( + "BLS12_381 keys require privateKeyHex", + )); + }; + + // Derive public key from private key + let public_key_data = key_type + .public_key_data_from_private_key_data(&private_key_bytes, network) + .map_err(|e| { + WasmSdkError::generic(format!( + "Failed to derive BLS12_381 public key: {}", + e + )) + })?; + + (public_key_data, Some(private_key_bytes)) + } + KeyType::ECDSA_HASH160 => { + // ECDSA_HASH160: Accept privateKeyHex (to derive hash) or data field + if let Some(pk_hex) = key_data["privateKeyHex"].as_str() { + let bytes = hex::decode(pk_hex).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid private key hex: {}", e)) + })?; + let private_key_bytes: [u8; 32] = + bytes.as_slice().try_into().map_err(|_| { + WasmSdkError::invalid_argument(format!( + "Private key must be 32 bytes, got {}", + bytes.len() + )) + })?; + + let derived_data = key_type + .public_key_data_from_private_key_data(&private_key_bytes, network) + .map_err(|e| { + WasmSdkError::generic(format!( + "Failed to derive ECDSA_HASH160 public key: {}", + e + )) + })?; + + (derived_data, None) // No signing needed for HASH160 + } else if let Some(data_str) = key_data["data"].as_str() { + let key_data_bytes = dash_sdk::dpp::dashcore::base64::decode(data_str) + .map_err(|e| { + WasmSdkError::invalid_argument(format!( + "Invalid base64 key data: {}", + e + )) + })?; + if key_data_bytes.len() != 20 { + return Err(WasmSdkError::invalid_argument(format!( + "ECDSA_HASH160 key data must be 20 bytes, got {}", + key_data_bytes.len() + ))); + } + (key_data_bytes, None) + } else { + return Err(WasmSdkError::invalid_argument( + "ECDSA_HASH160 requires either 'privateKeyHex' or 'data' field", + )); + } + } + KeyType::BIP13_SCRIPT_HASH | KeyType::EDDSA_25519_HASH160 => { + // These only accept data field (backward compatible) + let data_str = key_data["data"].as_str().ok_or_else(|| { + WasmSdkError::invalid_argument(format!( + "{:?} keys require 'data' field", + key_type + )) + })?; + let key_data_bytes = + dash_sdk::dpp::dashcore::base64::decode(data_str).map_err(|e| { + WasmSdkError::invalid_argument(format!("Invalid base64 key data: {}", e)) + })?; + // Validate expected length (20 bytes for hash-based key types) + if key_data_bytes.len() != 20 { + return Err(WasmSdkError::invalid_argument(format!( + "{:?} key data must be 20 bytes, got {}", + key_type, + key_data_bytes.len() + ))); + } + (key_data_bytes, None) + } + }; + + // Create the identity public key + use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; + let public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: next_key_id, + key_type, + purpose, + security_level, + contract_bounds: None, + read_only: false, + data: BinaryData::new(public_key_data), + disabled_at: None, + }); + + // Add to signer if this is a signing key type (is_unique_key_type == true) + if let Some(pk_bytes) = private_key_bytes_opt { + signer.add_identity_public_key(public_key.clone(), pk_bytes); + } + + keys_result.push(public_key); + next_key_id += 1; + } + + Ok(keys_result) +} + #[wasm_bindgen] impl WasmSdk { /// Create a new identity on Dash Platform. @@ -144,53 +388,9 @@ impl WasmSdk { .enumerate() .map(|(key, value)| (key as u32, value)) { - let key_type_str = key_data["keyType"] - .as_str() - .ok_or_else(|| WasmSdkError::invalid_argument("keyType is required"))?; - let purpose_str = key_data["purpose"] - .as_str() - .ok_or_else(|| WasmSdkError::invalid_argument("purpose is required"))?; - let security_level_str = key_data["securityLevel"].as_str().unwrap_or("HIGH"); - - // Parse key type first - let key_type = match key_type_str { - "ECDSA_SECP256K1" => KeyType::ECDSA_SECP256K1, - "BLS12_381" => KeyType::BLS12_381, - "ECDSA_HASH160" => KeyType::ECDSA_HASH160, - "BIP13_SCRIPT_HASH" => KeyType::BIP13_SCRIPT_HASH, - "EDDSA_25519_HASH160" => KeyType::EDDSA_25519_HASH160, - _ => { - return Err(WasmSdkError::invalid_argument(format!( - "Unknown key type: {}", - key_type_str - ))); - } - }; - - // Parse purpose - let purpose = match purpose_str { - "AUTHENTICATION" => Purpose::AUTHENTICATION, - "ENCRYPTION" => Purpose::ENCRYPTION, - "DECRYPTION" => Purpose::DECRYPTION, - "TRANSFER" => Purpose::TRANSFER, - "SYSTEM" => Purpose::SYSTEM, - "VOTING" => Purpose::VOTING, - _ => { - return Err(WasmSdkError::invalid_argument(format!( - "Unknown purpose: {}", - purpose_str - ))); - } - }; - - // Parse security level - let security_level = match security_level_str { - "MASTER" => SecurityLevel::MASTER, - "CRITICAL" => SecurityLevel::CRITICAL, - "HIGH" => SecurityLevel::HIGH, - "MEDIUM" => SecurityLevel::MEDIUM, - _ => SecurityLevel::HIGH, - }; + let key_type = parse_key_type(key_data["keyType"].as_str())?; + let purpose = parse_purpose(key_data["purpose"].as_str())?; + let security_level = parse_security_level(key_data["securityLevel"].as_str()); // Handle key data based on key type let (public_key_data, private_key_bytes) = match key_type { @@ -354,8 +554,8 @@ impl WasmSdk { } _ => { return Err(WasmSdkError::invalid_argument(format!( - "Unsupported key type for identity creation: {}", - key_type_str + "Unsupported key type for identity creation: {:?}", + key_type ))); } }; @@ -1045,100 +1245,22 @@ impl WasmSdk { WasmSdkError::invalid_argument("Provided private key does not match any master key") })?; - // Parse and prepare keys to add - let keys_to_add: Vec = if let Some(keys_json) = add_public_keys { - // Parse JSON array of keys - let keys_data: serde_json::Value = serde_json::from_str(&keys_json).map_err(|e| { - WasmSdkError::invalid_argument(format!("Invalid JSON for add_public_keys: {}", e)) - })?; - - let keys_array = keys_data.as_array().ok_or_else(|| { - WasmSdkError::invalid_argument("add_public_keys must be a JSON array") - })?; - - // Get the current max key ID - let mut next_key_id = identity.public_keys().keys().max().copied().unwrap_or(0) + 1; - - keys_array - .iter() - .map(|key_data| { - let key_type_str = key_data["keyType"] - .as_str() - .ok_or_else(|| WasmSdkError::invalid_argument("keyType is required"))?; - let purpose_str = key_data["purpose"] - .as_str() - .ok_or_else(|| WasmSdkError::invalid_argument("purpose is required"))?; - let security_level_str = key_data["securityLevel"].as_str().unwrap_or("HIGH"); - let data_str = key_data["data"] - .as_str() - .ok_or_else(|| WasmSdkError::invalid_argument("data is required"))?; - - // Parse key type - let key_type = match key_type_str { - "ECDSA_SECP256K1" => KeyType::ECDSA_SECP256K1, - "BLS12_381" => KeyType::BLS12_381, - "ECDSA_HASH160" => KeyType::ECDSA_HASH160, - "BIP13_SCRIPT_HASH" => KeyType::BIP13_SCRIPT_HASH, - "EDDSA_25519_HASH160" => KeyType::EDDSA_25519_HASH160, - _ => { - return Err(WasmSdkError::invalid_argument(format!( - "Unknown key type: {}", - key_type_str - ))); - } - }; - - // Parse purpose - let purpose = match purpose_str { - "AUTHENTICATION" => Purpose::AUTHENTICATION, - "ENCRYPTION" => Purpose::ENCRYPTION, - "DECRYPTION" => Purpose::DECRYPTION, - "TRANSFER" => Purpose::TRANSFER, - "SYSTEM" => Purpose::SYSTEM, - "VOTING" => Purpose::VOTING, - _ => { - return Err(WasmSdkError::invalid_argument(format!( - "Unknown purpose: {}", - purpose_str - ))); - } - }; + // Create signer with SimpleSigner to support multiple keys (including new signing keys) + let mut signer = SimpleSigner::default(); - // Parse security level - let security_level = match security_level_str { - "MASTER" => SecurityLevel::MASTER, - "CRITICAL" => SecurityLevel::CRITICAL, - "HIGH" => SecurityLevel::HIGH, - "MEDIUM" => SecurityLevel::MEDIUM, - _ => SecurityLevel::HIGH, - }; + // Get the master public key object and add it to the signer + let master_public_key = identity + .public_keys() + .get(&master_key) + .ok_or_else(|| WasmSdkError::invalid_argument("Master key not found"))? + .clone(); - // Decode key data from base64 - let key_data = - dash_sdk::dpp::dashcore::base64::decode(data_str).map_err(|e| { - WasmSdkError::invalid_argument(format!( - "Invalid base64 key data: {}", - e - )) - })?; + signer.add_identity_public_key(master_public_key, private_key.inner.secret_bytes()); - // Create the identity public key - use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; - let public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { - id: next_key_id, - key_type, - purpose, - security_level, - contract_bounds: None, - read_only: false, - data: BinaryData::new(key_data), - disabled_at: None, - }); - - next_key_id += 1; - Ok(public_key) - }) - .collect::, WasmSdkError>>()? + // Parse and prepare keys to add + let keys_to_add: Vec = if let Some(keys_json) = add_public_keys { + let next_key_id = identity.public_keys().keys().max().copied().unwrap_or(0) + 1; + parse_keys_for_identity_update(&keys_json, next_key_id, &mut signer, self.network())? } else { Vec::new() }; @@ -1185,10 +1307,6 @@ impl WasmSdk { .await .map_err(|e| WasmSdkError::generic(format!("Failed to get identity nonce: {}", e)))?; - // Create signer - let signer = SingleKeySigner::from_string(&private_key_wif, self.network()) - .map_err(WasmSdkError::invalid_argument)?; - // Create the identity update transition use dash_sdk::dpp::state_transition::identity_update_transition::methods::IdentityUpdateTransitionMethodsV0; use dash_sdk::dpp::state_transition::identity_update_transition::IdentityUpdateTransition; @@ -1692,4 +1810,284 @@ mod tests { "REGRESSION: ECDSA_SECP256K1 master key must be matchable with the correct private key" ); } + + // Tests for parse_keys_for_identity_update helper function + // These test the fix for: identity_update cannot add ECDSA_SECP256K1 keys + + #[test] + fn test_parse_keys_ecdsa_secp256k1_with_private_key_hex() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH", + "privateKeyHex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }]"#; + + let keys = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet) + .expect("Should parse ECDSA_SECP256K1 key with privateKeyHex"); + + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(keys[0].data().len(), 33); // Compressed public key + assert_eq!(keys[0].id(), 1); + assert_eq!(keys[0].purpose(), Purpose::AUTHENTICATION); + assert_eq!(keys[0].security_level(), SecurityLevel::HIGH); + + // Key should be added to signer (is_unique_key_type == true) + assert_eq!(signer.private_keys.len(), 1); + assert!(signer.private_keys.contains_key(&keys[0])); + } + + #[test] + fn test_parse_keys_ecdsa_secp256k1_with_private_key_wif() { + use dash_sdk::dpp::dashcore::{Network, PrivateKey}; + + // Generate valid WIF from known private key bytes + let private_key_bytes: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, + ]; + let private_key = PrivateKey::from_byte_array(&private_key_bytes, Network::Testnet) + .expect("Valid private key bytes"); + let wif = private_key.to_wif(); + + let mut signer = SimpleSigner::default(); + let keys_json = format!( + r#"[{{ + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH", + "privateKeyWif": "{}" + }}]"#, + wif + ); + + let keys = parse_keys_for_identity_update(&keys_json, 5, &mut signer, Network::Testnet) + .expect("Should parse ECDSA_SECP256K1 key with privateKeyWif"); + + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(keys[0].data().len(), 33); + assert_eq!(keys[0].id(), 5); + + // Key should be added to signer + assert_eq!(signer.private_keys.len(), 1); + assert!(signer.private_keys.contains_key(&keys[0])); + } + + #[test] + fn test_parse_keys_ecdsa_secp256k1_missing_private_key_fails() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH" + }]"#; + + let result = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("privateKeyHex or privateKeyWif"), + "Error should mention required fields" + ); + } + + #[test] + fn test_parse_keys_bls12_381_with_private_key_hex() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "BLS12_381", + "purpose": "VOTING", + "securityLevel": "HIGH", + "privateKeyHex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }]"#; + + let keys = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet) + .expect("Should parse BLS12_381 key with privateKeyHex"); + + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_type(), KeyType::BLS12_381); + assert_eq!(keys[0].data().len(), 48); // BLS public key is 48 bytes + + // Key should be added to signer (is_unique_key_type == true) + assert_eq!(signer.private_keys.len(), 1); + assert!(signer.private_keys.contains_key(&keys[0])); + } + + #[test] + fn test_parse_keys_bls12_381_rejects_wif() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "BLS12_381", + "purpose": "VOTING", + "securityLevel": "HIGH", + "privateKeyWif": "cNurmMT7bXe3S92JijPi2V5wSHvWzrk4vKJ7fgLPNo5Ajv2YAP3z" + }]"#; + + let result = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("do not support WIF format"), + "Error should indicate WIF is not supported for BLS" + ); + } + + #[test] + fn test_parse_keys_ecdsa_hash160_not_added_to_signer() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "ECDSA_HASH160", + "purpose": "TRANSFER", + "securityLevel": "HIGH", + "privateKeyHex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }]"#; + + let keys = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet) + .expect("Should parse ECDSA_HASH160 key"); + + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_type(), KeyType::ECDSA_HASH160); + assert_eq!(keys[0].data().len(), 20); // Hash160 is 20 bytes + + // Key should NOT be added to signer (is_unique_key_type == false) + assert_eq!(signer.private_keys.len(), 0); + } + + #[test] + fn test_parse_keys_ecdsa_hash160_with_data_field() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + // Base64-encoded 20-byte hash (20 bytes = 27 base64 chars + 1 padding) + let keys_json = r#"[{ + "keyType": "ECDSA_HASH160", + "purpose": "TRANSFER", + "securityLevel": "HIGH", + "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }]"#; + + let keys = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet) + .expect("Should parse ECDSA_HASH160 with data field"); + + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_type(), KeyType::ECDSA_HASH160); + assert_eq!(keys[0].data().len(), 20); + + // Key should NOT be added to signer + assert_eq!(signer.private_keys.len(), 0); + } + + #[test] + fn test_parse_keys_multiple_keys_mixed_types() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[ + { + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH", + "privateKeyHex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "keyType": "ECDSA_HASH160", + "purpose": "TRANSFER", + "securityLevel": "HIGH", + "privateKeyHex": "2102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f21" + } + ]"#; + + let keys = parse_keys_for_identity_update(keys_json, 10, &mut signer, Network::Testnet) + .expect("Should parse multiple keys"); + + assert_eq!(keys.len(), 2); + assert_eq!(keys[0].id(), 10); + assert_eq!(keys[1].id(), 11); + assert_eq!(keys[0].key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(keys[1].key_type(), KeyType::ECDSA_HASH160); + + // Only ECDSA_SECP256K1 should be in signer + assert_eq!(signer.private_keys.len(), 1); + assert!(signer.private_keys.contains_key(&keys[0])); + assert!(!signer.private_keys.contains_key(&keys[1])); + } + + #[test] + fn test_parse_keys_empty_array() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = "[]"; + + let keys = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet) + .expect("Should handle empty array"); + + assert_eq!(keys.len(), 0); + assert_eq!(signer.private_keys.len(), 0); + } + + #[test] + fn test_parse_keys_invalid_json_fails() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = "not valid json"; + + let result = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet); + assert!(result.is_err()); + } + + #[test] + fn test_parse_keys_invalid_private_key_hex_fails() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + let keys_json = r#"[{ + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH", + "privateKeyHex": "not_valid_hex" + }]"#; + + let result = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet); + assert!(result.is_err()); + } + + #[test] + fn test_parse_keys_wrong_length_private_key_fails() { + use dash_sdk::dpp::dashcore::Network; + + let mut signer = SimpleSigner::default(); + // Only 16 bytes instead of 32 + let keys_json = r#"[{ + "keyType": "ECDSA_SECP256K1", + "purpose": "AUTHENTICATION", + "securityLevel": "HIGH", + "privateKeyHex": "0102030405060708090a0b0c0d0e0f10" + }]"#; + + let result = parse_keys_for_identity_update(keys_json, 1, &mut signer, Network::Testnet); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("32 bytes"), + "Error should mention expected length" + ); + } }