use crate::{DIDWebVHError, parameters::Parameters, witness::Witnesses};
use affinidi_data_integrity::{DataIntegrityProof, verification_proof::verify_data};
use base58::ToBase58;
use multihash::Multihash;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use serde_json_canonicalizer::to_string;
use sha2::{Digest, Sha256};
use std::{fs::OpenOptions, io::Write};
use tracing::debug;
pub mod read;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaData {
pub version_id: String,
pub version_time: String,
pub created: String,
pub updated: String,
pub scid: String,
pub portable: bool,
pub deactivated: bool,
pub witness: Option<Witnesses>,
pub watchers: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LogEntry {
pub version_id: String,
pub version_time: String,
pub parameters: Parameters,
pub state: Value,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub proof: Vec<DataIntegrityProof>,
}
impl LogEntry {
pub fn save_to_file(&self, file_path: &str) -> Result<(), DIDWebVHError> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)
.map_err(|e| {
DIDWebVHError::LogEntryError(format!("Couldn't open file {file_path}: {e}"))
})?;
file.write_all(
serde_json::to_string(self)
.map_err(|e| {
DIDWebVHError::LogEntryError(format!(
"Couldn't serialize LogEntry to JSON. Reason: {e}",
))
})?
.as_bytes(),
)
.map_err(|e| {
DIDWebVHError::LogEntryError(format!(
"Couldn't append LogEntry to file({file_path}). Reason: {e}",
))
})?;
file.write_all("\n".as_bytes()).map_err(|e| {
DIDWebVHError::LogEntryError(format!(
"Couldn't append LogEntry to file({file_path}). Reason: {e}",
))
})?;
Ok(())
}
pub(crate) fn generate_scid(&self) -> Result<String, DIDWebVHError> {
self.generate_log_entry_hash().map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't generate SCID from preliminary LogEntry. Reason: {e}",
))
})
}
pub fn generate_log_entry_hash(&self) -> Result<String, DIDWebVHError> {
let jcs = to_string(self).map_err(|e| {
DIDWebVHError::SCIDError(format!("Couldn't generate JCS from LogEntry. Reason: {e}",))
})?;
debug!("JCS for LogEntry hash: {}", jcs);
let hash_encoded = Multihash::<32>::wrap(0x12, Sha256::digest(jcs.as_bytes()).as_slice())
.map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't create multihash encoding for LogEntry. Reason: {e}",
))
})?;
Ok(hash_encoded.to_bytes().to_base58())
}
pub fn validate_witness_proof(
&self,
witness_proof: &DataIntegrityProof,
) -> Result<bool, DIDWebVHError> {
verify_data(&json!({"versionId": &self.version_id}), None, witness_proof).map_err(|e| {
DIDWebVHError::LogEntryError(format!("Data Integrity Proof verification failed: {e}"))
})?;
Ok(true)
}
pub fn get_version_id_fields(&self) -> Result<(u32, String), DIDWebVHError> {
LogEntry::parse_version_id_fields(&self.version_id)
}
pub fn parse_version_id_fields(version_id: &str) -> Result<(u32, String), DIDWebVHError> {
let Some((id, hash)) = version_id.split_once('-') else {
return Err(DIDWebVHError::ValidationError(format!(
"versionID ({version_id}) doesn't match format <int>-<hash>",
)));
};
let id = id.parse::<u32>().map_err(|e| {
DIDWebVHError::ValidationError(
format!("Failed to parse version ID ({id}) as u32: {e}",),
)
})?;
Ok((id, hash.to_string()))
}
}