diff --git a/Cargo.lock b/Cargo.lock index 4bd553f..cf4426d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2926,6 +2926,7 @@ dependencies = [ "reqwest 0.12.19", "secp256k1", "serde", + "serde_json", "serde_wormhole", "sha3", "solana-account-decoder", diff --git a/Cargo.toml b/Cargo.toml index 5f824db..eb03763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,6 @@ tokio = "1.45.1" tokio-stream = "0.1.17" tracing = "0.1.41" wormhole-vaas-serde = "0.1.0" + +[dev-dependencies] +serde_json = "1.0.140" diff --git a/src/api_client.rs b/src/api_client.rs index 8e0e4ec..4e05ca7 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -111,3 +111,91 @@ impl ApiClient { } } } + +#[cfg(test)] +mod tests { + use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + PublicKey, + }; + use serde_json::Value; + use serde_wormhole::RawMessage; + use wormhole_sdk::{Address, Chain}; + + use super::*; + + #[test] + fn test_new_signed_observation() { + let secret_key = SecretKey::from_byte_array([1u8; 32]).expect("Invalid secret key length"); + let body = Body { + timestamp: 1234567890, + nonce: 42, + emitter_chain: Chain::Solana, + emitter_address: Address([1u8; 32]), + sequence: 1000, + consistency_level: 1, + payload: vec![1, 2, 3, 4, 5], + }; + let observation = + Observation::try_new(body.clone(), secret_key).expect("Failed to create observation"); + assert_eq!(observation.version, 1); + assert_eq!(observation.body, body); + + // Signature verification + let secp = Secp256k1::new(); + let digest = body.digest().expect("Failed to compute digest"); + let message = Message::from_digest(digest.secp256k_hash); + + let recovery_id: RecoveryId = (observation.signature[64] as i32) + .try_into() + .expect("Invalid recovery ID"); + let recoverable_sig = + RecoverableSignature::from_compact(&observation.signature[..64], recovery_id) + .expect("Invalid recoverable signature"); + + let pubkey = secp + .recover_ecdsa(message, &recoverable_sig) + .expect("Failed to recover pubkey"); + + let expected_pubkey = PublicKey::from_secret_key(&secp, &secret_key); + assert_eq!(pubkey, expected_pubkey); + } + + #[test] + fn test_observation_serialization() { + let payload = vec![5, 1, 2, 3, 4, 5]; + let observation = Observation { + version: 1, + signature: [1u8; 65], + body: Body { + timestamp: 1234567890, + nonce: 42, + emitter_chain: Chain::Solana, + emitter_address: Address([1u8; 32]), + sequence: 1000, + consistency_level: 1, + payload: RawMessage::new(payload.as_slice()), + }, + }; + + let serialized = + serde_json::to_string(&observation).expect("Failed to serialize observation"); + let parsed: Value = serde_json::from_str(&serialized).expect("Failed to parse JSON"); + + assert_eq!(parsed["version"], 1); + let sig = parsed["signature"] + .as_str() + .expect("Signature should be a string"); + let decoded = hex::decode(sig).expect("Should be valid hex"); + assert_eq!(decoded, observation.signature); + + let message = parsed["body"].as_array().expect("Body should be an array"); + let bytes = message + .iter() + .map(|v| v.as_u64().expect("Body elements should be u64") as u8) + .collect::>(); + let deserialized: Body<&RawMessage> = + serde_wormhole::from_slice(&bytes).expect("Failed to deserialize body"); + assert_eq!(deserialized, observation.body); + } +} diff --git a/src/posted_message.rs b/src/posted_message.rs index 899bb16..f7e1cec 100644 --- a/src/posted_message.rs +++ b/src/posted_message.rs @@ -7,12 +7,14 @@ use { }, }; -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct PostedMessageUnreliableData { pub message: MessageData, } -#[derive(Debug, Default, BorshSerialize, BorshDeserialize, Clone, Serialize, Deserialize)] +#[derive( + Debug, Default, BorshSerialize, BorshDeserialize, Clone, Serialize, Deserialize, PartialEq, +)] pub struct MessageData { /// Header of the posted VAA pub vaa_version: u8, @@ -89,3 +91,31 @@ impl DerefMut for PostedMessageUnreliableData { &mut self.message } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_borsh_roundtrip() { + let post_message_unreliable_data = PostedMessageUnreliableData { + message: MessageData { + vaa_version: 1, + consistency_level: 2, + vaa_time: 3, + vaa_signature_account: [4u8; 32], + submission_time: 5, + nonce: 6, + sequence: 7, + emitter_chain: 8, + emitter_address: [9u8; 32], + payload: vec![10u8; 32], + }, + }; + + let encoded = borsh::to_vec(&post_message_unreliable_data).unwrap(); + + let decoded = PostedMessageUnreliableData::try_from_slice(encoded.as_slice()).unwrap(); + assert_eq!(decoded, post_message_unreliable_data); + } +}