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
use sep_40_oracle::PriceData;
use soroban_sdk::Vec;

pub(crate) fn order_prices_by_timestamp(prices: &Vec<PriceData>) -> Vec<PriceData> {
    let mut ordered_prices: Vec<PriceData> = Vec::new(prices.env());
    for price in prices.iter() {
        if ordered_prices.is_empty()
            || price.timestamp
                >= ordered_prices
                    .get_unchecked(ordered_prices.len() - 1)
                    .timestamp
        {
            ordered_prices.push_back(price.clone());
            continue;
        }
        for (i, ordered_price) in ordered_prices.iter().enumerate() {
            if price.timestamp < ordered_price.timestamp {
                ordered_prices.insert(i as u32, price.clone());
                break;
            }
        }
    }
    ordered_prices
}

pub(crate) fn fixed_div_floor(dividend: i128, divisor: i128, decimals: u32) -> Option<i128> {
    if dividend <= 0 || divisor <= 0 {
        return None;
    }
    let ashift = core::cmp::min(38 - dividend.ilog10(), decimals);
    let bshift = core::cmp::max(decimals - ashift, 0);

    let mut vdividend = dividend;
    let mut vdivisor = divisor;
    if ashift > 0 {
        let svdividend = vdividend.checked_mul(10_i128.pow(ashift));
        if svdividend.is_none() {
            return None;
        }
        vdividend = svdividend?;
    }
    if bshift > 0 {
        vdivisor /= 10_i128.pow(bshift);
    }
    if vdivisor <= 0 {
        return None;
    }
    Some(vdividend / vdivisor)
}

pub fn twap(prices: &Vec<PriceData>) -> Option<i128> {
    if prices.is_empty() {
        return None;
    }
    let mut sum = 0i128;
    let mut count = 0i128;
    for price_data in prices.iter() {
        if price_data.price > 0 && price_data.timestamp > 0 {
            sum += price_data.price;
            count += 1;
        }
    }
    if count == 0 {
        return None;
    }
    Some(sum / count)
}

pub fn x_twap(prices_a: &Vec<PriceData>, prices_b: &Vec<PriceData>, decimals: u32) -> Option<i128> {
    if prices_a.is_empty() || prices_b.is_empty() {
        return None;
    }

    // Get the cross prices vector
    let x_prices =
        x_prices(prices_a, prices_b, decimals).unwrap_or_else(|| Vec::new(prices_a.env()));
    if x_prices.is_empty() {
        return None;
    }

    // Calculate the TWAP of the cross prices
    twap(&x_prices)
}

pub fn x_price(price_a: &PriceData, price_b: &PriceData, decimals: u32) -> Option<i128> {
    if price_a.price <= 0 || price_b.price <= 0 {
        return None;
    }

    fixed_div_floor(price_a.price, price_b.price, decimals)
}

pub fn x_prices(
    prices_a: &Vec<PriceData>,
    prices_b: &Vec<PriceData>,
    decimals: u32,
) -> Option<Vec<PriceData>> {
    let mut result = Vec::new(prices_a.env());

    let sorted_prices_a = order_prices_by_timestamp(prices_a);
    let sorted_prices_b = order_prices_by_timestamp(prices_b);

    // Iterate through both price vectors (use the longer one as the base)
    for i in 0..prices_a.len().max(prices_b.len()) {
        // Get the price data at the current index
        let mut price_a = sorted_prices_a.get(i).unwrap_or_else(|| PriceData {
            price: 0,
            timestamp: 0,
        });
        let mut price_b = sorted_prices_b.get(i).unwrap_or_else(|| PriceData {
            price: 0,
            timestamp: 0,
        });

        // Looks for the closest timestamp that is less than or equal to the target timestamp
        fn find_closest_price(target_timestamp: u64, sorted_prices: &Vec<PriceData>) -> PriceData {
            let mut price = PriceData {
                price: 0,
                timestamp: 0,
            };
            for i in 0..sorted_prices.len() {
                let candidate = sorted_prices.get_unchecked(i);
                if candidate.timestamp > target_timestamp {
                    break;
                }
                price = candidate;
            }
            price
        }

        // Look for the closest timestamp that is less than or equal to the larger timestamp
        if price_a.timestamp > price_b.timestamp {
            price_b = find_closest_price(price_a.timestamp, &sorted_prices_b);
        } else if price_b.timestamp > price_a.timestamp {
            price_a = find_closest_price(price_b.timestamp, &sorted_prices_a);
        }

        // Calculate the cross price
        let cross_price = x_price(&price_a, &price_b, decimals);
        if cross_price.is_none() {
            // Skip none prices
            continue;
        }
        result.push_back(PriceData {
            price: cross_price.unwrap(),
            timestamp: price_a.timestamp.max(price_b.timestamp),
        });
    }

    Some(result)
}

mod tests;