Skip to content

Commit 5cc36c5

Browse files
committed
feat: Add V8 and V9 Report Schemas to Rust SDK
1 parent a7efa82 commit 5cc36c5

File tree

6 files changed

+479
-6
lines changed

6 files changed

+479
-6
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use chainlink_data_streams_report::report::{decode_full_report, v9::ReportDataV9};
2+
use std::error::Error;
3+
4+
fn main() -> Result<(), Box<dyn Error>> {
5+
let payload = "00090d9e8d96765a0c49e03a6ae05c82e8f8de70cf179baa632f18313e54bd690000000000000000000000000000000000000000000000000000000000d8aa8c000000000000000000000000000000000000000000000000000000030000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400009df89c92184ed9d554ad42f8e8946894af1009f83d8355d99f7f2515b8ecd00000000000000000000000000000000000000000000000000000000686d8cb800000000000000000000000000000000000000000000000000000000686d8cb800000000000000000000000000000000000000000000000000006fc9aa5ea08000000000000000000000000000000000000000000000000000523a3cf5bb582300000000000000000000000000000000000000000000000000000000689519b80000000000000000000000000000000000000000000000000e13e7ef580a17b000000000000000000000000000000000000000000000000000000197e7550c000000000000000000000000000000000000000000000cf942fe702caa1da1e81700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002465e32c3ca1f7b8205cecb3e711925c27ce7937c66919d147063193f4e41caf4ee1813923314660ae905f6d827d706a0793c08111b3311f07506e378dbd76062000000000000000000000000000000000000000000000000000000000000000214b082ae5499faa7ad545415041690602e7adaed33a477a7a8a4255b1887d189532140eef0f57d104e6ed1aca53218296540214b1f4e3437f2d88100283e7bef";
6+
let payload = hex::decode(payload)?;
7+
8+
let (_report_context, report_blob) = decode_full_report(&payload)?;
9+
10+
let report = ReportDataV9::decode(&report_blob)?;
11+
12+
println!("{:#?}", report);
13+
14+
// Prints:
15+
//
16+
// ReportDataV9 {
17+
// feed_id: 0x0009df89c92184ed9d554ad42f8e8946894af1009f83d8355d99f7f2515b8ecd,
18+
// valid_from_timestamp: 1752009912,
19+
// observations_timestamp: 1752009912,
20+
// native_fee: 122911937437824,
21+
// link_fee: 23144981585418275,
22+
// expires_at: 1754601912,
23+
// nav_per_share: 1014409356248750000,
24+
// nav_date: 1751932800000,
25+
// aum: 15684214908922148763592727,
26+
// ripcord: 0,
27+
// }
28+
//
29+
30+
Ok(())
31+
}

rust/crates/report/src/report.rs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ pub mod v1;
44
pub mod v2;
55
pub mod v3;
66
pub mod v4;
7+
pub mod v8;
8+
pub mod v9;
79

810
use base::{ReportBase, ReportError};
911

@@ -117,7 +119,7 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), Re
117119
#[cfg(test)]
118120
mod tests {
119121
use super::*;
120-
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4};
122+
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v8::ReportDataV8, v9::ReportDataV9};
121123
use num_bigint::BigInt;
122124

