spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"""Immutable, hash-chained audit trail. Every risk decision, mode change, order and kill-switch flip goes here."""23from __future__ import annotations45import hashlib6import json7from typing import Any89import asyncpg1011from ktrade.logging import correlation_id121314def _hash(prev: str | None, actor: str, action: str, entity: str | None, details: dict[str, Any]) -> str:15 payload = json.dumps({"p": prev, "a": actor, "x": action, "e": entity, "d": details}, sort_keys=True, default=str)16 return hashlib.sha256(payload.encode()).hexdigest()171819async def audit(pool: asyncpg.Pool, actor: str, action: str, entity: str | None = None, details: dict[str, Any] | None = None) -> int:20 details = details or {}21 async with pool.acquire() as conn, conn.transaction():22 prev = await conn.fetchval("SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1 FOR UPDATE")23 h = _hash(prev, actor, action, entity, details)24 return await conn.fetchval(25 "INSERT INTO audit_log (actor, action, entity, details, cid, prev_hash, hash) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id",26 actor, action, entity, json.loads(json.dumps(details, default=str)), correlation_id.get(), prev, h,27 )282930async def verify_chain(pool: asyncpg.Pool, limit: int = 10000) -> tuple[bool, int]:31 rows = await pool.fetch("SELECT actor, action, entity, details, prev_hash, hash FROM audit_log ORDER BY id ASC LIMIT $1", limit)32 prev = None33 for i, r in enumerate(rows):34 if r["prev_hash"] != prev or _hash(prev, r["actor"], r["action"], r["entity"], r["details"]) != r["hash"]:35 return False, i36 prev = r["hash"]37 return True, len(rows)38