use serde::{Deserialize, Serialize};
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub method: String,
#[serde(rename = "protocolLevel", skip_serializing_if = "Option::is_none")]
pub protocol_level: Option<u8>,
#[serde(rename = "protocolName", skip_serializing_if = "Option::is_none")]
pub protocol_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub counterparty: Option<String>,
#[serde(rename = "keyID", skip_serializing_if = "Option::is_none")]
pub key_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub satoshis: Option<u64>,
pub originator: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub seq: u64,
}
fn log() -> &'static Mutex<Vec<AuditEntry>> {
static LOG: OnceLock<Mutex<Vec<AuditEntry>>> = OnceLock::new();
LOG.get_or_init(|| Mutex::new(Vec::new()))
}
const MAX_ENTRIES: usize = 5000;
pub fn record(mut entry: AuditEntry) {
if let Ok(mut guard) = log().lock() {
entry.seq = guard.len() as u64;
if guard.len() >= MAX_ENTRIES {
guard.remove(0);
}
guard.push(entry);
}
}
pub fn snapshot() -> Vec<AuditEntry> {
log().lock().map(|g| g.clone()).unwrap_or_default()
}
pub fn reset() {
if let Ok(mut guard) = log().lock() {
guard.clear();
}
}
pub fn protocol_entry(
method: &str,
level: u8,
name: &str,
counterparty: Option<String>,
key_id: Option<String>,
originator: &str,
) -> AuditEntry {
AuditEntry {
method: method.to_string(),
protocol_level: Some(level),
protocol_name: Some(name.to_string()),
counterparty,
key_id,
satoshis: None,
originator: originator.to_string(),
description: None,
seq: 0,
}
}
pub fn spend_entry(
method: &str,
satoshis: u64,
description: Option<String>,
originator: &str,
) -> AuditEntry {
AuditEntry {
method: method.to_string(),
protocol_level: None,
protocol_name: None,
counterparty: None,
key_id: None,
satoshis: Some(satoshis),
originator: originator.to_string(),
description,
seq: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn serial() -> std::sync::MutexGuard<'static, ()> {
static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
GUARD
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
#[test]
fn records_in_order_and_resets() {
let _s = serial();
reset();
record(protocol_entry(
"createSignature",
2,
"low settle",
Some("02ab".into()),
None,
"low.game",
));
record(spend_entry(
"createAction",
20_000,
Some("LOW pot JOIN funding hop".into()),
"low.game",
));
let s = snapshot();
assert_eq!(s.len(), 2);
assert_eq!(s[0].seq, 0);
assert_eq!(s[0].protocol_level, Some(2));
assert_eq!(s[1].seq, 1);
assert_eq!(s[1].satoshis, Some(20_000));
reset();
assert!(snapshot().is_empty());
}
#[test]
fn the_ring_is_bounded() {
let _s = serial();
reset();
for _ in 0..(MAX_ENTRIES + 10) {
record(spend_entry("createAction", 1, None, "low.game"));
}
assert_eq!(snapshot().len(), MAX_ENTRIES);
reset();
}
}