gmsol_chainlink_datastreams/
report.rs1use 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
15pub struct Report {
17 pub feed_id: ID,
19 pub valid_from_timestamp: u32,
21 pub observations_timestamp: u32,
23 native_fee: U192,
24 link_fee: U192,
25 expires_at: u32,
26 price: Signed,
28 bid: Signed,
30 ask: Signed,
32}
33
34impl Report {
35 pub const DECIMALS: u8 = 18;
37
38 const WORD_SIZE: usize = 32;
39
40 pub fn non_negative_price(&self) -> Option<U192> {
42 non_negative(self.price)
43 }
44
45 pub fn non_negative_bid(&self) -> Option<U192> {
47 non_negative(self.bid)
48 }
49
50 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#[derive(Debug, thiserror::Error)]
74pub enum DecodeError {
75 #[error("invalid data")]
77 InvalidData,
78 #[error("unsupported version: {0}")]
80 UnsupportedVersion(u16),
81 #[error("num overflow")]
83 NumOverflow,
84 #[error("negative value")]
86 NegativeValue,
87 #[error(transparent)]
89 Snap(#[from] snap::Error),
90 #[error(transparent)]
92 Report(#[from] chainlink_data_streams_report::report::base::ReportError),
93}
94
95pub 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
105pub 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
156pub 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 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 let offset = usize::from_be_bytes(
173 payload[96..128][24..Report::WORD_SIZE] .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 let length = usize::from_be_bytes(
184 payload[offset..offset + 32][24..Report::WORD_SIZE] .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 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}