pyth-lazer-protocol 0.40.0

Pyth Lazer SDK - protocol types.
Documentation
//! Lazer type definitions and utilities.

/// Types describing Lazer HTTP and WebSocket APIs.
pub mod api;
/// Binary delivery format for WebSocket.
pub mod binary_update;
mod dynamic_value;
mod exchange_enums;
mod feed_kind;
/// Lazer Agent JSON-RPC API.
pub mod jrpc;
/// Types describing Lazer's verifiable messages containing signature and payload.
pub mod message;
/// Types describing Lazer's message payload.
pub mod payload;
mod price;
/// Legacy Websocket API for publishers.
pub mod publisher;
mod rate;
mod serde_price_as_i64;
mod serde_str;
mod symbol_state;
/// Lazer's types for time representation.
pub mod time;

use {
    protobuf::MessageFull,
    serde::{Deserialize, Serialize},
};

use {
    derive_more::{From, Into},
    strum::FromRepr,
};

pub use crate::{
    dynamic_value::DynamicValue,
    exchange_enums::{ExchangeAssetClass, ExchangeAssetSector, ExchangeAssetSubclass},
    feed_kind::FeedKind,
    price::{Price, PriceError},
    rate::{Rate, RateError},
    symbol_state::SymbolState,
};

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
pub struct AssetId(pub u32);

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
pub struct ExchangeId(pub u32);

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
#[cfg_attr(feature = "utoipa", schema(value_type = u16))]
pub struct PublisherId(pub u16);

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
pub struct PriceFeedId(pub u32);

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
pub struct ChannelId(pub u8);

/// Per-publisher datapoint in an aggregate. Represents a price mantissa for
/// price feeds or a rate mantissa for funding rate feeds.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
)]
pub struct PublisherDatapoint(pub i64);

impl From<Price> for PublisherDatapoint {
    fn from(price: Price) -> Self {
        Self(price.mantissa_i64())
    }
}

impl From<Rate> for PublisherDatapoint {
    fn from(rate: Rate) -> Self {
        Self(rate.mantissa())
    }
}

impl ChannelId {
    pub const REAL_TIME: ChannelId = ChannelId(1);
    pub const FIXED_RATE_50: ChannelId = ChannelId(2);
    pub const FIXED_RATE_200: ChannelId = ChannelId(3);
    pub const FIXED_RATE_1000: ChannelId = ChannelId(4);
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, FromRepr)]
#[serde(rename_all = "camelCase")]
#[repr(u8)]
pub enum PriceFeedProperty {
    Price,
    BestBidPrice,
    BestAskPrice,
    PublisherCount,
    Exponent,
    Confidence,
    FundingRate,
    FundingTimestamp,
    FundingRateInterval,
    MarketSession,
    EmaPrice,
    EmaConfidence,
    FeedUpdateTimestamp,
    // More fields may be added later.
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AssetClass {
    Crypto,
    Fx,
    Equity,
    Metal,
    Rates,
    Nav,
    Commodity,
    FundingRate,
    Eco,
    Kalshi,
}

impl AssetClass {
    pub fn as_str(&self) -> &'static str {
        match self {
            AssetClass::Crypto => "crypto",
            AssetClass::Fx => "fx",
            AssetClass::Equity => "equity",
            AssetClass::Metal => "metal",
            AssetClass::Rates => "rates",
            AssetClass::Nav => "nav",
            AssetClass::Commodity => "commodity",
            AssetClass::FundingRate => "funding-rate",
            AssetClass::Eco => "eco",
            AssetClass::Kalshi => "kalshi",
        }
    }
}

// Operation and coefficient for converting value to mantissa.
enum ExponentFactor {
    // mantissa = value * factor
    Mul(i64),
    // mantissa = value / factor
    Div(i64),
}

impl ExponentFactor {
    fn get(exponent: i16) -> Option<Self> {
        if exponent >= 0 {
            let exponent: u32 = exponent.try_into().ok()?;
            Some(ExponentFactor::Div(10_i64.checked_pow(exponent)?))
        } else {
            let minus_exponent: u32 = exponent.checked_neg()?.try_into().ok()?;
            Some(ExponentFactor::Mul(10_i64.checked_pow(minus_exponent)?))
        }
    }
}

pub fn parse_proto_json<M: MessageFull>(
    json: &str,
) -> Result<M, protobuf_json_mapping::ParseError> {
    protobuf_json_mapping::parse_from_str_with_options::<M>(
        json,
        &protobuf_json_mapping::ParseOptions {
            ignore_unknown_fields: true,
            ..Default::default()
        },
    )
}

#[test]
fn magics_in_big_endian() {
    use crate::{
        binary_update::BINARY_UPDATE_FORMAT_MAGIC,
        message::format_magics_le::{
            EVM_FORMAT_MAGIC, JSON_FORMAT_MAGIC, LE_ECDSA_FORMAT_MAGIC, LE_UNSIGNED_FORMAT_MAGIC,
            SOLANA_FORMAT_MAGIC,
        },
        payload::PAYLOAD_FORMAT_MAGIC,
    };

    // The values listed in this test can be used when reading the magic headers in BE format
    // (e.g., on EVM).

    assert_eq!(u32::swap_bytes(BINARY_UPDATE_FORMAT_MAGIC), 1937213467);
    assert_eq!(u32::swap_bytes(PAYLOAD_FORMAT_MAGIC), 1976813459);

    assert_eq!(u32::swap_bytes(SOLANA_FORMAT_MAGIC), 3103857282);
    assert_eq!(u32::swap_bytes(JSON_FORMAT_MAGIC), 2584795844);
    assert_eq!(u32::swap_bytes(EVM_FORMAT_MAGIC), 706910618);
    assert_eq!(u32::swap_bytes(LE_ECDSA_FORMAT_MAGIC), 3837609805);
    assert_eq!(u32::swap_bytes(LE_UNSIGNED_FORMAT_MAGIC), 206398297);

    for magic in [
        BINARY_UPDATE_FORMAT_MAGIC,
        PAYLOAD_FORMAT_MAGIC,
        SOLANA_FORMAT_MAGIC,
        JSON_FORMAT_MAGIC,
        EVM_FORMAT_MAGIC,
        LE_ECDSA_FORMAT_MAGIC,
        LE_UNSIGNED_FORMAT_MAGIC,
    ] {
        // Required to distinguish between byte orders.
        assert_ne!(u32::swap_bytes(magic), magic);
    }
}

#[test]
fn parse_proto_json_works() {
    use protobuf::descriptor::descriptor_proto::ExtensionRange;

    let ts1 = parse_proto_json::<ExtensionRange>(r#"{"start":1,"end":2}"#).unwrap();
    assert_eq!(ts1.start, Some(1));
    assert_eq!(ts1.end, Some(2));

    let ts2 = parse_proto_json::<ExtensionRange>(r#"{"start":3,"extra":5}"#).unwrap();
    assert_eq!(ts2.start, Some(3));
    assert_eq!(ts2.end, None);
}