diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8affa18..a9cccfd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -127,7 +127,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainlink-data-streams-report" -version = "1.0.2" +version = "1.0.3" dependencies = [ "hex", "num-bigint", @@ -139,7 +139,7 @@ dependencies = [ [[package]] name = "chainlink-data-streams-sdk" -version = "1.0.2" +version = "1.0.3" dependencies = [ "byteorder", "chainlink-data-streams-report", diff --git a/rust/README.md b/rust/README.md index 76c927c..8afa176 100644 --- a/rust/README.md +++ b/rust/README.md @@ -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 diff --git a/rust/crates/report/Cargo.toml b/rust/crates/report/Cargo.toml index f7bc70e..df9046d 100644 --- a/rust/crates/report/Cargo.toml +++ b/rust/crates/report/Cargo.toml @@ -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" diff --git a/rust/crates/report/src/report.rs b/rust/crates/report/src/report.rs index 29dbf0e..0ffb49a 100644 --- a/rust/crates/report/src/report.rs +++ b/rust/crates/report/src/report.rs @@ -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; @@ -120,7 +123,7 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec), 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([ @@ -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, @@ -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, @@ -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(); diff --git a/rust/crates/report/src/report/v5.rs b/rust/crates/report/src/report/v5.rs new file mode 100644 index 0000000..fed306b --- /dev/null +++ b/rust/crates/report/src/report/v5.rs @@ -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 { + 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, 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); + } +} diff --git a/rust/crates/report/src/report/v6.rs b/rust/crates/report/src/report/v6.rs new file mode 100644 index 0000000..f217eb9 --- /dev/null +++ b/rust/crates/report/src/report/v6.rs @@ -0,0 +1,162 @@ +use crate::feed_id::ID; +use crate::report::base::{ReportBase, ReportError}; + +use num_bigint::BigInt; + +/// Represents a Report Data V6 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. +/// - `price`: The primary price value. +/// - `price2`: The second price value. +/// - `price3`: The third price value. +/// - `price4`: The fourth price value. +/// - `price5`: The fifth price value. +/// +/// # Solidity Equivalent +/// ```solidity +/// struct ReportDataV6 { +/// bytes32 feedId; +/// uint32 validFromTimestamp; +/// uint32 observationsTimestamp; +/// uint192 nativeFee; +/// uint192 linkFee; +/// uint32 expiresAt; +/// int192 price; +/// int192 price2; +/// int192 price3; +/// int192 price4; +/// int192 price5; +/// } +/// ``` +#[derive(Debug)] +pub struct ReportDataV6 { + 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 price: BigInt, + pub price2: BigInt, + pub price3: BigInt, + pub price4: BigInt, + pub price5: BigInt, +} + +impl ReportDataV6 { + /// Decodes an ABI-encoded `ReportDataV6` from bytes. + /// + /// # Parameters + /// + /// - `data`: The encoded report data. + /// + /// # Returns + /// + /// The decoded `ReportDataV6`. + /// + /// # Errors + /// + /// Returns a `ReportError` if the data is too short or if the data is invalid. + pub fn decode(data: &[u8]) -> Result { + if data.len() < 11 * ReportBase::WORD_SIZE { + return Err(ReportError::DataTooShort("ReportDataV6")); + } + + 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 price = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?; + let price2 = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?; + let price3 = ReportBase::read_int192(data, 8 * ReportBase::WORD_SIZE)?; + let price4 = ReportBase::read_int192(data, 9 * ReportBase::WORD_SIZE)?; + let price5 = ReportBase::read_int192(data, 10 * ReportBase::WORD_SIZE)?; + + Ok(Self { + feed_id, + valid_from_timestamp, + observations_timestamp, + native_fee, + link_fee, + expires_at, + price, + price2, + price3, + price4, + price5, + }) + } + + /// Encodes the `ReportDataV6` 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, ReportError> { + let mut buffer = Vec::with_capacity(11 * 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.price)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.price2)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.price3)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.price4)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.price5)?); + + Ok(buffer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::report::tests::{ + generate_mock_report_data_v6, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, + }; + + const V6_FEED_ID_STR: &str = + "0x00066b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472"; + + #[test] + fn test_decode_report_data_v6() { + let report_data = generate_mock_report_data_v6(); + let encoded = report_data.abi_encode().unwrap(); + let decoded = ReportDataV6::decode(&encoded).unwrap(); + + let expected_feed_id = ID::from_hex_str(V6_FEED_ID_STR).unwrap(); + let expected_timestamp: u32 = MOCK_TIMESTAMP; + let expected_fee = BigInt::from(MOCK_FEE); + let expected_price = BigInt::from(MOCK_PRICE); + + 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.price, expected_price); + assert_eq!(decoded.price2, BigInt::from(MOCK_PRICE + 10)); + assert_eq!(decoded.price3, BigInt::from(MOCK_PRICE + 20)); + assert_eq!(decoded.price4, BigInt::from(MOCK_PRICE + 30)); + assert_eq!(decoded.price5, BigInt::from(MOCK_PRICE + 40)); + } +} diff --git a/rust/crates/report/src/report/v7.rs b/rust/crates/report/src/report/v7.rs new file mode 100644 index 0000000..b20d335 --- /dev/null +++ b/rust/crates/report/src/report/v7.rs @@ -0,0 +1,134 @@ +use crate::feed_id::ID; +use crate::report::base::{ReportBase, ReportError}; + +use num_bigint::BigInt; + +/// Represents a Report Data V7 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. +/// - `exchange_rate`: The exchange rate. +/// +/// # Solidity Equivalent +/// ```solidity +/// struct ReportDataV7 { +/// bytes32 feedId; +/// uint32 validFromTimestamp; +/// uint32 observationsTimestamp; +/// uint192 nativeFee; +/// uint192 linkFee; +/// uint32 expiresAt; +/// int192 exchangeRate; +/// } +/// ``` +#[derive(Debug)] +pub struct ReportDataV7 { + 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 exchange_rate: BigInt, +} + +impl ReportDataV7 { + /// Decodes an ABI-encoded `ReportDataV7` from bytes. + /// + /// # Parameters + /// + /// - `data`: The encoded report data. + /// + /// # Returns + /// + /// The decoded `ReportDataV7`. + /// + /// # Errors + /// + /// Returns a `ReportError` if the data is too short or if the data is invalid. + pub fn decode(data: &[u8]) -> Result { + if data.len() < 7 * ReportBase::WORD_SIZE { + return Err(ReportError::DataTooShort("ReportDataV7")); + } + + 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 exchange_rate = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?; + + Ok(Self { + feed_id, + valid_from_timestamp, + observations_timestamp, + native_fee, + link_fee, + expires_at, + exchange_rate, + }) + } + + /// Encodes the `ReportDataV7` 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, ReportError> { + let mut buffer = Vec::with_capacity(7 * 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.exchange_rate)?); + + Ok(buffer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::report::tests::{ + generate_mock_report_data_v7, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, + }; + + const V7_FEED_ID_STR: &str = + "0x00076b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472"; + + #[test] + fn test_decode_report_data_v7() { + let report_data = generate_mock_report_data_v7(); + let encoded = report_data.abi_encode().unwrap(); + let decoded = ReportDataV7::decode(&encoded).unwrap(); + + let expected_feed_id = ID::from_hex_str(V7_FEED_ID_STR).unwrap(); + let expected_timestamp: u32 = MOCK_TIMESTAMP; + let expected_fee = BigInt::from(MOCK_FEE); + let expected_exchange_rate = BigInt::from(MOCK_PRICE); + + 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.exchange_rate, expected_exchange_rate); + } +} diff --git a/rust/crates/sdk/Cargo.toml b/rust/crates/sdk/Cargo.toml index 06a661a..b320254 100644 --- a/rust/crates/sdk/Cargo.toml +++ b/rust/crates/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainlink-data-streams-sdk" -version = "1.0.2" +version = "1.0.3" edition = "2021" rust-version = "1.70" description = "Chainlink Data Streams client SDK" @@ -11,7 +11,7 @@ exclude = ["/target/*", "examples/*", "tests/*", "docs/*", "book/*"] keywords = ["chainlink"] [dependencies] -chainlink-data-streams-report = { path = "../report", version = "1.0.2" } +chainlink-data-streams-report = { path = "../report", version = "1.0.3" } reqwest = { version = "0.11.20", features = ["json", "rustls-tls"] } tokio = { version = "1.29.1", features = ["full"] } tokio-tungstenite = { version = "0.20.1", features = [