pyra-types 0.3.1

Shared account types for Pyra services — Drift positions, spot markets, vaults, and Redis cache wrappers
Documentation
//! Shared account types for Pyra services.
//!
//! Provides type definitions for Drift protocol accounts, Pyra vault accounts,
//! and Redis cache wrappers. These types are the superset of fields needed by
//! api-v2, settlement-service, and notification-service for Redis deserialization.

mod cache;
mod drift;
mod pyra;

pub use cache::Cache;
pub use drift::{
    DriftUser, HistoricalOracleData, InsuranceFund, SpotBalanceType, SpotMarket, SpotPosition,
};
pub use pyra::{
    DepositAddressSolAccount, DepositAddressSplAccount, SpendLimitsOrderAccount, TimeLock, Vault,
    WithdrawOrderAccount,
};

pub type VaultCache = Cache<Vault>;
pub type SpotMarketCache = Cache<SpotMarket>;
pub type DriftUserCache = Cache<DriftUser>;
pub type WithdrawOrderCache = Cache<WithdrawOrderAccount>;
pub type SpendLimitsOrderCache = Cache<SpendLimitsOrderAccount>;
pub type DepositAddressSplCache = Cache<DepositAddressSplAccount>;
pub type DepositAddressSolCache = Cache<DepositAddressSolAccount>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cache_deserialize_with_slot() {
        let json = r#"{"account":{"spot_positions":[]},"last_updated_slot":285847350}"#;
        let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
        assert_eq!(cache.last_updated_slot, 285847350);
        assert!(cache.account.spot_positions.is_empty());
    }

    #[test]
    fn cache_deserialize_without_slot() {
        let json = r#"{"account":{"spot_positions":[]}}"#;
        let cache: Cache<DriftUser> = serde_json::from_str(json).unwrap();
        assert_eq!(cache.last_updated_slot, 0);
    }

    #[test]
    fn spot_market_partial_fields() {
        // Core fields (market_index, decimals, interest, margin weights) are required.
        // Optional service-specific fields use serde(default).
        let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0}"#;
        let market: SpotMarket = serde_json::from_str(json).unwrap();
        assert_eq!(market.market_index, 1);
        assert_eq!(market.decimals, 6);
        assert_eq!(market.initial_asset_weight, 8000);
        assert_eq!(market.initial_liability_weight, 12000);
        // Optional fields default to zero
        assert_eq!(market.deposit_balance, 0);
    }

    #[test]
    fn spot_market_missing_core_field_fails() {
        // market_index, decimals, cumulative_*_interest, margin weights are required
        let json = r#"{"market_index":1}"#;
        let result = serde_json::from_str::<SpotMarket>(json);
        assert!(result.is_err());

        // Missing margin weight fields also fails
        let json = r#"{"market_index":1,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50}"#;
        let result = serde_json::from_str::<SpotMarket>(json);
        assert!(result.is_err());
    }

    #[test]
    fn spot_position_with_balance_type() {
        let json = r#"{"scaled_balance":1000000,"market_index":0,"balance_type":"Deposit"}"#;
        let pos: SpotPosition = serde_json::from_str(json).unwrap();
        assert_eq!(pos.scaled_balance, 1000000);
        assert_eq!(pos.balance_type, SpotBalanceType::Deposit);
    }

    #[test]
    fn spot_position_borrow() {
        let json =
            r#"{"scaled_balance":500,"market_index":1,"balance_type":"Borrow","open_orders":2}"#;
        let pos: SpotPosition = serde_json::from_str(json).unwrap();
        assert_eq!(pos.balance_type, SpotBalanceType::Borrow);
        assert_eq!(pos.open_orders, 2);
    }

    #[test]
    fn drift_user_with_positions() {
        let json = r#"{
            "spot_positions": [
                {"scaled_balance":1000,"market_index":0,"balance_type":"Deposit"},
                {"scaled_balance":500,"market_index":1,"balance_type":"Borrow"}
            ]
        }"#;
        let user: DriftUser = serde_json::from_str(json).unwrap();
        assert_eq!(user.spot_positions.len(), 2);
        assert_eq!(user.spot_positions[0].market_index, 0);
        assert_eq!(user.spot_positions[1].balance_type, SpotBalanceType::Borrow);
    }

    #[test]
    fn vault_all_fields_required() {
        let json = r#"{"owner":[1,2,3],"bump":1,"spend_limit_per_transaction":100,"spend_limit_per_timeframe":1000,"remaining_spend_limit_per_timeframe":500,"next_timeframe_reset_timestamp":12345,"timeframe_in_seconds":86400}"#;
        let vault: Vault = serde_json::from_str(json).unwrap();
        assert_eq!(vault.owner, vec![1, 2, 3]);
        assert_eq!(vault.spend_limit_per_transaction, 100);
        assert_eq!(vault.timeframe_in_seconds, 86400);
    }

    #[test]
    fn vault_missing_field_fails() {
        // All fields are required — partial data should fail
        let json = r#"{"owner":[1,2,3]}"#;
        let result = serde_json::from_str::<Vault>(json);
        assert!(result.is_err());
    }

    #[test]
    fn time_lock_deserializes_snake_case() {
        // Pubkey serializes as a byte array [0,0,...,0]
        let zero_pubkey = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
        let json = format!(
            r#"{{"owner":{pk},"payer":{pk},"release_slot":42}}"#,
            pk = zero_pubkey
        );
        let tl: TimeLock = serde_json::from_str(&json).unwrap();
        assert_eq!(tl.release_slot, 42);
    }

    #[test]
    fn time_lock_serializes_camel_case() {
        let tl = TimeLock {
            owner: solana_pubkey::Pubkey::default(),
            payer: solana_pubkey::Pubkey::default(),
            release_slot: 42,
        };
        let json = serde_json::to_string(&tl).unwrap();
        assert!(json.contains("releaseSlot"));
        assert!(!json.contains("release_slot"));
    }

    #[test]
    fn withdraw_order_deserializes_snake_case() {
        let pk = serde_json::to_string(&solana_pubkey::Pubkey::default()).unwrap();
        let json = format!(
            concat!(
                r#"{{"time_lock":{{"owner":{pk},"payer":{pk},"release_slot":100}},"#,
                r#""amount_base_units":5000,"drift_market_index":0,"reduce_only":false,"#,
                r#""destination":{pk}}}"#,
            ),
            pk = pk
        );
        let order: WithdrawOrderAccount = serde_json::from_str(&json).unwrap();
        assert_eq!(order.amount_base_units, 5000);
        assert_eq!(order.drift_market_index, 0);
    }

    #[test]
    fn spot_market_roundtrip() {
        let market = SpotMarket {
            pubkey: vec![],
            market_index: 1,
            initial_asset_weight: 8000,
            initial_liability_weight: 0,
            imf_factor: 0,
            scale_initial_asset_weight_start: 0,
            decimals: 9,
            cumulative_deposit_interest: 1_050_000_000_000,
            cumulative_borrow_interest: 0,
            deposit_balance: 0,
            borrow_balance: 0,
            optimal_utilization: 0,
            optimal_borrow_rate: 0,
            max_borrow_rate: 0,
            min_borrow_rate: 0,
            insurance_fund: InsuranceFund::default(),
            historical_oracle_data: HistoricalOracleData::default(),
            oracle: None,
        };
        let json = serde_json::to_string(&market).unwrap();
        let deserialized: SpotMarket = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.market_index, 1);
        assert_eq!(deserialized.cumulative_deposit_interest, 1_050_000_000_000);
    }

    #[test]
    fn cache_spot_market_roundtrip() {
        let json = r#"{"account":{"market_index":0,"decimals":6,"cumulative_deposit_interest":100,"cumulative_borrow_interest":50,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0},"last_updated_slot":12345}"#;
        let deserialized: Cache<SpotMarket> = serde_json::from_str(json).unwrap();
        assert_eq!(deserialized.account.market_index, 0);
        assert_eq!(deserialized.last_updated_slot, 12345);
    }

    #[test]
    fn spot_market_ignores_unknown_fields() {
        let json = r#"{"market_index":1,"some_future_field":"value","decimals":6,"cumulative_deposit_interest":0,"cumulative_borrow_interest":0,"initial_asset_weight":8000,"initial_liability_weight":12000,"imf_factor":0,"scale_initial_asset_weight_start":0}"#;
        // Default serde behavior: unknown fields are ignored for structs without deny_unknown_fields
        let market: SpotMarket = serde_json::from_str(json).unwrap();
        assert_eq!(market.market_index, 1);
    }
}