Skip to content

Commit 5d545e9

Browse files
authored
Rust: Add ReportDataV10 schema (#23)
This PR adds Report Data Version 10 Schema to the Rust `chainlink_data_streams_report` crate
1 parent 7530bbf commit 5d545e9

File tree

5 files changed

+251
-6
lines changed

5 files changed

+251
-6
lines changed

rust/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/crates/report/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "chainlink-data-streams-report"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
edition = "2021"
55
description = "Chainlink Data Streams Report"
66
license = "MIT"

rust/crates/report/src/report.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod v3;
66
pub mod v4;
77
pub mod v8;
88
pub mod v9;
9+
pub mod v10;
910

1011
use base::{ReportBase, ReportError};
1112

@@ -119,7 +120,7 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), Re
119120
#[cfg(test)]
120121
mod tests {
121122
use super::*;
122-
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v8::ReportDataV8, v9::ReportDataV9};
123+
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v8::ReportDataV8, v9::ReportDataV9, v10::ReportDataV10};
123124
use num_bigint::BigInt;
124125

125126
const V1_FEED_ID: ID = ID([
@@ -146,6 +147,10 @@ mod tests {
146147
00, 09, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
147148
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
148149
]);
150+
const V10_FEED_ID: ID = ID([
151+
00, 10, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
152+
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
153+
]);
149154

150155
pub const MOCK_TIMESTAMP: u32 = 1718885772;
151156
pub const MOCK_FEE: usize = 10;
@@ -255,6 +260,28 @@ mod tests {
255260
report_data
256261
}
257262

263+
pub fn generate_mock_report_data_v10() -> ReportDataV10 {
264+
const MOCK_MULTIPLIER: isize = 1000000000000000000; // 1.0 with 18 decimals
265+
266+
let report_data = ReportDataV10 {
267+
feed_id: V10_FEED_ID,
268+
valid_from_timestamp: MOCK_TIMESTAMP,
269+
observations_timestamp: MOCK_TIMESTAMP,
270+
native_fee: BigInt::from(MOCK_FEE),
271+
link_fee: BigInt::from(MOCK_FEE),
272+
expires_at: MOCK_TIMESTAMP + 100,
273+
last_update_timestamp: MOCK_TIMESTAMP as u64,
274+
price: BigInt::from(MOCK_PRICE),
275+
market_status: MARKET_STATUS_OPEN,
276+
current_multiplier: BigInt::from(MOCK_MULTIPLIER),
277+
new_multiplier: BigInt::from(MOCK_MULTIPLIER),
278+
activation_date_time: MOCK_TIMESTAMP + 200,
279+
tokenized_price: BigInt::from(MOCK_PRICE * 2),
280+
};
281+
282+
report_data
283+
}
284+
258285
fn generate_mock_report(encoded_report_data: &[u8]) -> Vec<u8> {
259286
let mut payload = Vec::new();
260287

@@ -476,4 +503,39 @@ mod tests {
476503

477504
assert_eq!(decoded_report.feed_id, V9_FEED_ID);
478505
}
506+
507+
#[test]
508+
fn test_decode_report_v10() {
509+
let report_data = generate_mock_report_data_v10();
510+
let encoded_report_data = report_data.abi_encode().unwrap();
511+
512+
let report = generate_mock_report(&encoded_report_data);
513+
514+
let (_report_context, report_blob) = decode_full_report(&report).unwrap();
515+
516+
let expected_report_blob = vec![
517+
"000a6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
518+
"0000000000000000000000000000000000000000000000000000000066741d8c",
519+
"0000000000000000000000000000000000000000000000000000000066741d8c",
520+
"000000000000000000000000000000000000000000000000000000000000000a",
521+
"000000000000000000000000000000000000000000000000000000000000000a",
522+
"0000000000000000000000000000000000000000000000000000000066741df0",
523+
"0000000000000000000000000000000000000000000000000000000066741d8c",
524+
"0000000000000000000000000000000000000000000000000000000000000064",
525+
"0000000000000000000000000000000000000000000000000000000000000002", // Market status: Open
526+
"0000000000000000000000000000000000000000000000000de0b6b3a7640000", // Current multiplier: 1.0 with 18 decimals
527+
"0000000000000000000000000000000000000000000000000de0b6b3a7640000", // New multiplier: 1.0 with 18 decimals
528+
"0000000000000000000000000000000000000000000000000000000066741e54", // Activation date time
529+
"00000000000000000000000000000000000000000000000000000000000000c8", // Tokenized price: 200
530+
];
531+
532+
let expected = bytes(&format!("0x{}", expected_report_blob.join("")));
533+
println!("Actual : {}", hex::encode(&report_blob));
534+
println!("Expected: {}", hex::encode(&expected));
535+
assert_eq!(report_blob, expected);
536+
537+
let decoded_report = ReportDataV10::decode(&report_blob).unwrap();
538+
539+
assert_eq!(decoded_report.feed_id, V10_FEED_ID);
540+
}
479541
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 V10 Schema.
7+
///
8+
/// This schema extends the V8 schema with additional fields for multipliers and tokenized pricing.
9+
///
10+
/// # Parameters
11+
/// - `feed_id`: The feed ID the report has data for.
12+
/// - `valid_from_timestamp`: Earliest timestamp for which price is applicable.
13+
/// - `observations_timestamp`: Latest timestamp for which price is applicable.
14+
/// - `native_fee`: Base cost to validate a transaction using the report, denominated in the chain's native token (e.g., WETH/ETH).
15+
/// - `link_fee`: Base cost to validate a transaction using the report, denominated in LINK.
16+
/// - `expires_at`: Latest timestamp where the report can be verified onchain.
17+
/// - `last_update_timestamp`: Timestamp of the last valid price update.
18+
/// - `price`: DON's consensus price (18 decimal precision).
19+
/// - `market_status`: Market status - 0 (Unknown), 1 (Closed), 2 (Open).
20+
/// - `current_multiplier`: Currently applied multiplier accounting for past corporate actions.
21+
/// - `new_multiplier`: Multiplier to be applied at the `activation_date_time` (set to 0 if none is scheduled).
22+
/// - `activation_date_time`: When the next corporate action takes effect (set to 0 if none is scheduled).
23+
/// - `tokenized_price`: 24/7 tokenized equity price.
24+
///
25+
/// # Solidity Equivalent
26+
/// ```solidity
27+
/// struct ReportDataV10 {
28+
/// bytes32 feedId;
29+
/// uint32 validFromTimestamp;
30+
/// uint32 observationsTimestamp;
31+
/// uint192 nativeFee;
32+
/// uint192 linkFee;
33+
/// uint32 expiresAt;
34+
/// uint64 lastUpdateTimestamp;
35+
/// int192 price;
36+
/// uint32 marketStatus;
37+
/// int192 currentMultiplier;
38+
/// int192 newMultiplier;
39+
/// uint32 activationDateTime;
40+
/// int192 tokenizedPrice;
41+
/// }
42+
/// ```
43+
#[derive(Debug)]
44+
pub struct ReportDataV10 {
45+
pub feed_id: ID,
46+
pub valid_from_timestamp: u32,
47+
pub observations_timestamp: u32,
48+
pub native_fee: BigInt,
49+
pub link_fee: BigInt,
50+
pub expires_at: u32,
51+
pub last_update_timestamp: u64,
52+
pub price: BigInt,
53+
pub market_status: u32,
54+
pub current_multiplier: BigInt,
55+
pub new_multiplier: BigInt,
56+
pub activation_date_time: u32,
57+
pub tokenized_price: BigInt,
58+
}
59+
60+
impl ReportDataV10 {
61+
/// Decodes an ABI-encoded `ReportDataV10` from bytes.
62+
///
63+
/// # Parameters
64+
///
65+
/// - `data`: The encoded report data.
66+
///
67+
/// # Returns
68+
///
69+
/// The decoded `ReportDataV10`.
70+
///
71+
/// # Errors
72+
///
73+
/// Returns a `ReportError` if the data is too short or if the data is invalid.
74+
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
75+
if data.len() < 13 * ReportBase::WORD_SIZE {
76+
return Err(ReportError::DataTooShort("ReportDataV10"));
77+
}
78+
79+
let feed_id = ID(data[..ReportBase::WORD_SIZE]
80+
.try_into()
81+
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);
82+
83+
let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
84+
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
85+
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
86+
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
87+
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
88+
let last_update_timestamp = ReportBase::read_uint64(data, 6 * ReportBase::WORD_SIZE)?;
89+
let price = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
90+
let market_status = ReportBase::read_uint32(data, 8 * ReportBase::WORD_SIZE)?;
91+
let current_multiplier = ReportBase::read_int192(data, 9 * ReportBase::WORD_SIZE)?;
92+
let new_multiplier = ReportBase::read_int192(data, 10 * ReportBase::WORD_SIZE)?;
93+
let activation_date_time = ReportBase::read_uint32(data, 11 * ReportBase::WORD_SIZE)?;
94+
let tokenized_price = ReportBase::read_int192(data, 12 * ReportBase::WORD_SIZE)?;
95+
96+
Ok(Self {
97+
feed_id,
98+
valid_from_timestamp,
99+
observations_timestamp,
100+
native_fee,
101+
link_fee,
102+
expires_at,
103+
last_update_timestamp,
104+
price,
105+
market_status,
106+
current_multiplier,
107+
new_multiplier,
108+
activation_date_time,
109+
tokenized_price,
110+
})
111+
}
112+
113+
/// Encodes the `ReportDataV10` into an ABI-encoded byte array.
114+
///
115+
/// # Returns
116+
///
117+
/// The ABI-encoded report data.
118+
///
119+
/// # Errors
120+
///
121+
/// Returns a `ReportError` if the data is invalid.
122+
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
123+
let mut buffer = Vec::with_capacity(13 * ReportBase::WORD_SIZE);
124+
125+
buffer.extend_from_slice(&self.feed_id.0);
126+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
127+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
128+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
129+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
130+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
131+
buffer.extend_from_slice(&ReportBase::encode_uint64(self.last_update_timestamp)?);
132+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.price)?);
133+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.market_status)?);
134+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.current_multiplier)?);
135+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.new_multiplier)?);
136+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.activation_date_time)?);
137+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.tokenized_price)?);
138+
139+
Ok(buffer)
140+
}
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::*;
146+
use crate::report::tests::{
147+
generate_mock_report_data_v10, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, MARKET_STATUS_OPEN
148+
};
149+
150+
const V10_FEED_ID_STR: &str =
151+
"0x000a6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";
152+
153+
#[test]
154+
fn test_decode_report_data_v10() {
155+
let report_data = generate_mock_report_data_v10();
156+
let encoded = report_data.abi_encode().unwrap();
157+
let decoded = ReportDataV10::decode(&encoded).unwrap();
158+
159+
const MOCK_MULTIPLIER: isize = 1000000000000000000;
160+
161+
let expected_feed_id = ID::from_hex_str(V10_FEED_ID_STR).unwrap();
162+
let expected_timestamp: u32 = MOCK_TIMESTAMP;
163+
let expected_fee = BigInt::from(MOCK_FEE);
164+
let expected_price = BigInt::from(MOCK_PRICE);
165+
let expected_market_status: u32 = MARKET_STATUS_OPEN;
166+
let expected_multiplier = BigInt::from(MOCK_MULTIPLIER); // 1.0 with 18 decimals
167+
let expected_tokenized_price = BigInt::from(MOCK_PRICE * 2); // Example tokenized price
168+
169+
assert_eq!(decoded.feed_id, expected_feed_id);
170+
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
171+
assert_eq!(decoded.observations_timestamp, expected_timestamp);
172+
assert_eq!(decoded.native_fee, expected_fee);
173+
assert_eq!(decoded.link_fee, expected_fee);
174+
assert_eq!(decoded.expires_at, expected_timestamp + 100);
175+
assert_eq!(decoded.last_update_timestamp, expected_timestamp as u64);
176+
assert_eq!(decoded.price, expected_price);
177+
assert_eq!(decoded.market_status, expected_market_status);
178+
assert_eq!(decoded.current_multiplier, expected_multiplier);
179+
assert_eq!(decoded.new_multiplier, expected_multiplier);
180+
assert_eq!(decoded.activation_date_time, expected_timestamp + 200);
181+
assert_eq!(decoded.tokenized_price, expected_tokenized_price);
182+
}
183+
}

rust/crates/sdk/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "chainlink-data-streams-sdk"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
edition = "2021"
55
rust-version = "1.70"
66
description = "Chainlink Data Streams client SDK"
@@ -11,7 +11,7 @@ exclude = ["/target/*", "examples/*", "tests/*", "docs/*", "book/*"]
1111
keywords = ["chainlink"]
1212

1313
[dependencies]
14-
chainlink-data-streams-report = { path = "../report", version = "1.0.1" }
14+
chainlink-data-streams-report = { path = "../report", version = "1.0.2" }
1515
reqwest = { version = "0.11.20", features = ["json", "rustls-tls"] }
1616
tokio = { version = "1.29.1", features = ["full"] }
1717
tokio-tungstenite = { version = "0.20.1", features = [

0 commit comments

Comments
 (0)