provable-proof 0.1.2

Provable proof envelope format for Rust
Documentation
use crate::types::{KayrosData, KayrosProof, ProofDataFormat};
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use provable_sdk::{keccak256, sha256, ProvableError, Result};
use serde::Serialize;

fn decode_base64(value: &str) -> Result<Vec<u8>> {
    BASE64_STANDARD.decode(value.trim()).map_err(|error| {
        ProvableError::new(format!(
            "Invalid proof data: expected base64 string ({})",
            error
        ))
    })
}

fn encode_base64(bytes: &[u8]) -> String {
    BASE64_STANDARD.encode(bytes)
}

fn normalize_hash(value: Option<&str>) -> Option<String> {
    value.and_then(|input| {
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return None;
        }
        let normalized = trimmed.trim_start_matches("0x").to_lowercase();
        if normalized.len() % 2 == 0 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
            return Some(normalized);
        }
        BASE64_STANDARD.decode(trimmed).ok().map(hex::encode)
    })
}

fn first_string(values: &[Option<String>]) -> Option<String> {
    values
        .iter()
        .flatten()
        .find(|value| !value.is_empty())
        .cloned()
}

#[derive(Debug, Clone, PartialEq)]
pub struct KayrosEnvelope {
    pub data: String,
    pub data_format: ProofDataFormat,
    pub kayros: KayrosData,
    data_bytes: Vec<u8>,
}

impl KayrosEnvelope {
    pub fn new(
        data: impl Into<String>,
        kayros: KayrosData,
        data_format: impl Into<String>,
    ) -> Result<Self> {
        let data = data.into();
        let data_bytes = decode_base64(&data)?;
        Ok(Self {
            data,
            data_format: data_format.into(),
            kayros,
            data_bytes,
        })
    }

    pub fn from_bytes(
        data: &[u8],
        kayros: KayrosData,
        data_format: impl Into<String>,
    ) -> Result<Self> {
        Self::new(encode_base64(data), kayros, data_format)
    }

    pub fn from_text(
        data: &str,
        kayros: KayrosData,
        data_format: impl Into<String>,
    ) -> Result<Self> {
        Self::from_bytes(data.as_bytes(), kayros, data_format)
    }

    pub fn from_json_value<T: Serialize>(
        data: &T,
        kayros: KayrosData,
        data_format: impl Into<String>,
    ) -> Result<Self> {
        let serialized = serde_json::to_vec(data)?;
        Self::from_bytes(&serialized, kayros, data_format)
    }

    pub fn from_json_str(json: &str) -> Result<Self> {
        let parsed: KayrosProof = serde_json::from_str(json)?;
        Self::new(
            parsed.data,
            parsed.kayros,
            parsed.data_format.unwrap_or_default(),
        )
    }

    pub fn get_data_format(&self) -> String {
        self.data_format.clone()
    }

    pub fn get_data_hash(&self) -> Option<String> {
        normalize_hash(self.kayros.hash.as_deref())
    }

    pub fn get_data_type(&self) -> Option<String> {
        self.kayros
            .timestamp
            .as_ref()
            .and_then(|timestamp| timestamp.response.data.as_ref())
            .map(|record| record.data_type.clone())
    }

    pub fn get_data_type_label(&self) -> Option<String> {
        self.get_data_type()
    }

    pub fn get_data_type_lookup_candidates(&self) -> Vec<Option<String>> {
        vec![self.get_data_type()]
    }

    pub fn get_kayros_hash(&self) -> Option<String> {
        let direct = self
            .kayros
            .timestamp
            .as_ref()
            .and_then(|timestamp| timestamp.response.response.as_ref())
            .and_then(|response| response.hash.clone());
        let fallback = self
            .kayros
            .timestamp
            .as_ref()
            .and_then(|timestamp| timestamp.response.data.as_ref())
            .map(|record| record.hash_item.clone());
        normalize_hash(first_string(&[direct, fallback]).as_deref())
    }

    pub fn get_timeuuid(&self) -> Option<String> {
        first_string(&[
            self.kayros
                .timestamp
                .as_ref()
                .and_then(|timestamp| timestamp.response.response.as_ref())
                .and_then(|response| response.timeuuid.clone()),
            self.kayros
                .timestamp
                .as_ref()
                .and_then(|timestamp| timestamp.response.data.as_ref())
                .map(|record| record.ts.clone()),
        ])
    }

    pub fn get_hash_algorithm(&self) -> String {
        self.kayros
            .hash_algorithm
            .clone()
            .unwrap_or_else(|| "sha256".to_string())
            .to_lowercase()
            .replace('_', "")
            .replace('-', "")
    }

    pub fn get_data(&self) -> &[u8] {
        &self.data_bytes
    }

    pub fn get_data_text(&self) -> Result<String> {
        String::from_utf8(self.data_bytes.clone())
            .map_err(|error| ProvableError::new(error.to_string()))
    }

    pub fn parse_data_json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
        Ok(serde_json::from_slice(&self.data_bytes)?)
    }

    pub fn to_proof(&self) -> KayrosProof {
        KayrosProof {
            data: self.data.clone(),
            data_format: Some(self.data_format.clone()),
            kayros: self.kayros.clone(),
        }
    }

    pub fn compute_data_hash(&self) -> Result<String> {
        if self.data_format == "raw_hash" {
            return Ok(hex::encode(&self.data_bytes));
        }
        Ok(match self.get_hash_algorithm().as_str() {
            "keccak256" => keccak256(&self.data_bytes),
            _ => sha256(&self.data_bytes),
        })
    }
}