Skip to content
Merged
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.

4 changes: 2 additions & 2 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Add the following to your `Cargo.toml`:

```toml
[dependencies]
chainlink-data-streams-report = "1.0.0"
chainlink-data-streams-sdk = { version = "1.0.0", features = ["full"] }
chainlink-data-streams-report = "1.0.3"
chainlink-data-streams-sdk = { version = "1.0.3", features = ["full"] }
```

#### Features
Expand Down
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.2"
version = "1.0.3"
edition = "2021"
description = "Chainlink Data Streams Report"
license = "MIT"
Expand Down
160 changes: 159 additions & 1 deletion rust/crates/report/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pub mod v1;
pub mod v2;
pub mod v3;
pub mod v4;
pub mod v5;
pub mod v6;
pub mod v7;
pub mod v8;
pub mod v9;
pub mod v10;
Expand Down Expand Up @@ -120,7 +123,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, 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};
use num_bigint::BigInt;

const V1_FEED_ID: ID = ID([
Expand All @@ -139,6 +142,18 @@ mod tests {
00, 04, 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 V5_FEED_ID: ID = ID([
00, 05, 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 V6_FEED_ID: ID = ID([
00, 06, 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 V7_FEED_ID: ID = ID([
00, 07, 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 V8_FEED_ID: ID = ID([
00, 08, 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,
Expand Down Expand Up @@ -223,6 +238,56 @@ mod tests {
report_data
}

pub fn generate_mock_report_data_v5() -> ReportDataV5 {
let one_hour_in_seconds: u32 = 3600;

let report_data = ReportDataV5 {
feed_id: V5_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,
rate: BigInt::from(MOCK_PRICE),
timestamp: MOCK_TIMESTAMP,
duration: one_hour_in_seconds,
};

report_data
}

pub fn generate_mock_report_data_v6() -> ReportDataV6 {
let report_data = ReportDataV6 {
feed_id: V6_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,
price: BigInt::from(MOCK_PRICE),
price2: BigInt::from(MOCK_PRICE + 10),
price3: BigInt::from(MOCK_PRICE + 20),
price4: BigInt::from(MOCK_PRICE + 30),
price5: BigInt::from(MOCK_PRICE + 40),
};

report_data
}

pub fn generate_mock_report_data_v7() -> ReportDataV7 {
let report_data = ReportDataV7 {
feed_id: V7_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,
exchange_rate: BigInt::from(MOCK_PRICE),
};

report_data
}

pub fn generate_mock_report_data_v8() -> ReportDataV8 {
let report_data = ReportDataV8 {
feed_id: V8_FEED_ID,
Expand Down Expand Up @@ -441,6 +506,99 @@ mod tests {
assert_eq!(decoded_report.feed_id, V4_FEED_ID);
}

#[test]
fn test_decode_report_v5() {
let report_data = generate_mock_report_data_v5();
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![
"00056b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"000000000000000000000000000000000000000000000000000000000000000a",
"000000000000000000000000000000000000000000000000000000000000000a",
"0000000000000000000000000000000000000000000000000000000066741df0",
"0000000000000000000000000000000000000000000000000000000000000064", // Rate: 100
"0000000000000000000000000000000000000000000000000000000066741d8c", // Timestamp
"0000000000000000000000000000000000000000000000000000000000000e10", // Duration: 3600
];

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

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

assert_eq!(decoded_report.feed_id, V5_FEED_ID);
}

#[test]
fn test_decode_report_v6() {
let report_data = generate_mock_report_data_v6();
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![
"00066b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"000000000000000000000000000000000000000000000000000000000000000a",
"000000000000000000000000000000000000000000000000000000000000000a",
"0000000000000000000000000000000000000000000000000000000066741df0",
"0000000000000000000000000000000000000000000000000000000000000064", // Price: 100
"000000000000000000000000000000000000000000000000000000000000006e", // Price2: 110
"0000000000000000000000000000000000000000000000000000000000000078", // Price3: 120
"0000000000000000000000000000000000000000000000000000000000000082", // Price4: 130
"000000000000000000000000000000000000000000000000000000000000008c", // Price5: 140
];

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

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

assert_eq!(decoded_report.feed_id, V6_FEED_ID);
}

#[test]
fn test_decode_report_v7() {
let report_data = generate_mock_report_data_v7();
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![
"00076b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"000000000000000000000000000000000000000000000000000000000000000a",
"000000000000000000000000000000000000000000000000000000000000000a",
"0000000000000000000000000000000000000000000000000000000066741df0",
"0000000000000000000000000000000000000000000000000000000000000064", // Exchange Rate: 100
];

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

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

assert_eq!(decoded_report.feed_id, V7_FEED_ID);
}

#[test]
fn test_decode_report_v8() {
let report_data = generate_mock_report_data_v8();
Expand Down
151 changes: 151 additions & 0 deletions rust/crates/report/src/report/v5.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use crate::feed_id::ID;
use crate::report::base::{ReportBase, ReportError};

use num_bigint::BigInt;

/// Represents a Report Data V5 Schema.
///
/// # Parameters
/// - `feed_id`: The feed ID the report has data for.
/// - `valid_from_timestamp`: Earliest timestamp for which price is applicable.
/// - `observations_timestamp`: Latest timestamp for which price is applicable.
/// - `native_fee`: Base cost to validate a transaction using the report, denominated in the chain's native token (e.g., WETH/ETH).
/// - `link_fee`: Base cost to validate a transaction using the report, denominated in LINK.
/// - `expires_at`: Latest timestamp where the report can be verified onchain.
/// - `rate`: The interest rate.
/// - `timestamp`: Timestamp when the rate was observed.
/// - `duration`: Duration for which the rate is applicable.
///
/// # Solidity Equivalent
/// ```solidity
/// struct ReportDataV5 {
/// bytes32 feedId;
/// uint32 validFromTimestamp;
/// uint32 observationsTimestamp;
/// uint192 nativeFee;
/// uint192 linkFee;
/// uint32 expiresAt;
/// int192 rate;
/// uint32 timestamp;
/// uint32 duration;
/// }
/// ```
#[derive(Debug)]
pub struct ReportDataV5 {
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 rate: BigInt,
pub timestamp: u32,
pub duration: u32,
}

impl ReportDataV5 {
/// Decodes an ABI-encoded `ReportDataV5` from bytes.
///
/// # Parameters
///
/// - `data`: The encoded report data.
///
/// # Returns
///
/// The decoded `ReportDataV5`.
///
/// # 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() < 9 * ReportBase::WORD_SIZE {
return Err(ReportError::DataTooShort("ReportDataV5"));
}

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 rate = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?;
let timestamp = ReportBase::read_uint32(data, 7 * ReportBase::WORD_SIZE)?;
let duration = ReportBase::read_uint32(data, 8 * ReportBase::WORD_SIZE)?;

Ok(Self {
feed_id,
valid_from_timestamp,
observations_timestamp,
native_fee,
link_fee,
expires_at,
rate,
timestamp,
duration,
})
}

/// Encodes the `ReportDataV5` 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(9 * 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.rate)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.timestamp)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.duration)?);

Ok(buffer)
}
}

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

const V5_FEED_ID_STR: &str =
"0x00056b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";

#[test]
fn test_decode_report_data_v5() {
let report_data = generate_mock_report_data_v5();
let encoded = report_data.abi_encode().unwrap();
let decoded = ReportDataV5::decode(&encoded).unwrap();

let one_hour_in_seconds: u32 = 3600;

let expected_feed_id = ID::from_hex_str(V5_FEED_ID_STR).unwrap();
let expected_timestamp: u32 = MOCK_TIMESTAMP;
let expected_fee = BigInt::from(MOCK_FEE);
let expected_rate = BigInt::from(MOCK_PRICE);
let expected_duration = one_hour_in_seconds;

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.rate, expected_rate);
assert_eq!(decoded.timestamp, expected_timestamp);
assert_eq!(decoded.duration, expected_duration);
}
}
Loading