123125
const V1_FEED_ID: ID = ID([
@@ -136,10 +138,19 @@ mod tests {
136138
00, 04, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
137139
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
138140
]);
141+
const V8_FEED_ID: ID = ID([
142+
00, 08, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
143+
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
144+
]);
145+
const V9_FEED_ID: ID = ID([
146+
00, 09, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
147+
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
148+
]);
139149

140150
pub const MOCK_TIMESTAMP: u32 = 1718885772;
141151
pub const MOCK_FEE: usize = 10;
142152
pub const MOCK_PRICE: isize = 100;
153+
pub const MARKET_STATUS_OPEN: u32 = 2;
143154

144155
pub fn generate_mock_report_data_v1() -> ReportDataV1 {
145156
let report_data = ReportDataV1 {
@@ -193,8 +204,6 @@ mod tests {
193204
}
194205

195206
pub fn generate_mock_report_data_v4() -> ReportDataV4 {
196-
const MARKET_STATUS_OPEN: u32 = 2;
197-
198207
let report_data = ReportDataV4 {
199208
feed_id: V4_FEED_ID,
200209
valid_from_timestamp: MOCK_TIMESTAMP,
@@ -209,6 +218,43 @@ mod tests {
209218
report_data
210219
}
211220

221+
pub fn generate_mock_report_data_v8() -> ReportDataV8 {
222+
let report_data = ReportDataV8 {
223+
feed_id: V8_FEED_ID,
224+
valid_from_timestamp: MOCK_TIMESTAMP,
225+
observations_timestamp: MOCK_TIMESTAMP,
226+
native_fee: BigInt::from(MOCK_FEE),
227+
link_fee: BigInt::from(MOCK_FEE),
228+
expires_at: MOCK_TIMESTAMP + 100,
229+
last_update_timestamp: MOCK_TIMESTAMP as u64,
230+
mid_price: BigInt::from(MOCK_PRICE),
231+
market_status: MARKET_STATUS_OPEN as u8,
232+
};
233+
234+
report_data
235+
}
236+
237+
pub fn generate_mock_report_data_v9() -> ReportDataV9 {
238+
const MOCK_NAV_PER_SHARE: isize = 1;
239+
const MOCK_AUM: isize = 1000;
240+
const RIPCORD_NORMAL: u32 = 0;
241+
242+
let report_data = ReportDataV9 {
243+
feed_id: V9_FEED_ID,
244+
valid_from_timestamp: MOCK_TIMESTAMP,
245+
observations_timestamp: MOCK_TIMESTAMP,
246+
native_fee: BigInt::from(MOCK_FEE),
247+
link_fee: BigInt::from(MOCK_FEE),
248+
expires_at: MOCK_TIMESTAMP + 100,
249+
nav_per_share: BigInt::from(MOCK_NAV_PER_SHARE),
250+
nav_date: MOCK_TIMESTAMP as u64,
251+
aum: BigInt::from(MOCK_AUM),
252+
ripcord: RIPCORD_NORMAL,
253+
};
254+
255+
report_data
256+
}
257+
212258
fn generate_mock_report(encoded_report_data: &[u8]) -> Vec<u8> {
213259
let mut payload = Vec::new();
214260

@@ -367,4 +413,67 @@ mod tests {
367413

368414
assert_eq!(decoded_report.feed_id, V4_FEED_ID);
369415
}
416+
417+
#[test]
418+
fn test_decode_report_v8() {
419+
let report_data = generate_mock_report_data_v8();
420+
let encoded_report_data = report_data.abi_encode().unwrap();
421+
422+
let report = generate_mock_report(&encoded_report_data);
423+
424+
let (_report_context, report_blob) = decode_full_report(&report).unwrap();
425+
426+
let expected_report_blob = vec![
427+
"00086b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
428+
"0000000000000000000000000000000000000000000000000000000066741d8c",
429+
"0000000000000000000000000000000000000000000000000000000066741d8c",
430+
"000000000000000000000000000000000000000000000000000000000000000a",
431+
"000000000000000000000000000000000000000000000000000000000000000a",
432+
"0000000000000000000000000000000000000000000000000000000066741df0",
433+
"0000000000000000000000000000000000000000000000000000000066741d8c",
434+
"0000000000000000000000000000000000000000000000000000000000000064",
435+
"0000000000000000000000000000000000000000000000000000000000000002", // Market status: Open
436+
];
437+
438+
assert_eq!(
439+
report_blob,
440+
bytes(&format!("0x{}", expected_report_blob.join("")))
441+
);
442+
443+
let decoded_report = ReportDataV8::decode(&report_blob).unwrap();
444+
445+
assert_eq!(decoded_report.feed_id, V8_FEED_ID);
446+
}
447+
448+
#[test]
449+
fn test_decode_report_v9() {
450+
let report_data = generate_mock_report_data_v9();
451+
let encoded_report_data = report_data.abi_encode().unwrap();
452+
453+
let report = generate_mock_report(&encoded_report_data);
454+
455+
let (_report_context, report_blob) = decode_full_report(&report).unwrap();
456+
457+
let expected_report_blob = vec![
458+
"00096b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
459+
"0000000000000000000000000000000000000000000000000000000066741d8c",
460+
"0000000000000000000000000000000000000000000000000000000066741d8c",
461+
"000000000000000000000000000000000000000000000000000000000000000a",
462+
"000000000000000000000000000000000000000000000000000000000000000a",
463+
"0000000000000000000000000000000000000000000000000000000066741df0",
464+
"0000000000000000000000000000000000000000000000000000000000000001", // NAV per share
465+
"0000000000000000000000000000000000000000000000000000000066741d8c",
466+
"00000000000000000000000000000000000000000000000000000000000003e8", // AUM
467+
"0000000000000000000000000000000000000000000000000000000000000000", // Ripcord: Normal
468+
];
469+
470+
assert_eq!(
471+
report_blob,
472+
bytes(&format!("0x{}", expected_report_blob.join("")))
473+
);
474+
475+
let decoded_report = ReportDataV9::decode(&report_blob).unwrap();
476+
477+
assert_eq!(decoded_report.feed_id, V9_FEED_ID);
478+
}
370479
}

rust/crates/report/src/report/base.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,22 @@ impl ReportBase {
9797
buffer[24..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
9898
Ok(buffer)
9999
}
100+
101+
pub(crate) fn read_uint8(data: &[u8], offset: usize) -> Result<u8, ReportError> {
102+
if offset + Self::WORD_SIZE > data.len() {
103+
return Err(ReportError::DataTooShort("uint8"));
104+
}
105+
let value_bytes = &data[offset + 31..offset + 32];
106+
Ok(u8::from_be_bytes(
107+
value_bytes
108+
.try_into()
109+
.map_err(|_| ReportError::InvalidLength("uint8"))?,
110+
))
111+
}
112+
113+
pub(crate) fn encode_uint8(value: u8) -> Result<[u8; 32], ReportError> {
114+
let mut buffer = [0u8; 32];
115+
buffer[31] = value; // Place at the end of the 32 bytes word
116+
Ok(buffer)
117+
}
100118
}

rust/crates/report/src/report/v4.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ impl ReportDataV4 {
111111
mod tests {
112112
use super::*;
113113
use crate::report::tests::{
114-
generate_mock_report_data_v4, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP,
114+
generate_mock_report_data_v4, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, MARKET_STATUS_OPEN
115115
};
116116

117117
const V4_FEED_ID_STR: &str =
@@ -127,7 +127,7 @@ mod tests {
127127
let expected_timestamp: u32 = MOCK_TIMESTAMP;
128128
let expected_fee = BigInt::from(MOCK_FEE);
129129
let expected_price = BigInt::from(MOCK_PRICE);
130-
let expected_market_stats: u32 = 2; // Open
130+
let expected_market_status: u32 = MARKET_STATUS_OPEN;
131131

132132
assert_eq!(decoded.feed_id, expected_feed_id);
133133
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
@@ -136,6 +136,6 @@ mod tests {
136136
assert_eq!(decoded.link_fee, expected_fee);
137137
assert_eq!(decoded.expires_at, expected_timestamp + 100);
138138
assert_eq!(decoded.price, expected_price);
139-
assert_eq!(decoded.market_status, expected_market_stats);
139+
assert_eq!(decoded.market_status, expected_market_status);
140140
}
141141
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use crate::feed_id::ID;
2+
use crate::report::base::{ReportBase, ReportError};
3+
4+
use num_bigint::BigInt;
5+
6+
/// Represents a Report Data V8 Schema (Non-OTC RWA Data Streams).
7+
///
8+
/// # Parameters
9+
/// - `feed_id`: The feed ID the report has data for.
10+
/// - `valid_from_timestamp`: Earliest timestamp for which price is applicable.
11+
/// - `observations_timestamp`: Latest timestamp for which price is applicable.
12+
/// - `native_fee`: Base cost to validate a transaction using the report, denominated in the chain's native token (e.g., WETH/ETH).
13+
/// - `link_fee`: Base cost to validate a transaction using the report, denominated in LINK.
14+
/// - `expires_at`: Latest timestamp where the report can be verified onchain.
15+
/// - `last_update_timestamp`: Timestamp of the last valid price update.
16+
/// - `mid_price`: DON's consensus median price (18 decimal precision).
17+
/// - `market_status`: Market status - 0 (Unknown), 1 (Closed), 2 (Open).
18+
///
19+
/// # Solidity Equivalent
20+
/// ```solidity
21+
/// struct ReportDataV8 {
22+
/// bytes32 feedId;
23+
/// uint32 validFromTimestamp;
24+
/// uint32 observationsTimestamp;
25+
/// uint192 nativeFee;
26+
/// uint192 linkFee;
27+
/// uint32 expiresAt;
28+
/// uint64 lastUpdateTimestamp;
29+
/// int192 midPrice;
30+
/// uint8 marketStatus;
31+
/// }
32+
/// ```
33+
#[derive(Debug)]
34+
pub struct ReportDataV8 {
35+
pub feed_id: ID,
36+
pub valid_from_timestamp: u32,
37+
pub observations_timestamp: u32,
38+
pub native_fee: BigInt,
39+
pub link_fee: BigInt,
40+
pub expires_at: u32,
41+
pub last_update_timestamp: u64,
42+
pub mid_price: BigInt,
43+
pub market_status: u8,
44+
}
45+
46+
impl ReportDataV8 {
47+
/// Decodes an ABI-encoded `ReportDataV8` from bytes.
48+
///
49+
/// # Parameters
50+
///
51+
/// - `data`: The encoded report data.
52+
///
53+
/// # Returns
54+
///
55+
/// The decoded `ReportDataV8`.
56+
///
57+
/// # Errors
58+
///
59+
/// Returns a `ReportError` if the data is too short or if the data is invalid.
60+
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
61+
if data.len() < 9 * ReportBase::WORD_SIZE {
62+
return Err(ReportError::DataTooShort("ReportDataV8"));
63+
}
64+
65+
let feed_id = ID(data[..ReportBase::WORD_SIZE]
66+
.try_into()
67+
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);
68+
69+
let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
70+
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
71+
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
72+
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
73+
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
74+
let last_update_timestamp = ReportBase::read_uint64(data, 6 * ReportBase::WORD_SIZE)?;
75+
let mid_price = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
76+
let market_status = ReportBase::read_uint8(data, 8 * ReportBase::WORD_SIZE)?;
77+
78+
Ok(Self {
79+
feed_id,
80+
valid_from_timestamp,
81+
observations_timestamp,
82+
native_fee,
83+
link_fee,
84+
expires_at,
85+
last_update_timestamp,
86+
mid_price,
87+
market_status,
88+
})
89+
}
90+
91+
/// Encodes the `ReportDataV8` into an ABI-encoded byte array.
92+
///
93+
/// # Returns
94+
///
95+
/// The ABI-encoded report data.
96+
///
97+
/// # Errors
98+
///
99+
/// Returns a `ReportError` if the data is invalid.
100+
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
101+
let mut buffer = Vec::with_capacity(9 * ReportBase::WORD_SIZE);
102+
103+
buffer.extend_from_slice(&self.feed_id.0);
104+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
105+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
106+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
107+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
108+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
109+
buffer.extend_from_slice(&ReportBase::encode_uint64(self.last_update_timestamp)?);
110+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.mid_price)?);
111+
buffer.extend_from_slice(&ReportBase::encode_uint8(self.market_status)?);
112+
113+
Ok(buffer)
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
use crate::report::tests::{
121+
generate_mock_report_data_v8, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, MARKET_STATUS_OPEN
122+
};
123+
124+
const V8_FEED_ID_STR: &str =
125+
"0x00086b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";
126+
127+
#[test]
128+
fn test_decode_report_data_v8() {
129+
let report_data = generate_mock_report_data_v8();
130+
let encoded = report_data.abi_encode().unwrap();
131+
let decoded = ReportDataV8::decode(&encoded).unwrap();
132+
133+
let expected_feed_id = ID::from_hex_str(V8_FEED_ID_STR).unwrap();
134+
let expected_timestamp: u32 = MOCK_TIMESTAMP;
135+
let expected_fee = BigInt::from(MOCK_FEE);
136+
let expected_price = BigInt::from(MOCK_PRICE);
137+
let expected_market_status: u8 = MARKET_STATUS_OPEN as u8;
138+
139+
assert_eq!(decoded.feed_id, expected_feed_id);
140+
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
141+
assert_eq!(decoded.observations_timestamp, expected_timestamp);
142+
assert_eq!(decoded.native_fee, expected_fee);
143+
assert_eq!(decoded.link_fee, expected_fee);
144+
assert_eq!(decoded.expires_at, expected_timestamp + 100);
145+
assert_eq!(decoded.last_update_timestamp, expected_timestamp as u64);
146+
assert_eq!(decoded.mid_price, expected_price);
147+
assert_eq!(decoded.market_status, expected_market_status);
148+
}
149+
}

0 commit comments

Comments
 (0)