Skip to main content

rialo_types/
oracle.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Oracle Module
5//!
6//! This module contains all core oracle-related types and structures.
7//!
8//! ## Oracle Identification
9//! - [`OracleId`] - Uniquely identifies an oracle using a nonce and creator
10//!
11//! ## Oracle Configuration & State
12//! - [`OracleInfo`] - Complete oracle definition and configuration
13//! - [`OracleEntry`] - Oracle registry entry with metadata
14//!
15//! ## Oracle Values
16//! - [`OracleValue`] - String value (plain or encrypted)
17//! - [`OracleValueBody`] - Binary value (plain or encrypted)
18//!
19//! ## Oracle Targets & Updates
20//! - [`TargetOracle`] - Defines what the oracle should query (HTTP, Time, etc.)
21//! - [`OracleUpdateResult`] - Result of an oracle update with signature
22//!
23//! ## Oracle Requests & Scheduling
24//! - [`OracleRequest`] - Request parameters for oracle execution
25//! - [`UpdateFrequency`] - How often the oracle should run
26//! - [`StartingTimestamp`] - When the oracle should start
27
28use std::{
29    collections::BTreeMap,
30    convert::Infallible,
31    fmt,
32    ops::Deref,
33    str::FromStr,
34    sync::Arc,
35    time::{Duration, SystemTime, UNIX_EPOCH},
36};
37
38use borsh::{BorshDeserialize, BorshSerialize};
39#[cfg(feature = "non-pdk")]
40use clap::Subcommand;
41use rialo_cli_representable::Representable;
42use rialo_limits::{max_oracle_output_serialized_bytes, MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE};
43use rialo_s_compute_budget::compute_budget_limits::{MAX_COMPUTE_UNIT_LIMIT, MAX_HEAP_FRAME_BYTES};
44use rialo_s_pubkey::Pubkey;
45use serde::{Deserialize, Serialize};
46use serde_big_array::BigArray;
47#[cfg(feature = "non-pdk")]
48use url::Url;
49
50use crate::{AttestationReport, AuthorityKeyBytes, Headers, HttpFilter, Nonce, OracleDutyConfig};
51
52/// Type alias for timestamp in milliseconds
53// TODO: Unify with BlockTimestampMs in fourier.
54pub type TimestampMs = u64;
55
56/// Lowest allowed update period for periodic oracles, in milliseconds.
57///
58/// This is a pragmatic lower bound to avoid excessive scheduling / load.
59const MIN_UPDATE_PERIOD_MS: TimestampMs = 50;
60
61// ============================================================================
62// Oracle Identification
63// ============================================================================
64
65/// Oracle identifier that uniquely identifies an oracle using a nonce and creator.
66///
67/// # String Parsing
68///
69/// `OracleId` implements `FromStr` which expects a JSON format:
70/// ```json
71/// {"nonce":"<nonce_value>","creator":"<base58_pubkey>"}
72/// ```
73///
74/// This JSON format is used for CLI parsing and other string-based inputs.
75#[derive(
76    Debug,
77    Default,
78    Clone,
79    Copy,
80    PartialEq,
81    Eq,
82    Hash,
83    PartialOrd,
84    Ord,
85    Serialize,
86    Deserialize,
87    BorshSerialize,
88    BorshDeserialize,
89)]
90pub struct OracleId {
91    pub nonce: Nonce,
92    pub creator: Pubkey,
93}
94
95impl OracleId {
96    /// Create a new OracleId from a nonce and creator
97    pub fn new(creator: Pubkey, nonce: impl Into<Nonce>) -> Self {
98        Self {
99            nonce: nonce.into(),
100            creator,
101        }
102    }
103}
104
105impl fmt::Display for OracleId {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "{}:{}", &self.nonce, &self.creator)
108    }
109}
110
111impl FromStr for OracleId {
112    type Err = String;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        // Parse JSON format: {"nonce":"...","creator":"..."}
116        serde_json::from_str(s).map_err(|e| format!("Failed to parse OracleId: {}", e))
117    }
118}
119
120/// Parse an OracleId from a string (for clap CLI parsing)
121#[cfg(feature = "non-pdk")]
122fn parse_oracle_id(s: &str) -> Result<OracleId, String> {
123    OracleId::from_str(s)
124}
125
126// ============================================================================
127// Oracle Configuration & State
128// ============================================================================
129
130/// Represents an oracle's definition and configuration
131#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Representable)]
132#[representable(human_readable = "oracle_info_human_readable")]
133pub struct OracleInfo {
134    /// Unique identifier
135    pub id: OracleId,
136    /// Description for the oracle
137    pub description: String,
138    /// When the oracle should bring back data
139    pub update_frequency: UpdateFrequency,
140    /// URLs for the oracle
141    pub target_oracles: Vec<TargetOracle>,
142    /// Timestamp in which the oracle will start
143    pub starting_timestamp: StartingTimestamp,
144    /// Whether oracle is active
145    pub is_active: bool,
146    /// When oracle was created
147    pub created_at_ms: i64,
148    /// Number of validators to assign per oracle request
149    #[serde(default = "default_validators_per_duty")]
150    pub validators_per_duty: u32,
151    /// Delay sufficient for the oracle to complete, in milliseconds.
152    #[serde(default = "default_oracle_request_delay_ms")]
153    pub oracle_request_delay_ms: TimestampMs,
154    /// Optionally, change the compute usage limit.
155    pub compute_units_limit: Option<u32>,
156    /// Optionally, change the heap size limit.
157    pub heap_size_limit: Option<u32>,
158}
159
160fn oracle_info_human_readable(info: &OracleInfo) -> String {
161    let mut out = String::new();
162
163    out.push_str(&format!("Oracle ID: {}\n", info.id));
164    out.push_str(&format!("Description: {}\n", info.description));
165    out.push_str(&format!("Active: {}\n", info.is_active));
166    out.push_str(&format!(
167        "Starting Timestamp: {:?}\n",
168        info.starting_timestamp
169    ));
170    out.push_str(&format!("Update Frequency: {:?}\n", info.update_frequency));
171    out.push_str(&format!("Created At: {}\n", info.created_at_ms));
172    out.push_str(&format!(
173        "Validators Per Duty: {}\n",
174        info.validators_per_duty
175    ));
176    out.push_str(&format!(
177        "Oracle Request Delay: {}\n",
178        info.oracle_request_delay_ms
179    ));
180
181    if !info.target_oracles.is_empty() {
182        out.push_str(&format!(
183            "\nTarget Oracles ({}):\n",
184            info.target_oracles.len()
185        ));
186        for (i, target) in info.target_oracles.iter().enumerate() {
187            out.push_str(&format!("  {}. {:?}\n", i + 1, target));
188        }
189    }
190
191    if let Some(compute_units) = info.compute_units_limit {
192        out.push_str(&format!("\nCompute Units Limit: {}\n", compute_units));
193    }
194
195    if let Some(heap_size) = info.heap_size_limit {
196        out.push_str(&format!("Heap Size Limit: {}\n", heap_size));
197    }
198
199    out
200}
201
202impl Default for OracleInfo {
203    fn default() -> Self {
204        Self {
205            id: OracleId::default(),
206            description: String::new(),
207            update_frequency: UpdateFrequency::default(),
208            target_oracles: Vec::new(),
209            starting_timestamp: StartingTimestamp::default(),
210            is_active: false,
211            created_at_ms: 0,
212            validators_per_duty: default_validators_per_duty(),
213            oracle_request_delay_ms: default_oracle_request_delay_ms(),
214            compute_units_limit: None,
215            heap_size_limit: None,
216        }
217    }
218}
219
220impl OracleInfo {
221    /// Returns true if the oracle should start as soon as possible (ASAP),
222    /// rather than at a specific timestamp.
223    pub fn is_asap(&self) -> bool {
224        matches!(self.starting_timestamp, StartingTimestamp::Asap)
225    }
226
227    pub fn target_timestamp(&self) -> Option<TimestampMs> {
228        match self.starting_timestamp {
229            StartingTimestamp::Timestamp(timestamp) => Some(timestamp),
230            StartingTimestamp::Asap => None,
231        }
232    }
233
234    /// Validates all oracle configuration fields and their consistency.
235    pub fn validate(&self) -> Result<(), String> {
236        match self.starting_timestamp {
237            StartingTimestamp::Asap => {
238                if !matches!(self.update_frequency, UpdateFrequency::OneShot) {
239                    return Err("ASAP oracles cannot be periodic".to_string());
240                }
241            }
242            StartingTimestamp::Timestamp(starting_timestamp) => {
243                match self.update_frequency {
244                    UpdateFrequency::OneShot => {}
245                    UpdateFrequency::Periodic(period)
246                    | UpdateFrequency::LimitedPeriodic(period, _) => {
247                        validate_periodic_frequency(period)?;
248
249                        // Additional validation for LimitedPeriodic
250                        if let UpdateFrequency::LimitedPeriodic(_, end_timestamp) =
251                            self.update_frequency
252                        {
253                            if starting_timestamp >= end_timestamp {
254                                return Err("end_timestamp of a LimitedPeriodic oracle should be above starting_timestamp".to_string());
255                            }
256                        }
257                    }
258                }
259            }
260        }
261
262        // Validate `target_oracles`.
263        if self.target_oracles.is_empty() {
264            return Err("OracleTargets cannot be empty".to_string());
265        }
266
267        // Validate `oracle_request_delay`.
268        if self.oracle_request_delay_ms < OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY {
269            return Err(format!(
270                "oracle_request_delay cannot be below {}",
271                OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY
272            ));
273        }
274        if self.oracle_request_delay_ms > OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY_MS {
275            return Err(format!(
276                "oracle_request_delay cannot be above {}",
277                OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY_MS
278            ));
279        }
280
281        // Validate `validators_per_duty`.
282        if self.validators_per_duty == 0 {
283            return Err("validators_per_duty cannot be 0".to_string());
284        }
285        let max_oracle_output_size = max_oracle_output_serialized_bytes(self.validators_per_duty);
286        if max_oracle_output_size < MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE {
287            return Err(format!("validators_per_duty is too high, results in max size of oracle updates that is too low: {max_oracle_output_size} vs {MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE}"));
288        }
289
290        // Validate `compute_units_limit`.
291        if let Some(compute_units_limit) = self.compute_units_limit {
292            if compute_units_limit == 0 {
293                return Err("compute_usage_limit cannot be Some(0)".to_string());
294            }
295            if compute_units_limit > MAX_COMPUTE_UNIT_LIMIT {
296                return Err(format!("compute_usage_limit cannot be above MAX_COMPUTE_UNIT_LIMIT={MAX_COMPUTE_UNIT_LIMIT}"));
297            }
298        }
299
300        // Validate `heap_size_limit`.
301        if let Some(heap_size_limit) = self.heap_size_limit {
302            if heap_size_limit == 0 {
303                return Err("heap_size_limit cannot be Some(0)".to_string());
304            }
305            if heap_size_limit > MAX_HEAP_FRAME_BYTES {
306                return Err(format!(
307                    "heap_size_limit cannot be above MAX_HEAP_FRAME_BYTES={MAX_HEAP_FRAME_BYTES}"
308                ));
309            }
310        }
311
312        Ok(())
313    }
314
315    /// Extracts the WebSocket operation from the oracle targets, if any.
316    pub fn websocket_op(&self) -> Option<WebSocketOperation> {
317        self.target_oracles
318            .first()
319            .and_then(|target| target.websocket_op())
320    }
321}
322
323fn validate_periodic_frequency(period_ms: TimestampMs) -> Result<(), String> {
324    if period_ms == 0 {
325        return Err("update frequency cannot be zero".to_string());
326    }
327
328    if period_ms < MIN_UPDATE_PERIOD_MS {
329        return Err(format!(
330            "update frequency {period_ms} cannot be below {MIN_UPDATE_PERIOD_MS}"
331        ));
332    }
333
334    Ok(())
335}
336
337impl TargetOracle {
338    /// Extracts the WebSocket operation if this is a WebSocket target.
339    pub fn websocket_op(&self) -> Option<WebSocketOperation> {
340        if let TargetOracle::WebSocket(ws_op) = self {
341            Some(ws_op.clone())
342        } else {
343            None
344        }
345    }
346}
347
348fn default_validators_per_duty() -> u32 {
349    OracleDutyConfig::DEFAULT_VALIDATORS_PER_DUTY
350}
351
352fn default_oracle_request_delay_ms() -> TimestampMs {
353    OracleDutyConfig::DEFAULT_ORACLE_REQUEST_DELAY_MS
354}
355
356/// Represents an oracle registry entry containing oracle information and metadata.
357///
358/// This struct stores oracle data along with a hash of the data for change detection
359/// and tracking information about when the entry was last modified.
360#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
361pub struct OracleEntry {
362    oracle_info: Arc<OracleInfo>,
363    data_hash: [u8; OracleEntry::HASH_LENGTH],
364    last_modified_timestamp: u64,
365}
366
367impl OracleEntry {
368    const HASH_LENGTH: usize = 32;
369
370    /// Creates a new OracleEntry instance.
371    ///
372    /// # Arguments
373    ///
374    /// * `data`: The account data as a byte vector.
375    /// * `data_hash`: The hash of the account data.
376    /// * `last_modified_timestamp`: The round in which the account was last modified.
377    pub fn new(
378        oracle_info: OracleInfo,
379        data_hash: [u8; Self::HASH_LENGTH],
380        last_modified_round: u64,
381    ) -> Self {
382        Self {
383            oracle_info: Arc::new(oracle_info),
384            data_hash,
385            last_modified_timestamp: last_modified_round,
386        }
387    }
388
389    /// Retrieves the account data.
390    ///
391    /// # Returns
392    ///
393    /// The account data as an OracleInfo.
394    pub fn oracle_info(&self) -> Arc<OracleInfo> {
395        self.oracle_info.clone()
396    }
397
398    /// Retrieves the last modified round.
399    ///
400    /// # Returns
401    ///
402    /// The last modified round as a `u64`.
403    pub fn last_modified_timestamp(&self) -> u64 {
404        self.last_modified_timestamp
405    }
406
407    /// Retrieves the hash of the account data.
408    ///
409    /// # Returns
410    ///
411    /// The hash of the account data as a byte array.
412    pub fn data_hash(&self) -> &[u8; Self::HASH_LENGTH] {
413        &self.data_hash
414    }
415}
416
417// ============================================================================
418// Oracle Values
419// ============================================================================
420
421/// Represents a value that can be either plain text or encrypted.
422///
423/// This enum is used to handle oracle data that may contain sensitive information
424/// that needs to be encrypted when transmitted or stored, while also supporting
425/// plain text values for non-sensitive data.
426///
427/// # Variants
428/// * `Plain(String)` - A plain text value that is not encrypted
429/// * `Encrypted(String)` - A base64-encoded encrypted value that requires decryption within a TEE
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
431pub enum OracleValue {
432    Plain(String),
433    Encrypted(String),
434}
435
436impl Default for OracleValue {
437    fn default() -> Self {
438        OracleValue::Plain(String::new())
439    }
440}
441
442/// A wrapper type around `OracleValue` specifically for handling URLs in oracle configurations.
443///
444/// This type provides a convenient way to handle both plain text and encrypted URLs:
445/// - Plain text URLs are stored directly as strings
446/// - Encrypted URLs are stored as base64-encoded encrypted strings prefixed with "enc://"
447///
448/// The type implements common traits like Display and FromStr for easy conversion and
449/// formatting, and integrates with the url crate when the "non-pdk" feature is enabled.
450///
451/// # Examples
452///
453/// ```
454/// use std::str::FromStr;
455/// use rialo_types::OracleUrl;
456///
457/// // Create from plain text URL
458/// let plain_url = OracleUrl::from("https://example.com");
459///
460/// // Create from encrypted URL
461/// let encrypted_url = OracleUrl::from("enc://encrypted_data");
462/// ```
463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
464pub struct OracleUrl(OracleValue);
465
466impl fmt::Display for OracleUrl {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        match self.0 {
469            OracleValue::Plain(ref s) => write!(f, "{}", s),
470            OracleValue::Encrypted(ref s) => write!(f, "enc://{}", s),
471        }
472    }
473}
474
475impl Deref for OracleUrl {
476    type Target = OracleValue;
477
478    fn deref(&self) -> &Self::Target {
479        &self.0
480    }
481}
482
483#[cfg(feature = "non-pdk")]
484impl From<Url> for OracleUrl {
485    fn from(url: Url) -> Self {
486        url.to_string().into()
487    }
488}
489
490#[cfg(feature = "non-pdk")]
491impl From<&Url> for OracleUrl {
492    fn from(url: &Url) -> Self {
493        Self(OracleValue::Plain(url.to_string()))
494    }
495}
496
497impl From<String> for OracleUrl {
498    fn from(url: String) -> Self {
499        url.as_str().into()
500    }
501}
502
503impl From<&str> for OracleUrl {
504    fn from(s: &str) -> Self {
505        if let Some(encrypted) = s.strip_prefix("enc://") {
506            Self(OracleValue::Encrypted(encrypted.into()))
507        } else {
508            // If it isn't prefixed with `enc://`, treat it as plain text
509            Self(OracleValue::Plain(s.into()))
510        }
511    }
512}
513
514impl FromStr for OracleUrl {
515    type Err = Infallible;
516    fn from_str(s: &str) -> Result<Self, Self::Err> {
517        Ok(s.into())
518    }
519}
520
521/// Represents a value that can be either plain data or encrypted.
522///
523/// This enum is used to handle oracle data that may contain sensitive information
524/// that needs to be encrypted when transmitted or stored, while also supporting
525/// plain text values for non-sensitive data.
526///
527/// # Variants
528/// * `Plain(Vec<u8>)` - Data that is not encrypted
529/// * `Encrypted(Vec<u8>)` - A encrypted data that requires decryption within a TEE
530#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
531pub enum OracleValueBody {
532    Plain(Vec<u8>),
533    Encrypted(Vec<u8>),
534}
535
536impl Default for OracleValueBody {
537    fn default() -> Self {
538        OracleValueBody::Plain(vec![])
539    }
540}
541
542impl FromStr for OracleValueBody {
543    type Err = Infallible;
544    fn from_str(s: &str) -> Result<Self, Self::Err> {
545        Ok(OracleValueBody::Plain(s.as_bytes().to_vec()))
546    }
547}
548
549// ============================================================================
550// Oracle Targets
551// ============================================================================
552
553/// Enum that represents the target of the oracle request.
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, strum_macros::AsRefStr)]
555#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
556pub enum TargetOracle {
557    /// HTTP GET request to the oracle. The URL is specified in the OracleDutyRequest.
558    /// The URL must be an HTTPS URL.
559    /// The filter is optional, and if provided, it is used to filter the response.
560    HttpGet {
561        #[cfg_attr(feature = "non-pdk", clap(
562            long = "target-url",
563            value_parser = clap::value_parser!(OracleUrl)
564        ))]
565        url: OracleUrl,
566        #[cfg_attr(feature = "non-pdk", clap(long, default_value = None))]
567        filter: Option<Vec<HttpFilter>>,
568        #[cfg_attr(feature = "non-pdk", clap(long, default_value_t = Headers::default()))]
569        headers: Headers,
570    },
571    /// HTTP POST request to the oracle. The URL is specified in the OracleDutyRequest.
572    /// The URL must be an HTTPS URL. This oracle type is restricted to single-validator assignment
573    /// to prevent duplicate operations on non-idempotent endpoints.
574    HttpPost {
575        #[cfg_attr(feature = "non-pdk", clap(
576            long = "target-url",
577            value_parser = clap::value_parser!(OracleUrl)
578        ))]
579        url: OracleUrl,
580        #[cfg_attr(feature = "non-pdk", clap(long))]
581        filter: Option<Vec<HttpFilter>>,
582        #[cfg_attr(feature = "non-pdk", clap(
583            long,
584            value_parser = clap::value_parser!(OracleValueBody)
585        ))]
586        body: OracleValueBody,
587        #[cfg_attr(feature = "non-pdk", clap(long))]
588        content_type: String,
589        #[cfg_attr(feature = "non-pdk", clap(long, default_value_t = Headers::default()))]
590        headers: Headers,
591    },
592    /// Get the current time from several NIST servers.
593    Time,
594    /// Get the current price of several select cryptocurrencies.
595    PriceReactor,
596    /// Only used for testing purposes, to simulate an oracle that always returns a fixed value.
597    /// TODO: remove this variant with a cfg testing flag.
598    Number,
599    /// Generate a shared secret key within a committee of TEEs.
600    /// The manager TEE generates the key and distributes it to all committee members.
601    SecretKeyGeneration {
602        #[cfg_attr(feature = "non-pdk", clap(long))]
603        committee_id: String,
604        #[cfg_attr(feature = "non-pdk", clap(long))]
605        committee_members: Vec<String>,
606    },
607    /// Encrypt a secret key for a target TEE using their public key.
608    /// This is used to share keys with TEEs outside the original committee.
609    SecretKeyEncryption {
610        #[cfg_attr(feature = "non-pdk", clap(long))]
611        target_tee_id: String,
612        #[cfg_attr(feature = "non-pdk", clap(long))]
613        secret_data: Vec<u8>,
614        #[cfg_attr(feature = "non-pdk", clap(long))]
615        committee_id: String,
616    },
617    /// Decrypt a secret key that was encrypted for this TEE.
618    /// This is used by TEEs to access keys shared with them.
619    SecretKeyDecryption {
620        #[cfg_attr(feature = "non-pdk", clap(long))]
621        encrypted_data: Vec<u8>,
622        #[cfg_attr(feature = "non-pdk", clap(long))]
623        source_committee_id: String,
624    },
625    /// Reports prices for a batch of stocks based on the websocket price updates from massive.com
626    Stonks,
627    /// WebSocket oracle operations for persistent connections
628    #[cfg_attr(feature = "non-pdk", clap(subcommand))]
629    WebSocket(WebSocketOperation),
630}
631
632impl TargetOracle {
633    /// Returns true if this is a WebSocket operation.
634    pub fn is_websocket(&self) -> bool {
635        matches!(self, TargetOracle::WebSocket(_))
636    }
637}
638
639// ============================================================================
640// WebSocket Operations
641// ============================================================================
642
643// ============================================================================
644// WebSocket Types
645// ============================================================================
646
647/// Estimated size in bytes for system messages (OversizedWarning).
648/// This accounts for the struct fields and some padding.
649pub const SYSTEM_MESSAGE_SIZE: usize = 128;
650
651/// Read mode for WebSocket buffer.
652///
653/// Determines how messages are retrieved from the buffer.
654#[derive(
655    Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
656)]
657pub enum WebSocketReadMode {
658    /// Return only the most recent message (default, backward compatible).
659    #[default]
660    Latest,
661    /// Return all accumulated messages.
662    All,
663    /// Return messages newer than the given index.
664    ///
665    /// **Note:** Index wraparound at `u64::MAX` is not handled. See DataBuffer
666    /// documentation for limitations. At 1 million messages per second,
667    /// wraparound would take ~584,000 years.
668    FromIndex(u64),
669}
670
671/// Message content type - either user data or system notification.
672#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
673pub enum MessageContent {
674    /// Normal message data from WebSocket.
675    Data(Vec<u8>),
676    /// System-generated message for oversized content that was rejected.
677    OversizedWarning {
678        /// Size of the rejected message in bytes.
679        original_size: usize,
680        /// Buffer size limit in bytes.
681        limit: usize,
682        /// When the oversized message was received (RFC3339 with microseconds).
683        original_timestamp: String,
684    },
685}
686
687impl MessageContent {
688    /// Returns the byte size of this content for buffer accounting.
689    ///
690    /// For data messages, this is the actual data length.
691    /// For system messages, this is a fixed size constant.
692    pub fn byte_size(&self) -> usize {
693        match self {
694            MessageContent::Data(data) => data.len(),
695            MessageContent::OversizedWarning { .. } => SYSTEM_MESSAGE_SIZE,
696        }
697    }
698
699    /// Returns true if this is a system message.
700    pub fn is_system(&self) -> bool {
701        matches!(self, MessageContent::OversizedWarning { .. })
702    }
703
704    /// Returns the data bytes if this is a Data message, None otherwise.
705    pub fn as_data(&self) -> Option<&[u8]> {
706        match self {
707            MessageContent::Data(data) => Some(data),
708            MessageContent::OversizedWarning { .. } => None,
709        }
710    }
711}
712
713/// A single buffered message with metadata.
714#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
715pub struct BufferedMessage {
716    /// Unique, monotonically increasing index (never reused, persists across reconnects).
717    pub index: u64,
718    /// The message content.
719    pub content: MessageContent,
720    /// When this message was received/created (RFC3339 with microseconds).
721    pub received_at: String,
722}
723
724impl BufferedMessage {
725    /// Returns the size of this message in bytes (for buffer accounting).
726    pub fn byte_size(&self) -> usize {
727        self.content.byte_size()
728    }
729
730    /// Returns true if this is a system message.
731    pub fn is_system_message(&self) -> bool {
732        self.content.is_system()
733    }
734
735    /// Returns the data bytes if this contains a Data message, None otherwise.
736    pub fn as_data(&self) -> Option<&[u8]> {
737        self.content.as_data()
738    }
739}
740
741/// Response for WebSocket read operations.
742///
743/// This struct captures the read result with messages and buffer state information.
744///
745/// # Gap Detection
746///
747/// Consumers can detect gaps by comparing message indices. Each `BufferedMessage`
748/// contains an `index` field that increases monotonically. If there's a gap between
749/// indices, some messages were evicted from the buffer.
750///
751/// # Convenience Methods
752///
753/// For common access patterns, use the convenience methods instead of accessing
754/// fields directly:
755///
756/// ```ignore
757/// // Instead of:
758/// let data = response.messages.first().unwrap().content.as_data().unwrap();
759///
760/// // Use:
761/// let data = response.first_data().unwrap();
762/// ```
763#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
764pub struct WebSocketReadResponse {
765    /// All requested messages (based on read mode).
766    pub messages: Vec<BufferedMessage>,
767    /// Current highest index in buffer (for tracking).
768    pub latest_index: Option<u64>,
769    /// Current lowest index in buffer (helpful for gap detection).
770    pub oldest_index: Option<u64>,
771}
772
773impl WebSocketReadResponse {
774    /// Returns true if there are any messages in the response.
775    pub fn has_messages(&self) -> bool {
776        !self.messages.is_empty()
777    }
778
779    /// Returns the number of messages in the response.
780    pub fn message_count(&self) -> usize {
781        self.messages.len()
782    }
783
784    /// Get the first (oldest) message, if any.
785    pub fn first_message(&self) -> Option<&BufferedMessage> {
786        self.messages.first()
787    }
788
789    /// Get the last (most recent) message, if any.
790    pub fn latest_message(&self) -> Option<&BufferedMessage> {
791        self.messages.last()
792    }
793
794    /// Get the data from the first message (convenience for common pattern).
795    ///
796    /// Returns `None` if there are no messages or if the first message is a system message.
797    pub fn first_data(&self) -> Option<&[u8]> {
798        self.messages.first().and_then(|m| m.as_data())
799    }
800
801    /// Get the data from the latest message (convenience for common pattern).
802    ///
803    /// Returns `None` if there are no messages or if the latest message is a system message.
804    pub fn latest_data(&self) -> Option<&[u8]> {
805        self.messages.last().and_then(|m| m.as_data())
806    }
807
808    /// Iterate over only the data messages (filtering out system messages).
809    pub fn data_messages(&self) -> impl Iterator<Item = &BufferedMessage> {
810        self.messages.iter().filter(|m| !m.is_system_message())
811    }
812
813    /// Iterate over only the data bytes (convenience for processing).
814    ///
815    /// This filters out system messages and returns only the raw data bytes.
816    pub fn iter_data(&self) -> impl Iterator<Item = &[u8]> {
817        self.messages.iter().filter_map(|m| m.as_data())
818    }
819
820    /// Returns the number of data messages (excluding system messages).
821    pub fn data_message_count(&self) -> usize {
822        self.messages
823            .iter()
824            .filter(|m| !m.is_system_message())
825            .count()
826    }
827
828    /// Returns true if any message is a system message (like OversizedWarning).
829    pub fn has_system_messages(&self) -> bool {
830        self.messages.iter().any(|m| m.is_system_message())
831    }
832}
833
834/// WebSocket operation types for the WebSocket oracle.
835///
836/// This enum defines the different operations that can be performed on WebSocket connections.
837/// All WebSocket operations are handled through a single `TargetOracle::WebSocket` variant.
838///
839/// # Variants
840/// * `Connect` - Establish a new WebSocket connection to an external server
841/// * `Read` - Read the latest data from an existing WebSocket connection
842///
843/// # Future Extensions
844/// Additional operations like `Send` and `Close` may be added in the future.
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
846#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
847pub enum WebSocketOperation {
848    /// Establish a new WebSocket connection.
849    ///
850    /// When this operation is executed:
851    /// - A validator is randomly selected to handle the connection
852    /// - The TEE establishes the WebSocket connection to the specified URL
853    /// - On success, the connection is registered in the WebSocket Registry
854    /// - The assigned validator is recorded for future routing
855    Connect {
856        /// WebSocket URL (typically wss://...)
857        #[cfg_attr(feature = "non-pdk", clap(
858            long = "target-url",
859            value_parser = clap::value_parser!(OracleUrl)
860        ))]
861        url: OracleUrl,
862        /// The OracleId that represents this connection request
863        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_oracle_id))]
864        oracle_id: OracleId,
865    },
866    /// Read data from an existing WebSocket connection.
867    ///
868    /// When this operation is executed:
869    /// - The operation is automatically routed to the TEE holding the connection
870    /// - Returns messages from the connection's buffer based on the read mode
871    /// - The referenced connection must exist and be active
872    ///
873    /// # Read Modes
874    ///
875    /// - `Latest` (default): Returns only the most recent message
876    /// - `All`: Returns all accumulated messages in the buffer
877    /// - `FromIndex(n)`: Returns all messages with index > n, with gap detection
878    ///
879    /// # Validation
880    ///
881    /// The handler implementation must verify that:
882    /// - `connection_oracle_id` references an existing Connect operation in the WebSocket Registry
883    /// - The connection is still active and hasn't been closed
884    /// - The requesting validator has permission to read from this connection
885    ///
886    /// # Error Handling
887    ///
888    /// If validation fails, the oracle should return an appropriate error indicating:
889    /// - `ConnectionNotFound` - if the referenced oracle ID doesn't exist
890    /// - `ConnectionClosed` - if the connection has been terminated
891    /// - `InvalidConnectionType` - if the referenced oracle is not a WebSocket Connect operation
892    Read {
893        /// References the OracleId of the Connect operation that established the connection
894        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_oracle_id))]
895        connection_oracle_id: OracleId,
896        /// The read mode determining which messages to return (defaults to Latest for backward compatibility)
897        #[serde(default)]
898        #[cfg_attr(feature = "non-pdk", clap(skip))]
899        mode: WebSocketReadMode,
900    },
901    /// Send messages to an existing WebSocket connection.
902    ///
903    /// When this operation is executed:
904    /// - The operation is automatically routed to the TEE holding the connection
905    /// - Each `OracleValue` in `messages` is sent as a separate WebSocket message
906    /// - The inner value is extracted: `Plain(String)` → sent as-is, `Encrypted(String)` → decrypted then sent
907    /// - Returns the number of messages successfully sent
908    /// - The referenced connection must exist and be active
909    ///
910    /// # Validation
911    ///
912    /// The handler implementation must verify that:
913    /// - `connection_oracle_id` references an existing Connect operation in the WebSocket Registry
914    /// - The connection is still active and hasn't been closed
915    /// - The requesting validator has permission to send to this connection
916    ///
917    /// # Error Handling
918    ///
919    /// If validation fails, the oracle should return an appropriate error indicating:
920    /// - `ConnectionNotFound` - if the referenced oracle ID doesn't exist
921    /// - `ConnectionClosed` - if the connection has been terminated
922    /// - `InvalidConnectionType` - if the referenced oracle is not a WebSocket Connect operation
923    /// - `SecretDecryptionFailed` - if an encrypted message cannot be decrypted
924    Send {
925        /// References the OracleId of the Connect operation that established the connection
926        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_oracle_id))]
927        connection_oracle_id: OracleId,
928        /// Messages to send to the WebSocket connection
929        /// Each OracleValue will be sent as a separate WebSocket message
930        #[cfg_attr(feature = "non-pdk", clap(skip))]
931        messages: Vec<OracleValue>,
932    },
933    /// Close an existing WebSocket connection.
934    ///
935    /// When this operation is executed:
936    /// - The operation is automatically routed to the TEE holding the connection
937    /// - The WebSocket connection is gracefully closed
938    /// - The connection status in the WebSocket Registry is updated to `Closed`
939    /// - The referenced connection must exist and be active
940    ///
941    /// # Validation
942    ///
943    /// The handler implementation must verify that:
944    /// - `connection_oracle_id` references an existing Connect operation in the WebSocket Registry
945    /// - The requesting validator has permission to close this connection
946    ///
947    /// # Error Handling
948    ///
949    /// If validation fails, the oracle should return an appropriate error indicating:
950    /// - `ConnectionNotFound` - if the referenced oracle ID doesn't exist
951    /// - `ConnectionAlreadyClosed` - if the connection has already been terminated
952    /// - `InvalidConnectionType` - if the referenced oracle is not a WebSocket Connect operation
953    Close {
954        /// References the OracleId of the Connect operation to close
955        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_oracle_id))]
956        connection_oracle_id: OracleId,
957    },
958}
959
960impl WebSocketOperation {
961    /// Create a new Connect operation.
962    pub fn connect(url: impl Into<OracleUrl>, oracle_id: OracleId) -> Self {
963        WebSocketOperation::Connect {
964            url: url.into(),
965            oracle_id,
966        }
967    }
968
969    /// Create a new Read operation with default Latest mode.
970    pub fn read(connection_oracle_id: OracleId) -> Self {
971        WebSocketOperation::Read {
972            connection_oracle_id,
973            mode: WebSocketReadMode::default(),
974        }
975    }
976
977    /// Create a new Read operation with a specific mode.
978    pub fn read_with_mode(connection_oracle_id: OracleId, mode: WebSocketReadMode) -> Self {
979        WebSocketOperation::Read {
980            connection_oracle_id,
981            mode,
982        }
983    }
984
985    /// Create a new Send operation.
986    pub fn send(connection_oracle_id: OracleId, messages: Vec<OracleValue>) -> Self {
987        WebSocketOperation::Send {
988            connection_oracle_id,
989            messages,
990        }
991    }
992
993    /// Create a new Close operation.
994    pub fn close(connection_oracle_id: OracleId) -> Self {
995        WebSocketOperation::Close {
996            connection_oracle_id,
997        }
998    }
999
1000    /// Returns true if this is a Connect operation.
1001    pub fn is_connect(&self) -> bool {
1002        matches!(self, WebSocketOperation::Connect { .. })
1003    }
1004
1005    /// Returns true if this is a Read operation.
1006    pub fn is_read(&self) -> bool {
1007        matches!(self, WebSocketOperation::Read { .. })
1008    }
1009
1010    /// Returns true if this is a Send operation.
1011    pub fn is_send(&self) -> bool {
1012        matches!(self, WebSocketOperation::Send { .. })
1013    }
1014
1015    /// Returns true if this is a Close operation.
1016    pub fn is_close(&self) -> bool {
1017        matches!(self, WebSocketOperation::Close { .. })
1018    }
1019
1020    /// Returns the URL if this is a Connect operation, None otherwise.
1021    pub fn url(&self) -> Option<&OracleUrl> {
1022        match self {
1023            WebSocketOperation::Connect { url, .. } => Some(url),
1024            _ => None,
1025        }
1026    }
1027
1028    /// Returns the oracle_id if this is a Connect operation, None otherwise.
1029    pub fn oracle_id(&self) -> Option<&OracleId> {
1030        match self {
1031            WebSocketOperation::Connect { oracle_id, .. } => Some(oracle_id),
1032            _ => None,
1033        }
1034    }
1035
1036    /// Returns the connection_oracle_id if this is a Read, Send, or Close operation, None otherwise.
1037    pub fn connection_oracle_id(&self) -> Option<&OracleId> {
1038        match self {
1039            WebSocketOperation::Read {
1040                connection_oracle_id,
1041                ..
1042            }
1043            | WebSocketOperation::Send {
1044                connection_oracle_id,
1045                ..
1046            }
1047            | WebSocketOperation::Close {
1048                connection_oracle_id,
1049            } => Some(connection_oracle_id),
1050            _ => None,
1051        }
1052    }
1053
1054    /// Returns the read mode if this is a Read operation, None otherwise.
1055    pub fn read_mode(&self) -> Option<&WebSocketReadMode> {
1056        match self {
1057            WebSocketOperation::Read { mode, .. } => Some(mode),
1058            _ => None,
1059        }
1060    }
1061
1062    /// Returns the messages if this is a Send operation, None otherwise.
1063    pub fn messages(&self) -> Option<&[OracleValue]> {
1064        match self {
1065            WebSocketOperation::Send { messages, .. } => Some(messages),
1066            _ => None,
1067        }
1068    }
1069}
1070
1071impl fmt::Display for WebSocketOperation {
1072    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1073        match self {
1074            WebSocketOperation::Connect { url, oracle_id } => {
1075                write!(f, "Connect(url={url}, oracle_id={oracle_id})")
1076            }
1077            WebSocketOperation::Read {
1078                connection_oracle_id,
1079                mode,
1080            } => {
1081                write!(
1082                    f,
1083                    "Read(connection_oracle_id={}, mode={:?})",
1084                    connection_oracle_id, mode
1085                )
1086            }
1087            WebSocketOperation::Send {
1088                connection_oracle_id,
1089                messages,
1090            } => {
1091                write!(
1092                    f,
1093                    "Send(connection_oracle_id={}, messages_count={})",
1094                    connection_oracle_id,
1095                    messages.len()
1096                )
1097            }
1098            WebSocketOperation::Close {
1099                connection_oracle_id,
1100            } => {
1101                write!(f, "Close(connection_oracle_id={})", connection_oracle_id)
1102            }
1103        }
1104    }
1105}
1106
1107impl FromStr for TargetOracle {
1108    type Err = String;
1109
1110    fn from_str(s: &str) -> Result<Self, Self::Err> {
1111        if s == "Time" {
1112            Ok(TargetOracle::Time)
1113        } else if s == "PriceReactor" {
1114            Ok(TargetOracle::PriceReactor)
1115        } else if s == "SecretKeyGeneration" {
1116            Err("SecretKeyGeneration oracle requires committee_id and committee_members parameters. Use the appropriate API to create this oracle type.".to_string())
1117        } else if s == "SecretKeyEncryption" {
1118            Err("SecretKeyEncryption oracle requires target_tee_id, secret_data, and committee_id parameters. Use the appropriate API to create this oracle type.".to_string())
1119        } else if s == "SecretKeyDecryption" {
1120            Err("SecretKeyDecryption oracle requires encrypted_data and source_committee_id parameters. Use the appropriate API to create this oracle type.".to_string())
1121        } else if s == "number" {
1122            Err("The 'number' oracle is only for testing purposes and should not be used in production.".to_string())
1123        } else {
1124            if let Some(rest) = s.strip_prefix("HttpGet:") {
1125                let parts: Vec<&str> = rest.splitn(2, '|').collect();
1126                if parts.is_empty() {
1127                    return Err(
1128                        "Invalid HttpGet format. Use 'HttpGet:<url>[|<filter>]'.".to_string()
1129                    );
1130                }
1131
1132                let url = parts[0].to_string();
1133                let filter = if parts.len() > 1 && !parts[1].is_empty() {
1134                    Some(vec![HttpFilter::from_str(parts[1])?])
1135                } else {
1136                    None
1137                };
1138
1139                // Validate URL (only when url crate is available)
1140                #[cfg(feature = "non-pdk")]
1141                if Url::parse(&url).is_err() {
1142                    return Err(format!("Invalid URL: {url}"));
1143                }
1144
1145                return Ok(TargetOracle::HttpGet {
1146                    url: url.into(),
1147                    filter,
1148                    headers: Headers::default(),
1149                });
1150            }
1151
1152            Err(format!("Unknown TargetOracle type: {s}"))
1153        }
1154    }
1155}
1156
1157// ============================================================================
1158// Oracle Updates
1159// ============================================================================
1160
1161/// 32‑byte Blake3 hash of the raw request payload observed by the oracle service.
1162///
1163/// This binds a response to the exact input it was computed from and is included
1164/// in the signature transcript as `input_commitment || blake3(response_value)`.
1165pub type InputCommitmentBytes = [u8; 32];
1166
1167/// Raw 64‑byte Ed25519 signature over the oracle response transcript.
1168///
1169/// The transcript signed by the oracle is `input_commitment || blake3(response_value)`.
1170pub type SignatureBytes = [u8; 64];
1171
1172/// A structure representing the result of an oracle update
1173/// This structure is designed to fit within Solana's transaction size limit
1174/// TODO: <https://linear.app/subzero-labs/issue/SUB-449/audit-transaction-sizes-in-the-oracle-subsystem>
1175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1176pub struct OracleUpdateResult {
1177    /// The identifier of the oracle (should be kept under 100 bytes)
1178    pub oracle_id: OracleId,
1179
1180    /// The round in which this result should be proposed
1181    pub target_timestamp: TimestampMs,
1182
1183    /// Blake3 hash of `oracle_result` (32 bytes)
1184    #[serde(with = "BigArray")]
1185    pub response_hash: [u8; 32],
1186
1187    /// Blake3 hash commitment of the original request input bytes (32 bytes)
1188    /// This is included in the signature transcript to bind the response to
1189    /// the exact request payload observed by the oracle service.
1190    #[serde(with = "BigArray")]
1191    pub input_commitment: InputCommitmentBytes,
1192
1193    /// Signature of the hash of the oracle response, base64 encoded.
1194    #[serde(with = "BigArray")]
1195    pub signature: SignatureBytes,
1196
1197    /// Response data (up to MAX_ORACLE_RESPONSE_DATA_SIZE bytes in size)
1198    pub oracle_result: Vec<u8>,
1199
1200    /// Optional attestation report, if available
1201    /// This can be used to provide additional context or verification of the oracle result.
1202    pub attestation_report: Option<AttestationReport>,
1203
1204    /// Validator's protocol public key bytes of the validator that executed the edge rquest.
1205    #[serde(with = "BigArray")]
1206    pub authority_key: AuthorityKeyBytes,
1207}
1208
1209impl OracleUpdateResult {
1210    /// Create an OracleUpdateResult
1211    pub fn new(
1212        oracle_id: OracleId,
1213        target_timestamp: TimestampMs,
1214        oracle_result: Vec<u8>,
1215        input_commitment: InputCommitmentBytes,
1216        signature: SignatureBytes,
1217        attestation_report: Option<AttestationReport>,
1218        authority_key: AuthorityKeyBytes,
1219    ) -> Result<Self, &'static str> {
1220        let hash = blake3::hash(&oracle_result);
1221
1222        // This is a temporary solution to avoid having to deal with the size of the oracle result
1223        #[cfg(feature = "non-pdk")]
1224        let oracle_result = if oracle_result.len() > rialo_limits::MAX_TRANSACTION_SIZE as usize {
1225            tracing::error!(
1226                "Oracle result size {} exceeds maximum size {}, dropping the result.",
1227                oracle_result.len(),
1228                rialo_limits::MAX_TRANSACTION_SIZE
1229            );
1230            return Err("Oracle result exceeds maximum size");
1231        } else {
1232            // Use the oracle result as-is
1233            oracle_result
1234        };
1235
1236        #[cfg(not(feature = "non-pdk"))]
1237        let oracle_result = oracle_result;
1238
1239        Ok(Self {
1240            oracle_id,
1241            target_timestamp,
1242            response_hash: *hash.as_bytes(),
1243            input_commitment,
1244            signature,
1245            oracle_result,
1246            attestation_report,
1247            authority_key,
1248        })
1249    }
1250}
1251
1252impl Default for OracleUpdateResult {
1253    fn default() -> Self {
1254        Self {
1255            oracle_id: OracleId::default(),
1256            target_timestamp: 0,
1257            response_hash: [0; 32],
1258            input_commitment: [0xee; 32],
1259            signature: [0; 64],
1260            oracle_result: vec![],
1261            attestation_report: None,
1262            authority_key: [0xff; 96],
1263        }
1264    }
1265}
1266
1267// ============================================================================
1268// Oracle Requests & Scheduling
1269// ============================================================================
1270
1271/// Represents a request to an oracle with parameters that can be used to specify the query.
1272/// The parameters are stored as a BTreeMap to allow for flexible key-value pairs.
1273/// Contains fields that uniquely identify and configure the request.
1274#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)]
1275pub struct OracleRequest {
1276    /// Structured fields of the request.
1277    pub oracle_id: Option<OracleId>,
1278    pub target_timestamp: Option<TimestampMs>,
1279    pub authority_key: AuthorityKeyBytes,
1280    pub include_attestation: bool,
1281    pub max_oracle_output_size: u32,
1282
1283    /// Request-specific extra data for the request.
1284    pub params: BTreeMap<String, String>,
1285}
1286
1287impl Default for OracleRequest {
1288    fn default() -> Self {
1289        Self {
1290            oracle_id: None,
1291            target_timestamp: None,
1292            authority_key: [0; 96],
1293            include_attestation: true,
1294            max_oracle_output_size: 0,
1295            params: BTreeMap::default(),
1296        }
1297    }
1298}
1299
1300impl OracleRequest {
1301    pub fn input_commitment(&self) -> Result<blake3::Hash, &'static str> {
1302        let request_bytes = borsh::to_vec(self).map_err(|_| "Failed to serialize OracleRequest")?;
1303        Ok(blake3::hash(&request_bytes))
1304    }
1305}
1306
1307/// The frequency of the oracle update.
1308#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
1309pub enum UpdateFrequency {
1310    /// The oracle will update once.
1311    #[default]
1312    OneShot,
1313    /// The oracle will update every N milliseconds.
1314    Periodic(TimestampMs),
1315    /// The oracle will update every N milliseconds, but only up to end_timestamp_ms.
1316    /// First parameter is the frequency in ms, second parameter is the end timestamp in ms.
1317    LimitedPeriodic(TimestampMs, TimestampMs),
1318}
1319
1320/// Starting timestamp configuration for oracles
1321#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
1322pub enum StartingTimestamp {
1323    /// Start at a specific timestamp in milliseconds
1324    Timestamp(TimestampMs),
1325    /// Start as soon as possible
1326    Asap,
1327}
1328
1329impl Default for StartingTimestamp {
1330    fn default() -> Self {
1331        StartingTimestamp::Timestamp(0)
1332    }
1333}
1334
1335impl UpdateFrequency {
1336    /// Creates a periodic update frequency from a [`Duration`].
1337    pub fn periodic(duration: Duration) -> Self {
1338        Self::Periodic(duration.as_millis() as TimestampMs)
1339    }
1340}
1341
1342impl StartingTimestamp {
1343    pub fn start_offset(offset: Duration) -> Self {
1344        let timestamp = SystemTime::now() + offset;
1345        Self::Timestamp(timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis() as TimestampMs)
1346    }
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351    use super::*;
1352
1353    fn base_valid_oracle_info() -> OracleInfo {
1354        OracleInfo {
1355            description: "test".to_string(),
1356            target_oracles: vec![TargetOracle::Time],
1357            // Default is OneShot, which is allowed for both Asap and Timestamp
1358            update_frequency: UpdateFrequency::OneShot,
1359            // Start at timestamp 0 by default
1360            starting_timestamp: StartingTimestamp::Timestamp(0),
1361            // Keep other fields as default
1362            ..OracleInfo::default()
1363        }
1364    }
1365
1366    #[test]
1367    fn test_is_asap_true_and_false() {
1368        let mut info = base_valid_oracle_info();
1369        assert!(!info.is_asap());
1370        info.starting_timestamp = StartingTimestamp::Asap;
1371        assert!(info.is_asap());
1372    }
1373
1374    #[test]
1375    fn test_validate_success_minimal() {
1376        let info = base_valid_oracle_info();
1377        assert!(info.validate().is_ok());
1378    }
1379
1380    #[test]
1381    fn test_asap_cannot_be_periodic() {
1382        let mut info = base_valid_oracle_info();
1383        info.starting_timestamp = StartingTimestamp::Asap;
1384        info.update_frequency = UpdateFrequency::Periodic(10);
1385        let err = info.validate().unwrap_err();
1386        assert!(err.contains("ASAP oracles cannot be periodic"));
1387    }
1388
1389    #[test]
1390    fn test_asap_cannot_be_limited_periodic() {
1391        let mut info = base_valid_oracle_info();
1392        info.starting_timestamp = StartingTimestamp::Asap;
1393        info.update_frequency = UpdateFrequency::LimitedPeriodic(5, 100);
1394        let err = info.validate().unwrap_err();
1395        assert!(err.contains("ASAP oracles cannot be periodic"));
1396    }
1397
1398    #[test]
1399    fn test_periodic_with_zero_period_is_invalid() {
1400        let mut info = base_valid_oracle_info();
1401        info.starting_timestamp = StartingTimestamp::Timestamp(1);
1402        info.update_frequency = UpdateFrequency::Periodic(0);
1403        let err = info.validate().unwrap_err();
1404        assert!(err.contains("update frequency cannot be zero"));
1405    }
1406
1407    #[test]
1408    fn test_limited_periodic_with_zero_period_is_invalid() {
1409        let mut info = base_valid_oracle_info();
1410        info.starting_timestamp = StartingTimestamp::Timestamp(1);
1411        info.update_frequency = UpdateFrequency::LimitedPeriodic(0, 100);
1412        let err = info.validate().unwrap_err();
1413        assert!(err.contains("update frequency cannot be zero"));
1414    }
1415
1416    #[test]
1417    fn test_limited_periodic_end_timestamp_must_be_above_starting_timestamp() {
1418        let mut info = base_valid_oracle_info();
1419        info.starting_timestamp = StartingTimestamp::Timestamp(500);
1420        info.update_frequency = UpdateFrequency::LimitedPeriodic(300, 500);
1421        let err = info.validate().unwrap_err();
1422        assert!(err.contains(
1423            "end_timestamp of a LimitedPeriodic oracle should be above starting_timestamp"
1424        ));
1425    }
1426
1427    #[test]
1428    fn test_limited_periodic_end_timestamp_below_starting_timestamp_is_invalid() {
1429        let mut info = base_valid_oracle_info();
1430        info.starting_timestamp = StartingTimestamp::Timestamp(1000);
1431        info.update_frequency = UpdateFrequency::LimitedPeriodic(300, 900);
1432        let err = info.validate().unwrap_err();
1433        assert!(err.contains(
1434            "end_timestamp of a LimitedPeriodic oracle should be above starting_timestamp"
1435        ));
1436    }
1437
1438    #[test]
1439    fn test_target_oracles_cannot_be_empty() {
1440        let mut info = base_valid_oracle_info();
1441        info.target_oracles.clear();
1442        let err = info.validate().unwrap_err();
1443        assert!(err.contains("OracleTargets cannot be empty"));
1444    }
1445
1446    #[test]
1447    fn test_oracle_request_delay_bounds() {
1448        // Below minimum
1449        let mut info = base_valid_oracle_info();
1450        info.oracle_request_delay_ms = OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY - 1;
1451        let err = info.validate().unwrap_err();
1452        assert!(err.contains(&format!(
1453            "oracle_request_delay cannot be below {}",
1454            OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY
1455        )));
1456
1457        // Above maximum
1458        let mut info = base_valid_oracle_info();
1459        info.oracle_request_delay_ms = OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY_MS + 1;
1460        let err = info.validate().unwrap_err();
1461        assert!(err.contains(&format!(
1462            "oracle_request_delay cannot be above {}",
1463            OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY_MS
1464        )));
1465    }
1466
1467    #[test]
1468    fn test_validators_per_duty_cannot_be_zero() {
1469        let mut info = base_valid_oracle_info();
1470        info.validators_per_duty = 0;
1471        let err = info.validate().unwrap_err();
1472        assert!(err.contains("validators_per_duty cannot be 0"));
1473    }
1474
1475    #[test]
1476    fn test_validators_per_duty_too_high_results_in_too_low_output_size() {
1477        let mut info = base_valid_oracle_info();
1478        info.validators_per_duty = 1_000_000; // ridiculously high
1479        let err = info.validate().unwrap_err();
1480        assert!(err.contains("validators_per_duty is too high"));
1481    }
1482
1483    #[test]
1484    fn test_compute_units_limit_checks() {
1485        // Zero is invalid
1486        let mut info = base_valid_oracle_info();
1487        info.compute_units_limit = Some(0);
1488        let err = info.validate().unwrap_err();
1489        assert!(err.contains("compute_usage_limit cannot be Some(0)"));
1490
1491        // Heap size above the max is invalid
1492        let mut info = base_valid_oracle_info();
1493        info.compute_units_limit = Some(MAX_COMPUTE_UNIT_LIMIT + 1);
1494        let err = info.validate().unwrap_err();
1495        assert!(err.contains(&format!(
1496            "compute_usage_limit cannot be above MAX_COMPUTE_UNIT_LIMIT={}",
1497            MAX_COMPUTE_UNIT_LIMIT
1498        )));
1499    }
1500
1501    #[test]
1502    fn test_heap_size_limit_checks() {
1503        // Zero is invalid
1504        let mut info = base_valid_oracle_info();
1505        info.heap_size_limit = Some(0);
1506        let err = info.validate().unwrap_err();
1507        assert!(err.contains("heap_size_limit cannot be Some(0)"));
1508
1509        // Heap size above the max is invalid
1510        let mut info = base_valid_oracle_info();
1511        info.heap_size_limit = Some(MAX_HEAP_FRAME_BYTES + 1);
1512        let err = info.validate().unwrap_err();
1513        assert!(err.contains(&format!(
1514            "heap_size_limit cannot be above MAX_HEAP_FRAME_BYTES={}",
1515            MAX_HEAP_FRAME_BYTES
1516        )));
1517    }
1518
1519    // ========================================================================
1520    // WebSocket Operation Tests
1521    // ========================================================================
1522
1523    #[test]
1524    fn test_websocket_connect_operation_creation() {
1525        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1526        let op = WebSocketOperation::connect("wss://example.com/stream", oracle_id);
1527        assert!(op.is_connect());
1528        assert!(!op.is_read());
1529
1530        if let WebSocketOperation::Connect {
1531            url,
1532            oracle_id: op_oracle_id,
1533        } = op
1534        {
1535            assert_eq!(url.to_string(), "wss://example.com/stream");
1536            assert_eq!(op_oracle_id, oracle_id);
1537        } else {
1538            panic!("Expected Connect variant");
1539        }
1540    }
1541
1542    #[test]
1543    fn test_websocket_read_operation_creation() {
1544        let oracle_id = OracleId::new(Pubkey::default(), 42u64);
1545        let op = WebSocketOperation::read(oracle_id);
1546        assert!(op.is_read());
1547        assert!(!op.is_connect());
1548
1549        if let WebSocketOperation::Read {
1550            connection_oracle_id,
1551            mode,
1552        } = op
1553        {
1554            assert_eq!(connection_oracle_id, oracle_id);
1555            assert_eq!(mode, WebSocketReadMode::Latest);
1556        } else {
1557            panic!("Expected Read variant");
1558        }
1559    }
1560
1561    #[test]
1562    fn test_websocket_operation_serde_roundtrip_connect() {
1563        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1564        let op = WebSocketOperation::Connect {
1565            url: "wss://example.com/stream".into(),
1566            oracle_id,
1567        };
1568
1569        // Serialize to JSON
1570        let json = serde_json::to_string(&op).expect("Failed to serialize");
1571        // Deserialize back
1572        let deserialized: WebSocketOperation =
1573            serde_json::from_str(&json).expect("Failed to deserialize");
1574
1575        assert_eq!(op, deserialized);
1576    }
1577
1578    #[test]
1579    fn test_websocket_operation_serde_roundtrip_read() {
1580        let oracle_id = OracleId::new(Pubkey::default(), 123u64);
1581        let op = WebSocketOperation::Read {
1582            connection_oracle_id: oracle_id,
1583            mode: WebSocketReadMode::default(),
1584        };
1585
1586        // Serialize to JSON
1587        let json = serde_json::to_string(&op).expect("Failed to serialize");
1588        // Deserialize back
1589        let deserialized: WebSocketOperation =
1590            serde_json::from_str(&json).expect("Failed to deserialize");
1591
1592        assert_eq!(op, deserialized);
1593    }
1594
1595    #[test]
1596    fn test_target_oracle_websocket_serde_roundtrip() {
1597        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1598        let target = TargetOracle::WebSocket(WebSocketOperation::Connect {
1599            url: "wss://example.com/stream".into(),
1600            oracle_id,
1601        });
1602
1603        // Serialize to JSON
1604        let json = serde_json::to_string(&target).expect("Failed to serialize");
1605        // Deserialize back
1606        let deserialized: TargetOracle =
1607            serde_json::from_str(&json).expect("Failed to deserialize");
1608
1609        assert_eq!(target, deserialized);
1610    }
1611
1612    #[test]
1613    fn test_websocket_operation_display() {
1614        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1615        let connect_op = WebSocketOperation::Connect {
1616            url: "wss://example.com".into(),
1617            oracle_id,
1618        };
1619        let display = format!("{}", connect_op);
1620        assert!(display.contains("Connect"));
1621        assert!(display.contains("wss://example.com"));
1622
1623        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1624        let read_op = WebSocketOperation::Read {
1625            connection_oracle_id: oracle_id,
1626            mode: WebSocketReadMode::default(),
1627        };
1628        let display = format!("{}", read_op);
1629        assert!(display.contains("Read"));
1630        assert!(display.contains("connection_oracle_id"));
1631    }
1632
1633    #[test]
1634    fn test_websocket_connect_with_encrypted_url() {
1635        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1636        let op = WebSocketOperation::Connect {
1637            url: "enc://encrypted_websocket_url".into(),
1638            oracle_id,
1639        };
1640
1641        if let WebSocketOperation::Connect { url, .. } = &op {
1642            assert_eq!(url.to_string(), "enc://encrypted_websocket_url");
1643        }
1644
1645        // Verify serde roundtrip preserves encrypted URL
1646        let json = serde_json::to_string(&op).expect("Failed to serialize");
1647        let deserialized: WebSocketOperation =
1648            serde_json::from_str(&json).expect("Failed to deserialize");
1649        assert_eq!(op, deserialized);
1650    }
1651
1652    #[test]
1653    fn test_oracle_info_with_websocket_target() {
1654        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1655        let mut info = base_valid_oracle_info();
1656        info.target_oracles = vec![TargetOracle::WebSocket(WebSocketOperation::Connect {
1657            url: "wss://example.com/stream".into(),
1658            oracle_id,
1659        })];
1660
1661        // Should validate successfully
1662        assert!(info.validate().is_ok());
1663    }
1664
1665    #[test]
1666    fn test_websocket_operation_borsh_roundtrip_connect() {
1667        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1668        let op = WebSocketOperation::Connect {
1669            url: "wss://example.com/stream".into(),
1670            oracle_id,
1671        };
1672
1673        // Serialize to Borsh
1674        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1675        // Deserialize back
1676        let deserialized: WebSocketOperation =
1677            borsh::from_slice(&bytes).expect("Failed to deserialize");
1678
1679        assert_eq!(op, deserialized);
1680    }
1681
1682    #[test]
1683    fn test_websocket_operation_borsh_roundtrip_read() {
1684        let oracle_id = OracleId::new(Pubkey::default(), 123u64);
1685        let op = WebSocketOperation::Read {
1686            connection_oracle_id: oracle_id,
1687            mode: WebSocketReadMode::default(),
1688        };
1689
1690        // Serialize to Borsh
1691        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1692        // Deserialize back
1693        let deserialized: WebSocketOperation =
1694            borsh::from_slice(&bytes).expect("Failed to deserialize");
1695
1696        assert_eq!(op, deserialized);
1697    }
1698
1699    #[test]
1700    fn test_websocket_operation_borsh_roundtrip_encrypted_url() {
1701        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1702        let op = WebSocketOperation::Connect {
1703            url: "enc://encrypted_websocket_url".into(),
1704            oracle_id,
1705        };
1706
1707        // Serialize to Borsh
1708        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1709        // Deserialize back
1710        let deserialized: WebSocketOperation =
1711            borsh::from_slice(&bytes).expect("Failed to deserialize");
1712
1713        assert_eq!(op, deserialized);
1714    }
1715
1716    #[test]
1717    fn test_websocket_operation_url_getter() {
1718        // Connect operation should return Some(url)
1719        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1720        let connect_op = WebSocketOperation::Connect {
1721            url: "wss://example.com/stream".into(),
1722            oracle_id,
1723        };
1724        assert!(connect_op.url().is_some());
1725        assert_eq!(
1726            connect_op.url().unwrap().to_string(),
1727            "wss://example.com/stream"
1728        );
1729
1730        // Read operation should return None
1731        let oracle_id = OracleId::new(Pubkey::default(), 42u64);
1732        let read_op = WebSocketOperation::Read {
1733            connection_oracle_id: oracle_id,
1734            mode: WebSocketReadMode::default(),
1735        };
1736        assert!(read_op.url().is_none());
1737    }
1738
1739    #[test]
1740    fn test_websocket_operation_connection_oracle_id_getter() {
1741        // Read operation should return Some(connection_oracle_id)
1742        let oracle_id = OracleId::new(Pubkey::default(), 42u64);
1743        let read_op = WebSocketOperation::Read {
1744            connection_oracle_id: oracle_id,
1745            mode: WebSocketReadMode::default(),
1746        };
1747        assert!(read_op.connection_oracle_id().is_some());
1748        assert_eq!(*read_op.connection_oracle_id().unwrap(), oracle_id);
1749
1750        // Connect operation should return None
1751        let oracle_id = OracleId::new(Pubkey::default(), 1u64);
1752        let connect_op = WebSocketOperation::Connect {
1753            url: "wss://example.com/stream".into(),
1754            oracle_id,
1755        };
1756        assert!(connect_op.connection_oracle_id().is_none());
1757
1758        // Send operation should return Some(connection_oracle_id)
1759        let oracle_id = OracleId::new(Pubkey::default(), 99u64);
1760        let send_op = WebSocketOperation::Send {
1761            connection_oracle_id: oracle_id,
1762            messages: vec![],
1763        };
1764        assert!(send_op.connection_oracle_id().is_some());
1765        assert_eq!(*send_op.connection_oracle_id().unwrap(), oracle_id);
1766    }
1767
1768    // ========================================================================
1769    // WebSocket Send Operation Tests
1770    // ========================================================================
1771
1772    #[test]
1773    fn test_websocket_send_operation_creation() {
1774        let oracle_id = OracleId::new(Pubkey::default(), 5u64);
1775        let messages = vec![
1776            OracleValue::Plain("message1".to_string()),
1777            OracleValue::Plain("message2".to_string()),
1778        ];
1779        let op = WebSocketOperation::send(oracle_id, messages.clone());
1780        assert!(op.is_send());
1781        assert!(!op.is_connect());
1782        assert!(!op.is_read());
1783
1784        if let WebSocketOperation::Send {
1785            connection_oracle_id,
1786            messages: op_messages,
1787        } = op
1788        {
1789            assert_eq!(connection_oracle_id, oracle_id);
1790            assert_eq!(op_messages, messages);
1791        } else {
1792            panic!("Expected Send variant");
1793        }
1794    }
1795
1796    #[test]
1797    fn test_websocket_send_messages_getter() {
1798        let oracle_id = OracleId::new(Pubkey::default(), 10u64);
1799        let messages = vec![OracleValue::Plain("test".to_string())];
1800        let send_op = WebSocketOperation::Send {
1801            connection_oracle_id: oracle_id,
1802            messages: messages.clone(),
1803        };
1804
1805        assert!(send_op.messages().is_some());
1806        assert_eq!(send_op.messages().unwrap(), &messages[..]);
1807
1808        // Connect and Read should return None
1809        let connect_op = WebSocketOperation::Connect {
1810            url: "wss://example.com".into(),
1811            oracle_id,
1812        };
1813        assert!(connect_op.messages().is_none());
1814
1815        let read_op = WebSocketOperation::Read {
1816            connection_oracle_id: oracle_id,
1817            mode: WebSocketReadMode::default(),
1818        };
1819        assert!(read_op.messages().is_none());
1820    }
1821
1822    #[test]
1823    fn test_websocket_operation_serde_roundtrip_send() {
1824        let oracle_id = OracleId::new(Pubkey::default(), 7u64);
1825        let messages = vec![
1826            OracleValue::Plain("plain_message".to_string()),
1827            OracleValue::Encrypted("encrypted_data".to_string()),
1828        ];
1829        let op = WebSocketOperation::Send {
1830            connection_oracle_id: oracle_id,
1831            messages,
1832        };
1833
1834        // Serialize to JSON
1835        let json = serde_json::to_string(&op).expect("Failed to serialize");
1836        // Deserialize back
1837        let deserialized: WebSocketOperation =
1838            serde_json::from_str(&json).expect("Failed to deserialize");
1839
1840        assert_eq!(op, deserialized);
1841    }
1842
1843    #[test]
1844    fn test_websocket_operation_borsh_roundtrip_send() {
1845        let oracle_id = OracleId::new(Pubkey::default(), 8u64);
1846        let messages = vec![
1847            OracleValue::Plain("hello".to_string()),
1848            OracleValue::Plain("world".to_string()),
1849        ];
1850        let op = WebSocketOperation::Send {
1851            connection_oracle_id: oracle_id,
1852            messages,
1853        };
1854
1855        // Serialize to Borsh
1856        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1857        // Deserialize back
1858        let deserialized: WebSocketOperation =
1859            borsh::from_slice(&bytes).expect("Failed to deserialize");
1860
1861        assert_eq!(op, deserialized);
1862    }
1863
1864    #[test]
1865    fn test_websocket_send_display() {
1866        let oracle_id = OracleId::new(Pubkey::default(), 20u64);
1867        let messages = vec![
1868            OracleValue::Plain("msg1".to_string()),
1869            OracleValue::Plain("msg2".to_string()),
1870            OracleValue::Plain("msg3".to_string()),
1871        ];
1872        let send_op = WebSocketOperation::Send {
1873            connection_oracle_id: oracle_id,
1874            messages,
1875        };
1876
1877        let display = format!("{}", send_op);
1878        assert!(display.contains("Send"));
1879        assert!(display.contains("connection_oracle_id"));
1880        assert!(display.contains("messages_count=3"));
1881    }
1882
1883    #[test]
1884    fn test_websocket_send_empty_messages() {
1885        let oracle_id = OracleId::new(Pubkey::default(), 15u64);
1886        let op = WebSocketOperation::send(oracle_id, vec![]);
1887
1888        assert!(op.is_send());
1889        assert_eq!(op.messages().unwrap().len(), 0);
1890
1891        let display = format!("{}", op);
1892        assert!(display.contains("messages_count=0"));
1893    }
1894
1895    #[test]
1896    fn test_websocket_send_with_encrypted_messages() {
1897        let oracle_id = OracleId::new(Pubkey::default(), 25u64);
1898        let messages = vec![
1899            OracleValue::Encrypted("encrypted1".to_string()),
1900            OracleValue::Encrypted("encrypted2".to_string()),
1901        ];
1902        let op = WebSocketOperation::Send {
1903            connection_oracle_id: oracle_id,
1904            messages: messages.clone(),
1905        };
1906
1907        // Verify serde roundtrip preserves encrypted messages
1908        let json = serde_json::to_string(&op).expect("Failed to serialize");
1909        let deserialized: WebSocketOperation =
1910            serde_json::from_str(&json).expect("Failed to deserialize");
1911        assert_eq!(op, deserialized);
1912
1913        // Verify borsh roundtrip preserves encrypted messages
1914        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1915        let deserialized: WebSocketOperation =
1916            borsh::from_slice(&bytes).expect("Failed to deserialize");
1917        assert_eq!(op, deserialized);
1918    }
1919
1920    #[test]
1921    fn test_target_oracle_websocket_send_serde_roundtrip() {
1922        let oracle_id = OracleId::new(Pubkey::default(), 30u64);
1923        let messages = vec![OracleValue::Plain("test_message".to_string())];
1924        let target = TargetOracle::WebSocket(WebSocketOperation::Send {
1925            connection_oracle_id: oracle_id,
1926            messages,
1927        });
1928
1929        // Serialize to JSON
1930        let json = serde_json::to_string(&target).expect("Failed to serialize");
1931        // Deserialize back
1932        let deserialized: TargetOracle =
1933            serde_json::from_str(&json).expect("Failed to deserialize");
1934
1935        assert_eq!(target, deserialized);
1936    }
1937
1938    // ========================================================================
1939    // WebSocket Close Operation Tests
1940    // ========================================================================
1941
1942    #[test]
1943    fn test_websocket_close_operation_creation() {
1944        let oracle_id = OracleId::new(Pubkey::default(), 50u64);
1945        let op = WebSocketOperation::close(oracle_id);
1946        assert!(op.is_close());
1947        assert!(!op.is_connect());
1948        assert!(!op.is_read());
1949        assert!(!op.is_send());
1950
1951        if let WebSocketOperation::Close {
1952            connection_oracle_id,
1953        } = op
1954        {
1955            assert_eq!(connection_oracle_id, oracle_id);
1956        } else {
1957            panic!("Expected Close variant");
1958        }
1959    }
1960
1961    #[test]
1962    fn test_websocket_close_connection_oracle_id_getter() {
1963        let oracle_id = OracleId::new(Pubkey::default(), 55u64);
1964        let close_op = WebSocketOperation::Close {
1965            connection_oracle_id: oracle_id,
1966        };
1967        assert!(close_op.connection_oracle_id().is_some());
1968        assert_eq!(*close_op.connection_oracle_id().unwrap(), oracle_id);
1969    }
1970
1971    #[test]
1972    fn test_websocket_operation_serde_roundtrip_close() {
1973        let oracle_id = OracleId::new(Pubkey::default(), 60u64);
1974        let op = WebSocketOperation::Close {
1975            connection_oracle_id: oracle_id,
1976        };
1977
1978        // Serialize to JSON
1979        let json = serde_json::to_string(&op).expect("Failed to serialize");
1980        // Deserialize back
1981        let deserialized: WebSocketOperation =
1982            serde_json::from_str(&json).expect("Failed to deserialize");
1983
1984        assert_eq!(op, deserialized);
1985    }
1986
1987    #[test]
1988    fn test_websocket_operation_borsh_roundtrip_close() {
1989        let oracle_id = OracleId::new(Pubkey::default(), 65u64);
1990        let op = WebSocketOperation::Close {
1991            connection_oracle_id: oracle_id,
1992        };
1993
1994        // Serialize to Borsh
1995        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
1996        // Deserialize back
1997        let deserialized: WebSocketOperation =
1998            borsh::from_slice(&bytes).expect("Failed to deserialize");
1999
2000        assert_eq!(op, deserialized);
2001    }
2002
2003    #[test]
2004    fn test_websocket_close_display() {
2005        let oracle_id = OracleId::new(Pubkey::default(), 70u64);
2006        let close_op = WebSocketOperation::Close {
2007            connection_oracle_id: oracle_id,
2008        };
2009
2010        let display = format!("{}", close_op);
2011        assert!(display.contains("Close"));
2012        assert!(display.contains("connection_oracle_id"));
2013        assert!(display.contains(&oracle_id.to_string()));
2014    }
2015
2016    #[test]
2017    fn test_target_oracle_websocket_close_serde_roundtrip() {
2018        let oracle_id = OracleId::new(Pubkey::default(), 75u64);
2019        let target = TargetOracle::WebSocket(WebSocketOperation::Close {
2020            connection_oracle_id: oracle_id,
2021        });
2022
2023        // Serialize to JSON
2024        let json = serde_json::to_string(&target).expect("Failed to serialize");
2025        // Deserialize back
2026        let deserialized: TargetOracle =
2027            serde_json::from_str(&json).expect("Failed to deserialize");
2028
2029        assert_eq!(target, deserialized);
2030    }
2031}