gmsol_chainlink_datastreams/
report.rs

1use std::fmt;
2
3use num_bigint::{BigInt, BigUint};
4use ruint::aliases::U192;
5
6use chainlink_data_streams_report::{
7    feed_id::ID,
8    report::{base::ReportError, v3::ReportDataV3},
9};
10
11type Sign = bool;
12
13type Signed = (Sign, U192);
14
15/// Report.
16pub struct Report {
17    /// The stream ID the report has data for.
18    pub feed_id: ID,
19    /// Earliest timestamp for which price is applicable.
20    pub valid_from_timestamp: u32,
21    /// Latest timestamp for which price is applicable.
22    pub observations_timestamp: u32,
23    native_fee: U192,
24    link_fee: U192,
25    expires_at: u32,
26    /// DON consensus median price (8 or 18 decimals).
27    price: Signed,
28    /// Simulated price impact of a buy order up to the X% depth of liquidity utilisation (8 or 18 decimals).
29    bid: Signed,
30    /// Simulated price impact of a sell order up to the X% depth of liquidity utilisation (8 or 18 decimals).
31    ask: Signed,
32}
33
34impl Report {
35    /// Decimals.
36    pub const DECIMALS: u8 = 18;
37
38    const WORD_SIZE: usize = 32;
39
40    /// Get non-negative price.
41    pub fn non_negative_price(&self) -> Option<U192> {
42        non_negative(self.price)
43    }
44
45    /// Get non-negative bid.
46    pub fn non_negative_bid(&self) -> Option<U192> {
47        non_negative(self.bid)
48    }
49
50    /// Get non-negative ask.
51    pub fn non_negative_ask(&self) -> Option<U192> {
52        non_negative(self.ask)
53    }
54}
55
56impl fmt::Debug for Report {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.debug_struct("Report")
59            .field("feed_id", &self.feed_id)
60            .field("valid_from_timestamp", &self.valid_from_timestamp)
61            .field("observations_timestamp", &self.observations_timestamp)
62            .field("native_fee", self.native_fee.as_limbs())
63            .field("link_fee", self.link_fee.as_limbs())
64            .field("expires_at", &self.expires_at)
65            .field("price", self.price.1.as_limbs())
66            .field("bid", self.bid.1.as_limbs())
67            .field("ask", self.ask.1.as_limbs())
68            .finish()
69    }
70}
71
72/// Decode Report Error.
73#[derive(Debug, thiserror::Error)]
74pub enum DecodeError {
75    /// Invalid data.
76    #[error("invalid data")]
77    InvalidData,
78    /// Unsupported Version.
79    #[error("unsupported version: {0}")]
80    UnsupportedVersion(u16),
81    /// Overflow.
82    #[error("num overflow")]
83    NumOverflow,
84    /// Negative value.
85    #[error("negative value")]
86    NegativeValue,
87    /// Snap Error.
88    #[error(transparent)]
89    Snap(#[from] snap::Error),
90    /// Report.
91    #[error(transparent)]
92    Report(#[from] chainlink_data_streams_report::report::base::ReportError),
93}
94
95/// Decode compressed full report.
96pub fn decode_compressed_full_report(compressed: &[u8]) -> Result<Report, DecodeError> {
97    use crate::utils::Compressor;
98
99    let data = Compressor::decompress(compressed)?;
100
101    let (_, blob) = decode_full_report(&data)?;
102    decode(blob)
103}
104
105/// Decode Report.
106pub fn decode(data: &[u8]) -> Result<Report, DecodeError> {
107    let report = ReportDataV3::decode(data)?;
108
109    Ok(Report {
110        feed_id: report.feed_id,
111        valid_from_timestamp: report.valid_from_timestamp,
112        observations_timestamp: report.observations_timestamp,
113        native_fee: bigint_to_u192(report.native_fee)?,
114        link_fee: bigint_to_u192(report.link_fee)?,
115        expires_at: report.expires_at,
116        price: bigint_to_signed(report.benchmark_price)?,
117        bid: bigint_to_signed(report.bid)?,
118        ask: bigint_to_signed(report.ask)?,
119    })
120}
121
122fn bigint_to_u192(num: BigInt) -> Result<U192, DecodeError> {
123    let Some(num) = num.to_biguint() else {
124        return Err(DecodeError::NegativeValue);
125    };
126    biguint_to_u192(num)
127}
128
129fn biguint_to_u192(num: BigUint) -> Result<U192, DecodeError> {
130    let mut iter = num.iter_u64_digits();
131    if iter.len() > 3 {
132        return Err(DecodeError::InvalidData);
133    }
134
135    let ans = U192::from_limbs([
136        iter.next().unwrap_or_default(),
137        iter.next().unwrap_or_default(),
138        iter.next().unwrap_or_default(),
139    ]);
140    Ok(ans)
141}
142
143fn bigint_to_signed(num: BigInt) -> Result<Signed, DecodeError> {
144    let (sign, num) = num.into_parts();
145    let sign = !matches!(sign, num_bigint::Sign::Minus);
146    Ok((sign, biguint_to_u192(num)?))
147}
148
149fn non_negative(num: Signed) -> Option<U192> {
150    match num.0 {
151        true => Some(num.1),
152        false => None,
153    }
154}
155
156/// Decode full report.
157pub fn decode_full_report(payload: &[u8]) -> Result<([[u8; 32]; 3], &[u8]), ReportError> {
158    if payload.len() < 128 {
159        return Err(ReportError::DataTooShort("Payload is too short"));
160    }
161
162    // Decode the first three bytes32 elements
163    let mut report_context: [[u8; 32]; 3] = Default::default();
164    for idx in 0..3 {
165        let context = payload[idx * Report::WORD_SIZE..(idx + 1) * Report::WORD_SIZE]
166            .try_into()
167            .map_err(|_| ReportError::ParseError("report_context"))?;
168        report_context[idx] = context;
169    }
170
171    // Decode the offset for the bytes reportBlob data
172    let offset = usize::from_be_bytes(
173        payload[96..128][24..Report::WORD_SIZE] // Offset value is stored as Little Endian
174            .try_into()
175            .map_err(|_| ReportError::ParseError("offset as usize"))?,
176    );
177
178    if offset < 128 || offset >= payload.len() {
179        return Err(ReportError::InvalidLength("offset"));
180    }
181
182    // Decode the length of the bytes reportBlob data
183    let length = usize::from_be_bytes(
184        payload[offset..offset + 32][24..Report::WORD_SIZE] // Length value is stored as Little Endian
185            .try_into()
186            .map_err(|_| ReportError::ParseError("length as usize"))?,
187    );
188
189    if offset + Report::WORD_SIZE + length > payload.len() {
190        return Err(ReportError::InvalidLength("bytes data"));
191    }
192
193    // Decode the remainder of the payload (actual bytes reportBlob data)
194    let report_blob = &payload[offset + Report::WORD_SIZE..offset + Report::WORD_SIZE + length];
195
196    Ok((report_context, report_blob))
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_decode() {
205        let data = hex::decode(
206            "\
207        0006f3dad14cf5df26779bd7b940cd6a9b50ee226256194abbb7643655035d6f\
208        0000000000000000000000000000000000000000000000000000000037a8ac19\
209        0000000000000000000000000000000000000000000000000000000000000000\
210        00000000000000000000000000000000000000000000000000000000000000e0\
211        0000000000000000000000000000000000000000000000000000000000000220\
212        0000000000000000000000000000000000000000000000000000000000000280\
213        0101000000000000000000000000000000000000000000000000000000000000\
214        0000000000000000000000000000000000000000000000000000000000000120\
215        000305a183fedd7f783d99ac138950cff229149703d2a256d61227ad1e5e66ea\
216        000000000000000000000000000000000000000000000000000000006726f480\
217        000000000000000000000000000000000000000000000000000000006726f480\
218        0000000000000000000000000000000000000000000000000000251afa5b7860\
219        000000000000000000000000000000000000000000000000002063f8083c6714\
220        0000000000000000000000000000000000000000000000000000000067284600\
221        000000000000000000000000000000000000000000000000140f9559e8f303f4\
222        000000000000000000000000000000000000000000000000140ede2b99374374\
223        0000000000000000000000000000000000000000000000001410c8d592a7f800\
224        0000000000000000000000000000000000000000000000000000000000000002\
225        abc5fcd50a149ad258673b44c2d1737d175c134a29ab0e1091e1f591af564132\
226        737fedd8929a5e6ee155532f116946351e79c1ea3efdb3c88792f48c7cbb02ca\
227        0000000000000000000000000000000000000000000000000000000000000002\
228        7a478e131ba1474e6b53f2c626ec349f27d64606b1e783d7cb637568ad3b0f7c\
229        3ed29f3fd7de70dc2b08e010ab93448e7dd423047e0f224d7145e0489faa9f23",
230        )
231        .unwrap();
232        let (_, data) = decode_full_report(&data).unwrap();
233        let report = decode(data).unwrap();
234        println!("{report:?}");
235        assert!(report.price == (true, U192::from(1445538218802086900u64)));
236        assert!(report.bid == (true, U192::from(1445336809268003700u64)));
237        assert!(report.ask == (true, U192::from(1445876300000000000u64)));
238        assert_eq!(report.valid_from_timestamp, 1730606208);
239        assert_eq!(report.observations_timestamp, 1730606208);
240    }
241}