use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::error::{AptosError, AptosResult};
use crate::types::AptosSealRef;
use csv_adapter_core::hardening::{BoundedQueue, MAX_SEAL_REGISTRY_SIZE};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SealRecord {
pub account_address: [u8; 32],
pub resource_type: String,
pub nonce: u64,
pub consumed_at_version: u64,
pub consumed_at: i64,
}
impl SealRecord {
pub fn new(seal: &AptosSealRef, consumed_at_version: u64) -> Self {
Self {
account_address: seal.account_address,
resource_type: seal.resource_type.clone(),
nonce: seal.nonce,
consumed_at_version,
consumed_at: chrono::Utc::now().timestamp(),
}
}
pub fn key(&self) -> String {
format!(
"{}-{}-{}",
hex::encode(self.account_address),
self.resource_type,
self.nonce
)
}
}
#[derive(Debug)]
pub struct SealRegistry {
used_seals: HashSet<String>,
seal_records: Vec<SealRecord>,
seal_queue: BoundedQueue<String>,
max_size: usize,
}
impl SealRegistry {
pub fn new() -> Self {
Self::with_max_size(MAX_SEAL_REGISTRY_SIZE)
}
pub fn with_max_size(max_size: usize) -> Self {
Self {
used_seals: HashSet::new(),
seal_records: Vec::new(),
seal_queue: BoundedQueue::new(max_size),
max_size,
}
}
pub fn is_seal_used(&self, seal: &AptosSealRef) -> bool {
let key = format!(
"{}-{}-{}",
hex::encode(seal.account_address),
seal.resource_type,
seal.nonce
);
self.used_seals.contains(&key)
}
pub fn mark_seal_used(
&mut self,
seal: &AptosSealRef,
consumed_at_version: u64,
) -> AptosResult<()> {
if self.is_seal_used(seal) {
return Err(AptosError::ResourceUsed(format!(
"Seal at address 0x{} with resource type '{}' is already consumed",
hex::encode(seal.account_address),
seal.resource_type
)));
}
let record = SealRecord::new(seal, consumed_at_version);
let key = record.key();
self.seal_queue.push(key.clone());
self.used_seals.insert(key);
self.seal_records.push(record);
Ok(())
}
pub fn clear_seal(&mut self, seal: &AptosSealRef) -> AptosResult<()> {
let key = format!(
"{}-{}-{}",
hex::encode(seal.account_address),
seal.resource_type,
seal.nonce
);
if !self.used_seals.remove(&key) {
return Err(AptosError::ResourceUsed(format!(
"Seal at address 0x{} not found in registry",
hex::encode(seal.account_address)
)));
}
self.seal_records.retain(|r| r.key() != key);
Ok(())
}
pub fn len(&self) -> usize {
self.used_seals.len()
}
pub fn is_empty(&self) -> bool {
self.used_seals.is_empty()
}
pub fn is_full(&self) -> bool {
self.used_seals.len() >= self.max_size
}
pub fn max_size(&self) -> usize {
self.max_size
}
pub fn mark_seal_used_with_limit(
&mut self,
seal: &AptosSealRef,
consumed_at_version: u64,
) -> AptosResult<()> {
if self.is_seal_used(seal) {
return Err(AptosError::ResourceUsed(format!(
"Seal at address 0x{} with resource type '{}' is already consumed",
hex::encode(seal.account_address),
seal.resource_type
)));
}
if self.used_seals.len() >= self.max_size {
return Err(AptosError::ResourceUsed(format!(
"Seal registry is full (max {} entries)",
self.max_size
)));
}
let record = SealRecord::new(seal, consumed_at_version);
let key = record.key();
self.seal_queue.push(key.clone());
self.used_seals.insert(key);
self.seal_records.push(record);
Ok(())
}
pub fn records(&self) -> &[SealRecord] {
&self.seal_records
}
#[cfg(test)]
pub fn clear(&mut self) {
self.used_seals.clear();
self.seal_records.clear();
}
pub fn export_records(&self) -> Vec<SealRecord> {
self.seal_records.clone()
}
pub fn import_records(&mut self, records: Vec<SealRecord>) {
for record in &records {
self.used_seals.insert(record.key());
}
self.seal_records = records;
}
}
impl Default for SealRegistry {
fn default() -> Self {
Self::new()
}
}
pub trait SealStore: Send + Sync {
fn load_seals(&self) -> Result<Vec<SealRecord>, Box<dyn std::error::Error + Send + Sync>>;
fn save_seals(
&self,
records: &[SealRecord],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
#[cfg(test)]
mod tests {
use super::*;
fn test_seal() -> AptosSealRef {
AptosSealRef::new([1u8; 32], "CSV::Seal".to_string(), 0)
}
#[test]
fn test_new_registry_is_empty() {
let registry = SealRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_mark_seal_used() {
let mut registry = SealRegistry::new();
let seal = test_seal();
assert!(!registry.is_seal_used(&seal));
registry.mark_seal_used(&seal, 100).unwrap();
assert!(registry.is_seal_used(&seal));
assert_eq!(registry.len(), 1);
}
#[test]
fn test_mark_seal_used_replay_prevention() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
let result = registry.mark_seal_used(&seal, 200);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AptosError::ResourceUsed(_)));
}
#[test]
fn test_different_seals() {
let mut registry = SealRegistry::new();
let seal1 = AptosSealRef::new([1u8; 32], "CSV::Seal".to_string(), 0);
let seal2 = AptosSealRef::new([2u8; 32], "CSV::Seal".to_string(), 0);
registry.mark_seal_used(&seal1, 100).unwrap();
registry.mark_seal_used(&seal2, 200).unwrap();
assert!(registry.is_seal_used(&seal1));
assert!(registry.is_seal_used(&seal2));
assert_eq!(registry.len(), 2);
}
#[test]
fn test_same_seal_different_nonce() {
let mut registry = SealRegistry::new();
let seal1 = AptosSealRef::new([1u8; 32], "CSV::Seal".to_string(), 0);
let seal2 = AptosSealRef::new([1u8; 32], "CSV::Seal".to_string(), 1);
registry.mark_seal_used(&seal1, 100).unwrap();
registry.mark_seal_used(&seal2, 200).unwrap();
assert!(registry.is_seal_used(&seal1));
assert!(registry.is_seal_used(&seal2));
assert_eq!(registry.len(), 2);
}
#[test]
fn test_export_import() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
let records = registry.export_records();
let mut new_registry = SealRegistry::new();
new_registry.import_records(records);
assert!(new_registry.is_seal_used(&seal));
assert_eq!(new_registry.len(), 1);
}
#[test]
fn test_seal_record_serialization() {
let seal = test_seal();
let record = SealRecord::new(&seal, 100);
let json = serde_json::to_string(&record).unwrap();
let deserialized: SealRecord = serde_json::from_str(&json).unwrap();
assert_eq!(record, deserialized);
}
#[test]
fn test_clear_registry() {
let mut registry = SealRegistry::new();
let seal = test_seal();
registry.mark_seal_used(&seal, 100).unwrap();
registry.clear();
assert!(!registry.is_seal_used(&seal));
assert!(registry.is_empty());
}
}