chainlink-data-streams-report 0.0.1

Chainlink Data Streams Report
Documentation
pub mod base;
pub mod compress;
pub mod v1;
pub mod v2;
pub mod v3;
pub mod v4;

use base::{ReportBase, ReportError};

use crate::feed_id::ID;

use serde::{Deserialize, Serialize};

/// Represents a report that will be returned from the Data Streams DON.
///
/// The `Report` struct contains the following fields:
/// * `feed_id`: The unique identifier of the feed.
/// * `valid_from_timestamp`: Earliest timestamp for which price is applicable.
/// * `observations_timestamp`: Latest timestamp for which price is applicable.
/// * `full_report`: The report data (bytes) that needs to be decoded further - to version-specific report data.
///
/// # Examples
///
/// ```rust
/// use data_streams_report::report::Report;
/// use data_streams_report::feed_id::ID;
///
/// let id = ID::from_hex_str("0x00016b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472").unwrap();
/// let report = Report {
///    feed_id: id,
///    valid_from_timestamp: 1718885772,
///    observations_timestamp: 1718885772,
///    full_report: "00016b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b84720000000000000000000000000000000000000000000000000000000066741d8c00000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000640000070407020401522602090605060802080505a335ef7fae696b663f1b840100000000000000000000000000000000000000000000000000000000000bbbda0000000000000000000000000000000000000000000000000000000066741d8c".to_string(),
/// };
/// ```    
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Report {
    #[serde(rename = "feedID")]
    // pub feed_id: [u8; 32],
    pub feed_id: ID,

    #[serde(rename = "validFromTimestamp")]
    pub valid_from_timestamp: usize,

    #[serde(rename = "observationsTimestamp")]
    pub observations_timestamp: usize,

    #[serde(rename = "fullReport")]
    pub full_report: String,
}

