use alloy_primitives::{hex::FromHex, Bytes};
use serde::{Deserialize, Deserializer, Serialize};
pub type AttestationBytes = Vec<u8>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct V2AttestationResponse {
pub messages: Vec<V2Message>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct V2Message {
pub status: AttestationStatus,
#[serde(default, deserialize_with = "deserialize_optional_bytes_or_pending")]
pub message: Option<Bytes>,
#[serde(default, deserialize_with = "deserialize_optional_bytes_or_pending")]
pub attestation: Option<Bytes>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AttestationResponse {
pub status: AttestationStatus,
#[serde(default, deserialize_with = "deserialize_optional_bytes_or_pending")]
pub attestation: Option<Bytes>,
}
fn deserialize_optional_bytes_or_pending<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
None => Ok(None),
Some(s) if s.is_empty() => Ok(None),
Some(s) if s.eq_ignore_ascii_case("pending") => Ok(None),
Some(s) => {
let bytes = Bytes::from_hex(s).map_err(serde::de::Error::custom)?;
Ok(Some(bytes))
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AttestationStatus {
Complete,
Pending,
PendingConfirmations,
Failed,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_attestation_with_valid_hex() {
let json = r#"{"status":"complete","attestation":"0x1234abcd"}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Complete);
assert!(response.attestation.is_some());
assert_eq!(
response.attestation.unwrap().to_vec(),
vec![0x12, 0x34, 0xab, 0xcd]
);
}
#[test]
fn test_deserialize_attestation_with_pending_string() {
let json = r#"{"status":"pending","attestation":"PENDING"}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Pending);
assert!(response.attestation.is_none());
}
#[test]
fn test_deserialize_attestation_with_pending_lowercase() {
let json = r#"{"status":"pending","attestation":"pending"}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Pending);
assert!(response.attestation.is_none());
}
#[test]
fn test_deserialize_attestation_with_null() {
let json = r#"{"status":"pending","attestation":null}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Pending);
assert!(response.attestation.is_none());
}
#[test]
fn test_deserialize_attestation_missing_field() {
let json = r#"{"status":"pending"}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Pending);
assert!(response.attestation.is_none());
}
#[test]
fn test_deserialize_attestation_with_empty_string() {
let json = r#"{"status":"pending","attestation":""}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Pending);
assert!(response.attestation.is_none());
}
#[test]
fn test_deserialize_attestation_with_hex_no_prefix() {
let json = r#"{"status":"complete","attestation":"deadbeef"}"#;
let response: AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, AttestationStatus::Complete);
assert!(response.attestation.is_some());
assert_eq!(
response.attestation.unwrap().to_vec(),
vec![0xde, 0xad, 0xbe, 0xef]
);
}
#[test]
fn test_deserialize_attestation_with_invalid_hex_fails() {
let json = r#"{"status":"complete","attestation":"not_valid_hex"}"#;
let result = serde_json::from_str::<AttestationResponse>(json);
assert!(result.is_err());
}
#[test]
fn test_deserialize_all_status_variants() {
let complete = r#"{"status":"complete"}"#;
let pending = r#"{"status":"pending"}"#;
let pending_confirmations = r#"{"status":"pending_confirmations"}"#;
let failed = r#"{"status":"failed"}"#;
assert_eq!(
serde_json::from_str::<AttestationResponse>(complete)
.unwrap()
.status,
AttestationStatus::Complete
);
assert_eq!(
serde_json::from_str::<AttestationResponse>(pending)
.unwrap()
.status,
AttestationStatus::Pending
);
assert_eq!(
serde_json::from_str::<AttestationResponse>(pending_confirmations)
.unwrap()
.status,
AttestationStatus::PendingConfirmations
);
assert_eq!(
serde_json::from_str::<AttestationResponse>(failed)
.unwrap()
.status,
AttestationStatus::Failed
);
}
#[test]
fn test_v2_deserialize_complete_response() {
let json = r#"{
"messages": [
{
"status": "complete",
"message": "0xdeadbeef",
"attestation": "0x1234abcd"
}
]
}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 1);
assert_eq!(response.messages[0].status, AttestationStatus::Complete);
assert!(response.messages[0].message.is_some());
assert!(response.messages[0].attestation.is_some());
assert_eq!(
response.messages[0].attestation.as_ref().unwrap().to_vec(),
vec![0x12, 0x34, 0xab, 0xcd]
);
assert_eq!(
response.messages[0].message.as_ref().unwrap().to_vec(),
vec![0xde, 0xad, 0xbe, 0xef]
);
}
#[test]
fn test_v2_deserialize_pending_response() {
let json = r#"{
"messages": [
{
"status": "pending",
"message": null,
"attestation": null
}
]
}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 1);
assert_eq!(response.messages[0].status, AttestationStatus::Pending);
assert!(response.messages[0].message.is_none());
assert!(response.messages[0].attestation.is_none());
}
#[test]
fn test_v2_deserialize_pending_with_string() {
let json = r#"{
"messages": [
{
"status": "pending",
"message": "PENDING",
"attestation": "PENDING"
}
]
}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 1);
assert_eq!(response.messages[0].status, AttestationStatus::Pending);
assert!(response.messages[0].message.is_none());
assert!(response.messages[0].attestation.is_none());
}
#[test]
fn test_v2_deserialize_multiple_messages() {
let json = r#"{
"messages": [
{
"status": "complete",
"message": "0xaa",
"attestation": "0xbb"
},
{
"status": "complete",
"message": "0xcc",
"attestation": "0xdd"
}
]
}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 2);
assert_eq!(response.messages[0].status, AttestationStatus::Complete);
assert_eq!(response.messages[1].status, AttestationStatus::Complete);
}
#[test]
fn test_v2_deserialize_empty_messages() {
let json = r#"{"messages": []}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 0);
}
#[test]
fn test_v2_deserialize_pending_confirmations() {
let json = r#"{
"messages": [
{
"status": "pending_confirmations",
"message": "0xdeadbeef",
"attestation": null
}
]
}"#;
let response: V2AttestationResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.messages.len(), 1);
assert_eq!(
response.messages[0].status,
AttestationStatus::PendingConfirmations
);
assert!(response.messages[0].message.is_some());
assert!(response.messages[0].attestation.is_none());
}
}