prices-helper 0.1.0

A Rust library for calculating TWAP and cross-price calculations for Soroban smart contracts. This library provides utilities for working with SEP-40 oracle price data.
Documentation
#[cfg(test)]
mod tests {
    extern crate std;

    use sep_40_oracle::PriceData;
    use soroban_sdk::{Env, Vec};
    use test_case::test_case;

    use crate::{order_prices_by_timestamp, twap, x_price, x_prices, x_twap};

    fn build_prices(env: &Env, items: &[(i128, u64)]) -> Vec<PriceData> {
        let mut v = Vec::new(env);
        for (price, timestamp) in items {
            v.push_back(PriceData {
                price: *price as i128,
                timestamp: *timestamp,
            });
        }
        v
    }

    fn extract_ts(v: &Vec<PriceData>) -> std::vec::Vec<u64> {
        v.iter().map(|p| p.timestamp).collect()
    }

    #[test_case(&[] as &[(i128, u64)], &[] as &[u64]; "empty")]
    #[test_case(&[(100, 1000)], &[1000]; "single element")]
    #[test_case(&[(100, 1000), (200, 2000), (300, 3000)], &[1000, 2000, 3000]; "already sorted")]
    #[test_case(&[(300, 3000), (200, 2000), (100, 1000)], &[1000, 2000, 3000]; "reverse order")]
    #[test_case(&[(200, 2000), (100, 1000), (400, 4000), (300, 3000)], &[1000, 2000, 3000, 4000]; "random order")]
    #[test_case(&[(100, 2000), (200, 2000), (300, 1000)], &[1000, 2000, 2000]; "duplicate timestamps")]
    fn test_order_prices(input: &[(i128, u64)], expected_ts: &[u64]) {
        let env = Env::default();

        let prices = build_prices(&env, input);
        let result = order_prices_by_timestamp(&prices);

        assert_eq!(result.len(), expected_ts.len() as u32);
        assert_eq!(extract_ts(&result).as_slice(), expected_ts);
    }

    #[test_case(&[] as &[(i128, u64)], None; "empty vector")]
    #[test_case(&[(100, 1000)], Some(100); "single price")]
    #[test_case(
        &[(100, 1000), (200, 2000), (300, 3000)],
        Some(200);
        "multiple prices"
    )]
    #[test_case(
        &[(0, 1000), (200, 2000)],
        Some(200);
        "with zero price"
    )]
    #[test_case(
        &[(-100, 1000), (200, 2000)],
        Some(200);
        "with negative price"
    )]
    #[test_case(
        &[(100, 0), (200, 2000)],
        Some(200);
        "with invalid timestamp"
    )]
    #[test_case(
        &[(0, 1000), (-100, 0)],
        None;
        "all invalid prices -> None"
    )]
    fn test_twap_cases(input: &[(i128, u64)], expected: Option<i128>) {
        let env = Env::default();
        let prices = build_prices(&env, input);

        let result = twap(&prices);

        assert_eq!(result, expected);
    }

    #[test_case(100, 50, 0, Some(2); "simple division decimals=0 (100/50=2)")]
    #[test_case(9, 2, 0, Some(4); "floor division 9/2=4")]
    #[test_case(100, -50, 0, None; "negative second price -> None")]
    #[test_case(-100, 50, 0, None; "negative first price -> None")]
    #[test_case(0, 50, 0, None; "zero first price -> None")]
    fn test_x_price_cases(a: i128, b: i128, decimals: u32, expected: Option<i128>) {
        let result = x_price(
            &PriceData {
                price: a,
                timestamp: 1,
            },
            &PriceData {
                price: b,
                timestamp: 1,
            },
            decimals,
        );

        assert_eq!(result, expected);
    }

    #[test_case(
        &[(100, 1_000)],
        &[(50, 1_000)],
        0,
        Some(&[(2, 1_000)][..]); // 100/50=2
        "aligned single element"
    )]
    #[test_case(
        &[(100, 1_000), (200, 2_000), (300, 3_000)],
        &[(50, 1_000), (50, 2_000), (50, 3_000)],
        0,
        Some(&[(2, 1_000), (4, 2_000), (6, 3_000)][..]);
        "aligned multiple elements"
    )]
    #[test_case(
        &[(100, 1_000), (200, 2_000)],
        &[(10, 1_500)],
        0,
        Some(&[(10, 1_500), (20, 2_000)][..]);
        "different lengths with nearest timestamp matching"
    )]
    #[test_case(
        &[(0, 1_000), (-100, 2_000)],
        &[(50, 1_000)],
        0,
        Some(&[][..]);
        "all invalid prices -> empty result vec"
    )]
    fn test_x_prices_cases(
        prices_a: &[(i128, u64)],
        prices_b: &[(i128, u64)],
        decimals: u32,
        expected: Option<&[(i128, u64)]>,
    ) {
        let env = Env::default();
        let a = build_prices(&env, prices_a);
        let b = build_prices(&env, prices_b);

        let result = x_prices(&a, &b, decimals);

        let mapped_result = result.map(|v| {
            v.iter()
                .map(|p| (p.price, p.timestamp))
                .collect::<std::vec::Vec<_>>()
        });

        let expected_vec = expected.map(|s| s.to_vec());

        assert_eq!(mapped_result, expected_vec);
    }

    #[test_case(
        &[] as &[(i128, u64)],
        &[(100, 1_000)],
        0,
        None;
        "empty prices_a -> None"
    )]
    #[test_case(
        &[(100, 1_000)],
        &[] as &[(i128, u64)],
        0,
        None;
        "empty prices_b -> None"
    )]
    #[test_case(
        &[(2000000000, 1_000)],
        &[(1000000000, 1_000)],
        7,
        Some(20000000);
        "single aligned price -> TWAP is that cross price"
    )]
    #[test_case(
        &[(0, 1_000)],
        &[(100, 1_000)],
        7,
        None;
        "invalid cross price -> None"
    )]
    fn test_x_twap_cases(
        prices_a: &[(i128, u64)],
        prices_b: &[(i128, u64)],
        decimals: u32,
        expected: Option<i128>,
    ) {
        let env = Env::default();
        let a = build_prices(&env, prices_a);
        let b = build_prices(&env, prices_b);

        let result = x_twap(&a, &b, decimals);

        assert_eq!(result, expected);
    }
}