/// ABI-decodes a full report payload into its report context (`bytes32[3]`) and report blob (`bytes`).
/// The report blob is the actual report data that needs to be decoded further - to version-specific report data.
///
/// # Parameters
///
/// - `payload`: The full report payload.
///
/// Solidity Equivalent:
/// ```solidity
/// struct ReportCallback {
///     bytes32[3] reportContext;
///     bytes reportBlob;
///     bytes32[] rawRs;
///     bytes32[] rawSs;
///     bytes32 rawVs;
/// }
/// ```
///
/// # Returns
///
/// The report context and report blob.
///
/// # Errors
///
/// Returns a `String` if the payload is too short, the offset is invalid, or the length is invalid.
pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), ReportError> {
    if payload.len() < 128 {
        return Err(ReportError::DataTooShort("Payload is too short"));
    }

    // Decode the first three bytes32 elements
    let report_context = (0..3)
        .map(|i| payload[i * ReportBase::WORD_SIZE..(i + 1) * ReportBase::WORD_SIZE].try_into())
        .collect::<Result<Vec<_>, _>>()
        .map_err(|_| ReportError::ParseError("report_context"))?;

    // Decode the offset for the bytes reportBlob data
    let offset = usize::from_be_bytes(
        payload[96..128][24..ReportBase::WORD_SIZE] // Offset value is stored as Little Endian
            .try_into()
            .map_err(|_| ReportError::ParseError("offset as usize"))?,
    );

    if offset < 128 || offset >= payload.len() {
        return Err(ReportError::InvalidLength("offset"));
    }

    // Decode the length of the bytes reportBlob data
    let length = usize::from_be_bytes(
        payload[offset..offset + 32][24..ReportBase::WORD_SIZE] // Length value is stored as Little Endian
            .try_into()
            .map_err(|_| ReportError::ParseError("length as usize"))?,
    );

    if offset + ReportBase::WORD_SIZE + length > payload.len() {
        return Err(ReportError::InvalidLength("bytes data"));
    }

    // Decode the remainder of the payload (actual bytes reportBlob data)
    let report_blob =
        payload[offset + ReportBase::WORD_SIZE..offset + ReportBase::WORD_SIZE + length].to_vec();

    Ok((report_context, report_blob))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::report::{v1::ReportDataV1, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4};
    use num_bigint::BigInt;

    const V1_FEED_ID: ID = ID([
        0, 1, 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 V2_FEED_ID: ID = ID([
        00, 02, 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 V3_FEED_ID: ID = ID([
        00, 03, 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 V4_FEED_ID: ID = ID([
        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,
    ]);

    pub const MOCK_TIMESTAMP: u32 = 1718885772;
    pub const MOCK_FEE: usize = 10;
    pub const MOCK_PRICE: isize = 100;

    pub fn generate_mock_report_data_v1() -> ReportDataV1 {
        let report_data = ReportDataV1 {
            feed_id: V1_FEED_ID,
            observations_timestamp: MOCK_TIMESTAMP,
            benchmark_price: BigInt::from(MOCK_PRICE),
            bid: BigInt::from(MOCK_PRICE),
            ask: BigInt::from(MOCK_PRICE),
            current_block_num: 100,
            current_block_hash: [
                0, 0, 7, 4, 7, 2, 4, 1, 82, 38, 2, 9, 6, 5, 6, 8, 2, 8, 5, 5, 163, 53, 239, 127,
                174, 105, 107, 102, 63, 27, 132, 1,
            ],
            valid_from_block_num: 768986,
            current_block_timestamp: MOCK_TIMESTAMP as u64,
        };

        report_data
    }

    pub fn generate_mock_report_data_v2() -> ReportDataV2 {
        let report_data = ReportDataV2 {
            feed_id: V2_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,
            benchmark_price: BigInt::from(MOCK_PRICE),
        };

        report_data
    }

    pub fn generate_mock_report_data_v3() -> ReportDataV3 {
        let delta = BigInt::from(10) * BigInt::from(MOCK_PRICE) / BigInt::from(100); // 10% of mock_price

        let report_data = ReportDataV3 {
            feed_id: V3_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,
            benchmark_price: BigInt::from(MOCK_PRICE),
            bid: MOCK_PRICE - delta.clone(),
            ask: MOCK_PRICE + delta,
        };

        report_data
    }

    pub fn generate_mock_report_data_v4() -> ReportDataV4 {
        const MARKET_STATUS_OPEN: u32 = 2;

        let report_data = ReportDataV4 {
            feed_id: V4_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),
            market_status: MARKET_STATUS_OPEN,
        };

        report_data
    }

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

        let report_context = vec![[0u8; 32]; 3];
        for context in &report_context {
            payload.extend_from_slice(context);
        }

        let mut offset = [0u8; 32];
        let offset_value: usize = 96 + 32;
        offset[24..32].copy_from_slice(&offset_value.to_be_bytes());
        payload.extend_from_slice(&offset);

        let mut length = [0u8; 32];
        let length_value: usize = encoded_report_data.len();
        length[24..32].copy_from_slice(&length_value.to_be_bytes());
        payload.extend_from_slice(&length);

        payload.extend_from_slice(encoded_report_data);

        // Raw `r` values, `s` values, and `v` values are not used in this test

        payload
    }

    fn bytes(hex_str: &str) -> Vec<u8> {
        if hex_str.len() % 2 != 0 {
            panic!("Invalid hex string: odd number of characters");
        }

        hex_str
            .trim_start_matches("0x")
            .as_bytes()
            .chunks(2)
            .map(|chunk| u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap())
            .collect()
    }

    #[test]
    fn test_decode_report_v1() {
        let report_data = generate_mock_report_data_v1();
        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![
            "00016b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "0000000000000000000000000000000000000000000000000000000000000064",
            "0000000000000000000000000000000000000000000000000000000000000064",
            "0000000000000000000000000000000000000000000000000000000000000064",
            "0000000000000000000000000000000000000000000000000000000000000064",
            "0000070407020401522602090605060802080505a335ef7fae696b663f1b8401",
            "00000000000000000000000000000000000000000000000000000000000bbbda",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
        ];

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

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

        assert_eq!(decoded_report.feed_id, V1_FEED_ID);
    }

    #[test]
    fn test_decode_report_v2() {
        let report_data = generate_mock_report_data_v2();
        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![
            "00026b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "0000000000000000000000000000000000000000000000000000000066741df0",
            "0000000000000000000000000000000000000000000000000000000000000064",
        ];

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

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

        assert_eq!(decoded_report.feed_id, V2_FEED_ID);
    }

    #[test]
    fn test_decode_report_v3() {
        let report_data = generate_mock_report_data_v3();
        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![
            "00036b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "0000000000000000000000000000000000000000000000000000000066741df0",
            "0000000000000000000000000000000000000000000000000000000000000064", // Price: 100
            "000000000000000000000000000000000000000000000000000000000000005a", // Bid: 90
            "000000000000000000000000000000000000000000000000000000000000006e", // Ask: 110
        ];

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

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

        assert_eq!(decoded_report.feed_id, V3_FEED_ID);
    }

    #[test]
    fn test_decode_report_v4() {
        let report_data = generate_mock_report_data_v4();
        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![
            "00046b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "0000000000000000000000000000000000000000000000000000000066741d8c",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "000000000000000000000000000000000000000000000000000000000000000a",
            "0000000000000000000000000000000000000000000000000000000066741df0",
            "0000000000000000000000000000000000000000000000000000000000000064",
            "0000000000000000000000000000000000000000000000000000000000000002", // Market status: Open
        ];

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

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

        assert_eq!(decoded_report.feed_id, V4_FEED_ID);
    }
}