Initialize repository snapshot

This commit is contained in:
Vault Sovereign
2025-12-27 00:10:32 +00:00
commit 110d644e10
281 changed files with 40331 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "vaultmesh-treasury"
version = "0.1.0"
edition = "2021"
[dependencies]
vaultmesh-core = { path = "../vaultmesh-core" }
vaultmesh-observability = { path = "../vaultmesh-observability", optional = true }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[features]
default = []
metrics = ["vaultmesh-observability"]
[dev-dependencies]
tempfile = "3.8"

View File

@@ -0,0 +1,466 @@
//! VaultMesh Treasury Engine - Budget tracking and financial receipts
//!
//! The Treasury engine manages budgets, tracks spending, and emits receipts
//! for all financial operations in the Civilization Ledger.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[cfg(feature = "metrics")]
use std::time::Instant;
use vaultmesh_core::{Scroll, VmHash};
#[cfg(feature = "metrics")]
use vaultmesh_observability::ObservabilityEngine;
/// Schema version for treasury receipts
pub const SCHEMA_VERSION: &str = "2.0.0";
/// A budget allocation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Budget {
pub id: String,
pub name: String,
pub currency: String,
pub allocated: u64,
pub spent: u64,
pub created_at: DateTime<Utc>,
pub created_by: String,
}
impl Budget {
/// Remaining balance
pub fn remaining(&self) -> u64 {
self.allocated.saturating_sub(self.spent)
}
/// Check if a spend would exceed budget
pub fn can_spend(&self, amount: u64) -> bool {
self.spent + amount <= self.allocated
}
}
/// Receipt body for budget creation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetCreateReceipt {
pub budget_id: String,
pub name: String,
pub currency: String,
pub allocated: u64,
pub created_by: String,
}
/// Receipt body for debit operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryDebitReceipt {
pub budget_id: String,
pub amount: u64,
pub currency: String,
pub description: String,
pub debited_by: String,
pub new_spent: u64,
pub new_remaining: u64,
}
/// Receipt body for credit operations (refunds, adjustments)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryCreditReceipt {
pub budget_id: String,
pub amount: u64,
pub currency: String,
pub description: String,
pub credited_by: String,
pub new_allocated: u64,
}
/// Generic treasury receipt wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryReceipt {
pub schema_version: String,
#[serde(rename = "type")]
pub receipt_type: String,
pub timestamp: DateTime<Utc>,
pub scroll: String,
pub tags: Vec<String>,
pub root_hash: String,
pub body: serde_json::Value,
}
/// Treasury engine errors
#[derive(Debug)]
pub enum TreasuryError {
BudgetExists(String),
BudgetNotFound(String),
InsufficientFunds { budget_id: String, requested: u64, available: u64 },
IoError(std::io::Error),
SerializationError(serde_json::Error),
}
impl std::fmt::Display for TreasuryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TreasuryError::BudgetExists(id) => write!(f, "Budget already exists: {}", id),
TreasuryError::BudgetNotFound(id) => write!(f, "Budget not found: {}", id),
TreasuryError::InsufficientFunds { budget_id, requested, available } => {
write!(f, "Insufficient funds in {}: requested {}, available {}", budget_id, requested, available)
}
TreasuryError::IoError(e) => write!(f, "IO error: {}", e),
TreasuryError::SerializationError(e) => write!(f, "Serialization error: {}", e),
}
}
}
impl std::error::Error for TreasuryError {}
impl From<std::io::Error> for TreasuryError {
fn from(e: std::io::Error) -> Self {
TreasuryError::IoError(e)
}
}
impl From<serde_json::Error> for TreasuryError {
fn from(e: serde_json::Error) -> Self {
TreasuryError::SerializationError(e)
}
}
/// Treasury engine for budget management
pub struct TreasuryEngine {
/// Path to VaultMesh root
pub vaultmesh_root: PathBuf,
/// In-memory budget cache
budgets: HashMap<String, Budget>,
/// Default currency
pub default_currency: String,
/// Optional observability engine for metrics
#[cfg(feature = "metrics")]
pub observability: Option<Arc<ObservabilityEngine>>,
}
impl TreasuryEngine {
/// Create a new Treasury engine
pub fn new(vaultmesh_root: impl AsRef<Path>) -> Self {
Self {
vaultmesh_root: vaultmesh_root.as_ref().to_path_buf(),
budgets: HashMap::new(),
default_currency: "EUR".to_string(),
#[cfg(feature = "metrics")]
observability: None,
}
}
/// Set default currency
pub fn with_currency(mut self, currency: &str) -> Self {
self.default_currency = currency.to_string();
self
}
/// Set the observability engine for metrics
#[cfg(feature = "metrics")]
pub fn with_observability(mut self, obs: Arc<ObservabilityEngine>) -> Self {
self.observability = Some(obs);
self
}
/// Get path to treasury events JSONL
fn events_path(&self) -> PathBuf {
self.vaultmesh_root.join(Scroll::Treasury.jsonl_path())
}
/// Create a new budget
pub fn create_budget(
&mut self,
id: &str,
name: &str,
allocated: u64,
created_by: &str,
) -> Result<Budget, TreasuryError> {
self.create_budget_with_currency(id, name, allocated, &self.default_currency.clone(), created_by)
}
/// Create a new budget with specific currency
pub fn create_budget_with_currency(
&mut self,
id: &str,
name: &str,
allocated: u64,
currency: &str,
created_by: &str,
) -> Result<Budget, TreasuryError> {
if self.budgets.contains_key(id) {
return Err(TreasuryError::BudgetExists(id.to_string()));
}
let budget = Budget {
id: id.to_string(),
name: name.to_string(),
currency: currency.to_string(),
allocated,
spent: 0,
created_at: Utc::now(),
created_by: created_by.to_string(),
};
self.budgets.insert(id.to_string(), budget.clone());
// Emit receipt
let receipt_body = BudgetCreateReceipt {
budget_id: id.to_string(),
name: name.to_string(),
currency: currency.to_string(),
allocated,
created_by: created_by.to_string(),
};
self.emit_receipt("treasury_budget_create", &receipt_body, vec![
"treasury".to_string(),
"budget".to_string(),
"create".to_string(),
id.to_string(),
])?;
Ok(budget)
}
/// Debit (spend) from a budget
pub fn debit(
&mut self,
budget_id: &str,
amount: u64,
description: &str,
debited_by: &str,
) -> Result<Budget, TreasuryError> {
let budget = self.budgets.get_mut(budget_id)
.ok_or_else(|| TreasuryError::BudgetNotFound(budget_id.to_string()))?;
if !budget.can_spend(amount) {
return Err(TreasuryError::InsufficientFunds {
budget_id: budget_id.to_string(),
requested: amount,
available: budget.remaining(),
});
}
budget.spent += amount;
let updated = budget.clone();
// Emit receipt
let receipt_body = TreasuryDebitReceipt {
budget_id: budget_id.to_string(),
amount,
currency: updated.currency.clone(),
description: description.to_string(),
debited_by: debited_by.to_string(),
new_spent: updated.spent,
new_remaining: updated.remaining(),
};
self.emit_receipt("treasury_debit", &receipt_body, vec![
"treasury".to_string(),
"debit".to_string(),
budget_id.to_string(),
])?;
Ok(updated)
}
/// Credit (add funds) to a budget
pub fn credit(
&mut self,
budget_id: &str,
amount: u64,
description: &str,
credited_by: &str,
) -> Result<Budget, TreasuryError> {
let budget = self.budgets.get_mut(budget_id)
.ok_or_else(|| TreasuryError::BudgetNotFound(budget_id.to_string()))?;
budget.allocated += amount;
let updated = budget.clone();
// Emit receipt
let receipt_body = TreasuryCreditReceipt {
budget_id: budget_id.to_string(),
amount,
currency: updated.currency.clone(),
description: description.to_string(),
credited_by: credited_by.to_string(),
new_allocated: updated.allocated,
};
self.emit_receipt("treasury_credit", &receipt_body, vec![
"treasury".to_string(),
"credit".to_string(),
budget_id.to_string(),
])?;
Ok(updated)
}
/// Get a budget by ID
pub fn get_budget(&self, id: &str) -> Option<&Budget> {
self.budgets.get(id)
}
/// List all budgets
pub fn list_budgets(&self) -> Vec<&Budget> {
self.budgets.values().collect()
}
/// Emit a treasury receipt
fn emit_receipt<T: Serialize>(
&self,
receipt_type: &str,
body: &T,
tags: Vec<String>,
) -> Result<(), TreasuryError> {
#[cfg(feature = "metrics")]
let start = Instant::now();
let body_json = serde_json::to_value(body)?;
let root_hash = VmHash::from_json(&body_json)?;
let receipt = TreasuryReceipt {
schema_version: SCHEMA_VERSION.to_string(),
receipt_type: receipt_type.to_string(),
timestamp: Utc::now(),
scroll: "treasury".to_string(),
tags,
root_hash: root_hash.as_str().to_string(),
body: body_json,
};
// Ensure directory exists
let path = self.events_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
// Append to JSONL
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let json = serde_json::to_string(&receipt)?;
writeln!(file, "{}", json)?;
// Update ROOT file
self.update_root_file()?;
// Record metrics if observability is enabled
#[cfg(feature = "metrics")]
if let Some(ref obs) = self.observability {
let elapsed = start.elapsed().as_secs_f64();
obs.observe_emitted("treasury", elapsed);
}
Ok(())
}
/// Update ROOT.treasury.txt with current Merkle root
fn update_root_file(&self) -> Result<(), TreasuryError> {
let events_path = self.events_path();
if !events_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&events_path)?;
let hashes: Vec<VmHash> = content
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| VmHash::blake3(l.as_bytes()))
.collect();
let root = vaultmesh_core::merkle_root(&hashes);
let root_path = self.vaultmesh_root.join(Scroll::Treasury.root_file());
fs::write(&root_path, root.as_str())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_env() -> (TempDir, TreasuryEngine) {
let tmp = TempDir::new().unwrap();
let engine = TreasuryEngine::new(tmp.path());
(tmp, engine)
}
#[test]
fn test_create_budget() {
let (_tmp, mut engine) = setup_test_env();
let budget = engine.create_budget(
"ops-2025",
"Operations Budget 2025",
10000,
"did:vm:human:karol",
).unwrap();
assert_eq!(budget.id, "ops-2025");
assert_eq!(budget.allocated, 10000);
assert_eq!(budget.spent, 0);
assert_eq!(budget.remaining(), 10000);
}
#[test]
fn test_debit_reduces_balance() {
let (_tmp, mut engine) = setup_test_env();
engine.create_budget("test", "Test", 1000, "did:vm:human:test").unwrap();
let budget = engine.debit("test", 300, "Test expense", "did:vm:human:test").unwrap();
assert_eq!(budget.spent, 300);
assert_eq!(budget.remaining(), 700);
}
#[test]
fn test_debit_insufficient_funds() {
let (_tmp, mut engine) = setup_test_env();
engine.create_budget("small", "Small Budget", 100, "did:vm:human:test").unwrap();
let result = engine.debit("small", 500, "Too much", "did:vm:human:test");
assert!(matches!(result, Err(TreasuryError::InsufficientFunds { .. })));
}
#[test]
fn test_credit_increases_allocation() {
let (_tmp, mut engine) = setup_test_env();
engine.create_budget("grow", "Growing Budget", 500, "did:vm:human:test").unwrap();
let budget = engine.credit("grow", 200, "Additional funding", "did:vm:human:test").unwrap();
assert_eq!(budget.allocated, 700);
assert_eq!(budget.remaining(), 700);
}
#[test]
fn test_receipts_emitted() {
let (tmp, mut engine) = setup_test_env();
engine.create_budget("receipt-test", "Receipt Test", 1000, "did:vm:human:test").unwrap();
engine.debit("receipt-test", 100, "Test debit", "did:vm:human:test").unwrap();
let events_path = tmp.path().join("receipts/treasury/treasury_events.jsonl");
assert!(events_path.exists());
let content = fs::read_to_string(&events_path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2); // create + debit
assert!(lines[0].contains("treasury_budget_create"));
assert!(lines[1].contains("treasury_debit"));
}
}