diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 41961cf..8affa18 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -127,7 +127,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainlink-data-streams-report" -version = "1.0.1" +version = "1.0.2" dependencies = [ "hex", "num-bigint", @@ -139,7 +139,7 @@ dependencies = [ [[package]] name = "chainlink-data-streams-sdk" -version = "1.0.1" +version = "1.0.2" dependencies = [ "byteorder", "chainlink-data-streams-report", diff --git a/rust/crates/report/Cargo.toml b/rust/crates/report/Cargo.toml index aa72449..f7bc70e 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.1" +version = "1.0.2" 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 6f48226..29dbf0e 100644 --- a/rust/crates/report/src/report.rs +++ b/rust/crates/report/src/report.rs @@ -6,6 +6,7 @@ pub mod v3; pub mod v4; pub mod v8; pub mod v9; +pub mod v10; use base::{ReportBase, ReportError}; @@ -119,7 +120,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}; + use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v8::ReportDataV8, v9::ReportDataV9, v10::ReportDataV10}; use num_bigint::BigInt; const V1_FEED_ID: ID = ID([ @@ -146,6 +147,10 @@ mod tests { 00, 09, 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 V10_FEED_ID: ID = ID([ + 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, + ]); pub const MOCK_TIMESTAMP: u32 = 1718885772; pub const MOCK_FEE: usize = 10; @@ -255,6 +260,28 @@ mod tests { report_data } + pub fn generate_mock_report_data_v10() -> ReportDataV10 { + const MOCK_MULTIPLIER: isize = 1000000000000000000; // 1.0 with 18 decimals + + let report_data = ReportDataV10 { + feed_id: V10_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, + last_update_timestamp: MOCK_TIMESTAMP as u64, + price: BigInt::from(MOCK_PRICE), + market_status: MARKET_STATUS_OPEN, + current_multiplier: BigInt::from(MOCK_MULTIPLIER), + new_multiplier: BigInt::from(MOCK_MULTIPLIER), + activation_date_time: MOCK_TIMESTAMP + 200, + tokenized_price: BigInt::from(MOCK_PRICE * 2), + }; + + report_data + } + fn generate_mock_report(encoded_report_data: &[u8]) -> Vec { let mut payload = Vec::new(); @@ -476,4 +503,39 @@ mod tests { assert_eq!(decoded_report.feed_id, V9_FEED_ID); } + + #[test] + fn test_decode_report_v10() { + let report_data = generate_mock_report_data_v10(); + 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![ + "000a6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472", + "0000000000000000000000000000000000000000000000000000000066741d8c", + "0000000000000000000000000000000000000000000000000000000066741d8c", + "000000000000000000000000000000000000000000000000000000000000000a", + "000000000000000000000000000000000000000000000000000000000000000a", + "0000000000000000000000000000000000000000000000000000000066741df0", + "0000000000000000000000000000000000000000000000000000000066741d8c", + "0000000000000000000000000000000000000000000000000000000000000064", + "0000000000000000000000000000000000000000000000000000000000000002", // Market status: Open + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // Current multiplier: 1.0 with 18 decimals + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // New multiplier: 1.0 with 18 decimals + "0000000000000000000000000000000000000000000000000000000066741e54", // Activation date time + "00000000000000000000000000000000000000000000000000000000000000c8", // Tokenized price: 200 + ]; + + let expected = bytes(&format!("0x{}", expected_report_blob.join(""))); + println!("Actual : {}", hex::encode(&report_blob)); + println!("Expected: {}", hex::encode(&expected)); + assert_eq!(report_blob, expected); + + let decoded_report = ReportDataV10::decode(&report_blob).unwrap(); + + assert_eq!(decoded_report.feed_id, V10_FEED_ID); + } } diff --git a/rust/crates/report/src/report/v10.rs b/rust/crates/report/src/report/v10.rs new file mode 100644 index 0000000..8bbce30 --- /dev/null +++ b/rust/crates/report/src/report/v10.rs @@ -0,0 +1,183 @@ +use crate::feed_id::ID; +use crate::report::base::{ReportBase, ReportError}; + +use num_bigint::BigInt; + +/// Represents a Report Data V10 Schema. +/// +/// This schema extends the V8 schema with additional fields for multipliers and tokenized pricing. +/// +/// # 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. +/// - `last_update_timestamp`: Timestamp of the last valid price update. +/// - `price`: DON's consensus price (18 decimal precision). +/// - `market_status`: Market status - 0 (Unknown), 1 (Closed), 2 (Open). +/// - `current_multiplier`: Currently applied multiplier accounting for past corporate actions. +/// - `new_multiplier`: Multiplier to be applied at the `activation_date_time` (set to 0 if none is scheduled). +/// - `activation_date_time`: When the next corporate action takes effect (set to 0 if none is scheduled). +/// - `tokenized_price`: 24/7 tokenized equity price. +/// +/// # Solidity Equivalent +/// ```solidity +/// struct ReportDataV10 { +/// bytes32 feedId; +/// uint32 validFromTimestamp; +/// uint32 observationsTimestamp; +/// uint192 nativeFee; +/// uint192 linkFee; +/// uint32 expiresAt; +/// uint64 lastUpdateTimestamp; +/// int192 price; +/// uint32 marketStatus; +/// int192 currentMultiplier; +/// int192 newMultiplier; +/// uint32 activationDateTime; +/// int192 tokenizedPrice; +/// } +/// ``` +#[derive(Debug)] +pub struct ReportDataV10 { + 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 last_update_timestamp: u64, + pub price: BigInt, + pub market_status: u32, + pub current_multiplier: BigInt, + pub new_multiplier: BigInt, + pub activation_date_time: u32, + pub tokenized_price: BigInt, +} + +impl ReportDataV10 { + /// Decodes an ABI-encoded `ReportDataV10` from bytes. + /// + /// # Parameters + /// + /// - `data`: The encoded report data. + /// + /// # Returns + /// + /// The decoded `ReportDataV10`. + /// + /// # 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() < 13 * ReportBase::WORD_SIZE { + return Err(ReportError::DataTooShort("ReportDataV10")); + } + + 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 last_update_timestamp = ReportBase::read_uint64(data, 6 * ReportBase::WORD_SIZE)?; + let price = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?; + let market_status = ReportBase::read_uint32(data, 8 * ReportBase::WORD_SIZE)?; + let current_multiplier = ReportBase::read_int192(data, 9 * ReportBase::WORD_SIZE)?; + let new_multiplier = ReportBase::read_int192(data, 10 * ReportBase::WORD_SIZE)?; + let activation_date_time = ReportBase::read_uint32(data, 11 * ReportBase::WORD_SIZE)?; + let tokenized_price = ReportBase::read_int192(data, 12 * ReportBase::WORD_SIZE)?; + + Ok(Self { + feed_id, + valid_from_timestamp, + observations_timestamp, + native_fee, + link_fee, + expires_at, + last_update_timestamp, + price, + market_status, + current_multiplier, + new_multiplier, + activation_date_time, + tokenized_price, + }) + } + + /// Encodes the `ReportDataV10` 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(13 * 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_uint64(self.last_update_timestamp)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.price)?); + buffer.extend_from_slice(&ReportBase::encode_uint32(self.market_status)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.current_multiplier)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.new_multiplier)?); + buffer.extend_from_slice(&ReportBase::encode_uint32(self.activation_date_time)?); + buffer.extend_from_slice(&ReportBase::encode_int192(&self.tokenized_price)?); + + Ok(buffer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::report::tests::{ + generate_mock_report_data_v10, MOCK_FEE, MOCK_PRICE, MOCK_TIMESTAMP, MARKET_STATUS_OPEN + }; + + const V10_FEED_ID_STR: &str = + "0x000a6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472"; + + #[test] + fn test_decode_report_data_v10() { + let report_data = generate_mock_report_data_v10(); + let encoded = report_data.abi_encode().unwrap(); + let decoded = ReportDataV10::decode(&encoded).unwrap(); + + const MOCK_MULTIPLIER: isize = 1000000000000000000; + + let expected_feed_id = ID::from_hex_str(V10_FEED_ID_STR).unwrap(); + let expected_timestamp: u32 = MOCK_TIMESTAMP; + let expected_fee = BigInt::from(MOCK_FEE); + let expected_price = BigInt::from(MOCK_PRICE); + let expected_market_status: u32 = MARKET_STATUS_OPEN; + let expected_multiplier = BigInt::from(MOCK_MULTIPLIER); // 1.0 with 18 decimals + let expected_tokenized_price = BigInt::from(MOCK_PRICE * 2); // Example tokenized 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.last_update_timestamp, expected_timestamp as u64); + assert_eq!(decoded.price, expected_price); + assert_eq!(decoded.market_status, expected_market_status); + assert_eq!(decoded.current_multiplier, expected_multiplier); + assert_eq!(decoded.new_multiplier, expected_multiplier); + assert_eq!(decoded.activation_date_time, expected_timestamp + 200); + assert_eq!(decoded.tokenized_price, expected_tokenized_price); + } +} diff --git a/rust/crates/sdk/Cargo.toml b/rust/crates/sdk/Cargo.toml index 2faffd1..06a661a 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.1" +version = "1.0.2" 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.1" } +chainlink-data-streams-report = { path = "../report", version = "1.0.2" } reqwest = { version = "0.11.20", features = ["json", "rustls-tls"] } tokio = { version = "1.29.1", features = ["full"] } tokio-tungstenite = { version = "0.20.1", features = [