prices_helper/
lib.rs

1use sep_40_oracle::PriceData;
2use soroban_sdk::Vec;
3
4pub(crate) fn order_prices_by_timestamp(prices: &Vec<PriceData>) -> Vec<PriceData> {
5    let mut ordered_prices: Vec<PriceData> = Vec::new(prices.env());
6    for price in prices.iter() {
7        if ordered_prices.is_empty()
8            || price.timestamp
9                >= ordered_prices
10                    .get_unchecked(ordered_prices.len() - 1)
11                    .timestamp
12        {
13            ordered_prices.push_back(price.clone());
14            continue;
15        }
16        for (i, ordered_price) in ordered_prices.iter().enumerate() {
17            if price.timestamp < ordered_price.timestamp {
18                ordered_prices.insert(i as u32, price.clone());
19                break;
20            }
21        }
22    }
23    ordered_prices
24}
25
26pub(crate) fn fixed_div_floor(dividend: i128, divisor: i128, decimals: u32) -> Option<i128> {
27    if dividend <= 0 || divisor <= 0 {
28        return None;
29    }
30    let ashift = core::cmp::min(38 - dividend.ilog10(), decimals);
31    let bshift = core::cmp::max(decimals - ashift, 0);
32
33    let mut vdividend = dividend;
34    let mut vdivisor = divisor;
35    if ashift > 0 {
36        let svdividend = vdividend.checked_mul(10_i128.pow(ashift));
37        if svdividend.is_none() {
38            return None;
39        }
40        vdividend = svdividend?;
41    }
42    if bshift > 0 {
43        vdivisor /= 10_i128.pow(bshift);
44    }
45    if vdivisor <= 0 {
46        return None;
47    }
48    Some(vdividend / vdivisor)
49}
50
51pub fn twap(prices: &Vec<PriceData>) -> Option<i128> {
52    if prices.is_empty() {
53        return None;
54    }
55    let mut sum = 0i128;
56    let mut count = 0i128;
57    for price_data in prices.iter() {
58        if price_data.price > 0 && price_data.timestamp > 0 {
59            sum += price_data.price;
60            count += 1;
61        }
62    }
63    if count == 0 {
64        return None;
65    }
66    Some(sum / count)
67}
68
69pub fn x_twap(prices_a: &Vec<PriceData>, prices_b: &Vec<PriceData>, decimals: u32) -> Option<i128> {
70    if prices_a.is_empty() || prices_b.is_empty() {
71        return None;
72    }
73
74    // Get the cross prices vector
75    let x_prices =
76        x_prices(prices_a, prices_b, decimals).unwrap_or_else(|| Vec::new(prices_a.env()));
77    if x_prices.is_empty() {
78        return None;
79    }
80
81    // Calculate the TWAP of the cross prices
82    twap(&x_prices)
83}
84
85pub fn x_price(price_a: &PriceData, price_b: &PriceData, decimals: u32) -> Option<i128> {
86    if price_a.price <= 0 || price_b.price <= 0 {
87        return None;
88    }
89
90    fixed_div_floor(price_a.price, price_b.price, decimals)
91}
92
93pub fn x_prices(
94    prices_a: &Vec<PriceData>,
95    prices_b: &Vec<PriceData>,
96    decimals: u32,
97) -> Option<Vec<PriceData>> {
98    let mut result = Vec::new(prices_a.env());
99
100    let sorted_prices_a = order_prices_by_timestamp(prices_a);
101    let sorted_prices_b = order_prices_by_timestamp(prices_b);
102
103    // Iterate through both price vectors (use the longer one as the base)
104    for i in 0..prices_a.len().max(prices_b.len()) {
105        // Get the price data at the current index
106        let mut price_a = sorted_prices_a.get(i).unwrap_or_else(|| PriceData {
107            price: 0,
108            timestamp: 0,
109        });
110        let mut price_b = sorted_prices_b.get(i).unwrap_or_else(|| PriceData {
111            price: 0,
112            timestamp: 0,
113        });
114
115        // Looks for the closest timestamp that is less than or equal to the target timestamp
116        fn find_closest_price(target_timestamp: u64, sorted_prices: &Vec<PriceData>) -> PriceData {
117            let mut price = PriceData {
118                price: 0,
119                timestamp: 0,
120            };
121            for i in 0..sorted_prices.len() {
122                let candidate = sorted_prices.get_unchecked(i);
123                if candidate.timestamp > target_timestamp {
124                    break;
125                }
126                price = candidate;
127            }
128            price
129        }
130
131        // Look for the closest timestamp that is less than or equal to the larger timestamp
132        if price_a.timestamp > price_b.timestamp {
133            price_b = find_closest_price(price_a.timestamp, &sorted_prices_b);
134        } else if price_b.timestamp > price_a.timestamp {
135            price_a = find_closest_price(price_b.timestamp, &sorted_prices_a);
136        }
137
138        // Calculate the cross price
139        let cross_price = x_price(&price_a, &price_b, decimals);
140        if cross_price.is_none() {
141            // Skip none prices
142            continue;
143        }
144        result.push_back(PriceData {
145            price: cross_price.unwrap(),
146            timestamp: price_a.timestamp.max(price_b.timestamp),
147        });
148    }
149
150    Some(result)
151}
152
153mod tests;