use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use crate::error::{EscrowError, Result};
use crate::types::{EscrowId, EscrowStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
ContractCreated,
ParticipantJoined,
FundsDeposited,
ConditionAdded,
ConditionEvaluated,
SignatureAdded,
ReleaseInitiated,
FundsReleased,
ContractCancelled,
DisputeStarted,
DisputeResolved,
ContractExpired,
AIDecision,
OracleQuery,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: String,
pub timestamp: DateTime<Utc>,
pub event_type: AuditEventType,
pub escrow_id: EscrowId,
pub actor: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_hash: Option<String>,
pub hash: String,
}
impl AuditEntry {
pub fn new(
event_type: AuditEventType,
escrow_id: EscrowId,
actor: String,
description: String,
metadata: Option<HashMap<String, serde_json::Value>>,
prev_hash: Option<String>,
) -> Self {
let id = uuid::Uuid::new_v4().to_string();
let timestamp = Utc::now();
let hash = Self::calculate_hash(&id, ×tamp, &event_type, &escrow_id, &actor, &description, &prev_hash);
Self {
id,
timestamp,
event_type,
escrow_id,
actor,
description,
metadata,
prev_hash,
hash,
}
}
fn calculate_hash(
id: &str,
timestamp: &DateTime<Utc>,
event_type: &AuditEventType,
escrow_id: &EscrowId,
actor: &str,
description: &str,
prev_hash: &Option<String>,
) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
timestamp.to_rfc3339().hash(&mut hasher);
format!("{:?}", event_type).hash(&mut hasher);
escrow_id.to_string().hash(&mut hasher);
actor.hash(&mut hasher);
description.hash(&mut hasher);
prev_hash.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
}
#[derive(Debug)]
pub struct AuditLogger {
path: PathBuf,
writer: Arc<Mutex<BufWriter<File>>>,
last_hash: Arc<Mutex<Option<String>>>,
entries: Arc<Mutex<Vec<AuditEntry>>>,
}
impl AuditLogger {
pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self> {
let path = path.into();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let writer = Arc::new(Mutex::new(BufWriter::new(file)));
Ok(Self {
path,
writer,
last_hash: Arc::new(Mutex::new(None)),
entries: Arc::new(Mutex::new(Vec::new())),
})
}
pub fn in_memory() -> Self {
Self {
path: PathBuf::from(":memory:"),
writer: Arc::new(Mutex::new(BufWriter::new(
OpenOptions::new()
.write(true)
.open("/dev/null")
.unwrap()
))),
last_hash: Arc::new(Mutex::new(None)),
entries: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn log(
&self,
event_type: AuditEventType,
escrow_id: EscrowId,
actor: String,
description: String,
metadata: Option<HashMap<String, serde_json::Value>>,
) -> Result<AuditEntry> {
let prev_hash = self.last_hash.lock().unwrap().clone();
let entry = AuditEntry::new(
event_type,
escrow_id,
actor,
description,
metadata,
prev_hash,
);
*self.last_hash.lock().unwrap() = Some(entry.hash.clone());
self.entries.lock().unwrap().push(entry.clone());
self.write_entry(&entry)?;
Ok(entry)
}
fn write_entry(&self, entry: &AuditEntry) -> Result<()> {
let json = serde_json::to_string(entry)?;
let mut writer = self.writer.lock().unwrap();
writeln!(writer, "{}", json)?;
writer.flush()?;
Ok(())
}
pub fn get_entries(&self, escrow_id: &EscrowId) -> Vec<AuditEntry> {
self.entries
.lock()
.unwrap()
.iter()
.filter(|e| &e.escrow_id == escrow_id)
.cloned()
.collect()
}
pub fn get_all_entries(&self) -> Vec<AuditEntry> {
self.entries.lock().unwrap().clone()
}
pub fn verify_integrity(&self) -> Result<bool> {
let entries = self.entries.lock().unwrap();
for (i, entry) in entries.iter().enumerate() {
let expected_hash = AuditEntry::calculate_hash(
&entry.id,
&entry.timestamp,
&entry.event_type,
&entry.escrow_id,
&entry.actor,
&entry.description,
&entry.prev_hash,
);
if entry.hash != expected_hash {
return Ok(false);
}
if i > 0 {
let prev_entry = &entries[i - 1];
if entry.prev_hash.as_ref() != Some(&prev_entry.hash) {
return Ok(false);
}
}
}
Ok(true)
}
pub fn export_json(&self) -> Result<String> {
let entries = self.entries.lock().unwrap();
Ok(serde_json::to_string_pretty(&*entries)?)
}
pub fn export_to_file<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
let path = path.into();
let json = self.export_json()?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
pub fn default_audit_logger() -> Result<AuditLogger> {
AuditLogger::new("/tmp/escrow_audit.log")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_entry_creation() {
let entry = AuditEntry::new(
AuditEventType::ContractCreated,
EscrowId::new(),
"agent-1".to_string(),
"Contract created".to_string(),
None,
None,
);
assert!(!entry.id.is_empty());
assert!(!entry.hash.is_empty());
}
#[test]
fn test_audit_logger() {
let logger = AuditLogger::in_memory();
let escrow_id = EscrowId::new();
logger.log(
AuditEventType::ContractCreated,
escrow_id.clone(),
"agent-1".to_string(),
"Contract created".to_string(),
None,
).unwrap();
let entries = logger.get_entries(&escrow_id);
assert_eq!(entries.len(), 1);
}
#[test]
fn test_integrity_chain() {
let logger = AuditLogger::in_memory();
let escrow_id = EscrowId::new();
logger.log(
AuditEventType::ContractCreated,
escrow_id.clone(),
"agent-1".to_string(),
"Contract created".to_string(),
None,
).unwrap();
logger.log(
AuditEventType::FundsDeposited,
escrow_id.clone(),
"agent-1".to_string(),
"Funds deposited".to_string(),
None,
).unwrap();
assert!(logger.verify_integrity().unwrap());
}
}