Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/crates/report/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chainlink-data-streams-report"
version = "1.0.3"
version = "1.0.4"
edition = "2021"
description = "Chainlink Data Streams Report"
license = "MIT"
Expand Down
60 changes: 59 additions & 1 deletion rust/crates/report/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod v7;
pub mod v8;
pub mod v9;
pub mod v10;
pub mod v12;

use base::{ReportBase, ReportError};

Expand Down Expand Up @@ -123,7 +124,7 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), Re
#[cfg(test)]
mod tests {
use super::*;
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5, v6::ReportDataV6, v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9, v10::ReportDataV10};
use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5, v6::ReportDataV6, v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9, v10::ReportDataV10, v12::ReportDataV12};
use num_bigint::BigInt;

const V1_FEED_ID: ID = ID([
Expand Down Expand Up @@ -166,6 +167,10 @@ mod tests {
00, 10, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
]);
const V12_FEED_ID: ID = ID([
00, 12, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
]);

pub const MOCK_TIMESTAMP: u32 = 1718885772;
pub const MOCK_FEE: usize = 10;
Expand Down Expand Up @@ -347,6 +352,27 @@ mod tests {
report_data
}

pub fn generate_mock_report_data_v12() -> ReportDataV12 {
const MOCK_NAV_PER_SHARE: isize = 1;
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
const RIPCORD_NORMAL: u32 = 0;

let report_data = ReportDataV12 {
feed_id: V12_FEED_ID,
valid_from_timestamp: MOCK_TIMESTAMP,
observations_timestamp: MOCK_TIMESTAMP,
native_fee: BigInt::from(MOCK_FEE),
link_fee: BigInt::from(MOCK_FEE),
expires_at: MOCK_TIMESTAMP + 100,
nav_per_share: BigInt::from(MOCK_NAV_PER_SHARE),
next_nav_per_share: BigInt::from(MOCK_NEXT_NAV_PER_SHARE),
nav_date: MOCK_TIMESTAMP as i64,
ripcord: RIPCORD_NORMAL,
};

report_data
}

fn generate_mock_report(encoded_report_data: &[u8]) -> Vec<u8> {
let mut payload = Vec::new();

Expand Down Expand Up @@ -696,4 +722,36 @@ mod tests {

assert_eq!(decoded_report.feed_id, V10_FEED_ID);
}

#[test]
fn test_decode_report_v12() {
let report_data = generate_mock_report_data_v12();
let encoded_report_data = report_data.abi_encode().unwrap();

let report = generate_mock_report(&encoded_report_data);

let (_report_context, report_blob) = decode_full_report(&report).unwrap();

let expected_report_blob = vec![
"000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"000000000000000000000000000000000000000000000000000000000000000a",
"000000000000000000000000000000000000000000000000000000000000000a",
"0000000000000000000000000000000000000000000000000000000066741df0",
"0000000000000000000000000000000000000000000000000000000000000001", // NAV per share
"0000000000000000000000000000000000000000000000000000000000000002", // Next NAV per share
"0000000000000000000000000000000000000000000000000000000066741d8c", // NAV date
"0000000000000000000000000000000000000000000000000000000000000000", // Ripcord: Normal
];

assert_eq!(
report_blob,
bytes(&format!("0x{}", expected_report_blob.join("")))
);

let decoded_report = ReportDataV12::decode(&report_blob).unwrap();

assert_eq!(decoded_report.feed_id, V12_FEED_ID);
}
}
49 changes: 43 additions & 6 deletions rust/crates/report/src/report/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ impl ReportBase {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("uint32"));
}
let value_bytes = &data[offset + 28..offset + 32];
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(u32::from_be_bytes(
value_bytes
value_bytes[28..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("uint32"))?,
))
Expand All @@ -75,17 +75,23 @@ impl ReportBase {
pub(crate) fn encode_uint32(value: u32) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
buffer[28..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
let len = bytes_value.len();

if len > 4 {
return Err(ReportError::InvalidLength("uint32"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}

pub(crate) fn read_uint64(data: &[u8], offset: usize) -> Result<u64, ReportError> {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("uint64"));
}
let value_bytes = &data[offset + 24..offset + 32];
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(u64::from_be_bytes(
value_bytes
value_bytes[24..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("uint64"))?,
))
Expand All @@ -94,7 +100,38 @@ impl ReportBase {
pub(crate) fn encode_uint64(value: u64) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
buffer[24..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
let len = bytes_value.len();

if len > 8 {
return Err(ReportError::InvalidLength("uint64"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}

pub(crate) fn read_int64(data: &[u8], offset: usize) -> Result<i64, ReportError> {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("int64"));
}
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(i64::from_be_bytes(
value_bytes[24..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("int64"))?,
))
}

pub(crate) fn encode_int64(value: i64) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
let len = bytes_value.len();

if len > 8 {
return Err(ReportError::InvalidLength("int64"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}
}
165 changes: 165 additions & 0 deletions rust/crates/report/src/report/v12.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use crate::feed_id::ID;
use crate::report::base::{ReportBase, ReportError};
use num_bigint::BigInt;

/// Represents a Report Data V12 Schema.
///
/// # Parameters
/// - `feed_id`: Unique identifier for the Data Streams feed.
/// - `valid_from_timestamp`: Earliest timestamp when the price is valid (seconds).
/// - `observations_timestamp`: Latest timestamp when the price is valid (seconds).
/// - `native_fee`: Verification cost in native blockchain tokens.
/// - `link_fee`: Verification cost in LINK tokens.
/// - `expires_at`: Expiration date of the report (seconds).
/// - `nav_per_share`: DON consensus NAV Per Share value as reported by the Fund Manager.
/// - `next_nav_per_share`: DON consensus next NAV Per Share value as reported by the Fund Manager.
/// - `nav_date`: Timestamp for the publication date of the NAV Report (nanoseconds).
/// - `ripcord`: Whether the provider paused NAV reporting.
///
/// # Ripcord Flag
/// - `0` (false): Feed's data provider is OK. Fund's data provider and accuracy is as expected.
/// - `1` (true): Feed's data provider is flagging a pause. Data provider detected outliers,
/// deviated thresholds, or operational issues. **DO NOT consume NAV data when ripcord=1.**
///
/// # Solidity Equivalent
/// ```solidity
/// struct ReportDataV12 {
/// bytes32 feedId;
/// uint32 validFromTimestamp;
/// uint32 observationsTimestamp;
/// uint192 nativeFee;
/// uint192 linkFee;
/// uint32 expiresAt;
/// int192 navPerShare;
/// int192 nextNavPerShare;
/// int64 navDate;
/// uint32 ripcord;
/// }
/// ```
#[derive(Debug)]
pub struct ReportDataV12 {
pub feed_id: ID,
pub valid_from_timestamp: u32,
pub observations_timestamp: u32,
pub native_fee: BigInt,
pub link_fee: BigInt,
pub expires_at: u32,
pub nav_per_share: BigInt,
pub next_nav_per_share: BigInt,
pub nav_date: i64,
pub ripcord: u32,
}

impl ReportDataV12 {
/// Decodes an ABI-encoded `ReportDataV12` from bytes.
///
/// # Parameters
///
/// - `data`: The encoded report data.
///
/// # Returns
///
/// The decoded `ReportDataV12`.
///
/// # Errors
///
/// Returns a `ReportError` if the data is too short or if the data is invalid.
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
if data.len() < 10 * ReportBase::WORD_SIZE {
return Err(ReportError::DataTooShort("ReportDataV12"));
}

let feed_id = ID(data[..ReportBase::WORD_SIZE]
.try_into()
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);

let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
let nav_per_share = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?;
let next_nav_per_share = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
let nav_date = ReportBase::read_int64(data, 8 * ReportBase::WORD_SIZE)?;
let ripcord = ReportBase::read_uint32(data, 9 * ReportBase::WORD_SIZE)?;

Ok(Self {
feed_id,
valid_from_timestamp,
observations_timestamp,
native_fee,
link_fee,
expires_at,
nav_per_share,
next_nav_per_share,
nav_date,
ripcord,
})
}

/// Encodes the `ReportDataV12` into an ABI-encoded byte array.
///
/// # Returns
///
/// The ABI-encoded report data.
///
/// # Errors
///
/// Returns a `ReportError` if the data is invalid.
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
let mut buffer = Vec::with_capacity(10 * ReportBase::WORD_SIZE);

buffer.extend_from_slice(&self.feed_id.0);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
buffer.extend_from_slice(&ReportBase::encode_int192(&self.nav_per_share)?);
buffer.extend_from_slice(&ReportBase::encode_int192(&self.next_nav_per_share)?);
buffer.extend_from_slice(&ReportBase::encode_int64(self.nav_date)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.ripcord)?);

Ok(buffer)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::report::tests::{
generate_mock_report_data_v12, MOCK_FEE, MOCK_TIMESTAMP,
};

const V12_FEED_ID_STR: &str =
"0x000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";

const MOCK_NAV_PER_SHARE: isize = 1;
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
const RIPCORD_NORMAL: u32 = 0;

#[test]
fn test_decode_report_data_v12() {
let report_data = generate_mock_report_data_v12();
let encoded = report_data.abi_encode().unwrap();
let decoded = ReportDataV12::decode(&encoded).unwrap();

let expected_feed_id = ID::from_hex_str(V12_FEED_ID_STR).unwrap();
let expected_timestamp: u32 = MOCK_TIMESTAMP;
let expected_fee = BigInt::from(MOCK_FEE);
let expected_nav_per_share = BigInt::from(MOCK_NAV_PER_SHARE);
let expected_next_nav_per_share = BigInt::from(MOCK_NEXT_NAV_PER_SHARE);
let expected_ripcord = RIPCORD_NORMAL;

assert_eq!(decoded.feed_id, expected_feed_id);
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
assert_eq!(decoded.observations_timestamp, expected_timestamp);
assert_eq!(decoded.native_fee, expected_fee);
assert_eq!(decoded.link_fee, expected_fee);
assert_eq!(decoded.expires_at, expected_timestamp + 100);
assert_eq!(decoded.nav_per_share, expected_nav_per_share);
assert_eq!(decoded.next_nav_per_share, expected_next_nav_per_share);
assert_eq!(decoded.nav_date, expected_timestamp as i64);
assert_eq!(decoded.ripcord, expected_ripcord);
}
}
4 changes: 2 additions & 2 deletions rust/crates/sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chainlink-data-streams-sdk"
version = "1.0.3"
version = "1.0.4"
edition = "2021"
rust-version = "1.70"
description = "Chainlink Data Streams client SDK"
Expand All @@ -11,7 +11,7 @@ exclude = ["/target/*", "examples/*", "tests/*", "docs/*", "book/*"]
keywords = ["chainlink"]

[dependencies]
chainlink-data-streams-report = { path = "../report", version = "1.0.3" }
chainlink-data-streams-report = { path = "../report", version = "1.0.4" }
reqwest = { version = "0.11.20", features = ["json", "rustls-tls"] }
tokio = { version = "1.29.1", features = ["full"] }
tokio-tungstenite = { version = "0.20.1", features = [
Expand Down