newton-core 0.7.1

newton protocol core sdk
use alloy::primitives::Address;
use moka::future::Cache;
use std::time::Duration;

/// Configuration for the policy contract cache.
#[derive(Clone, Debug)]
pub struct PolicyContractCacheConfig {
    /// TTL for version caches (changes only on proxy upgrade).
    pub version_ttl_secs: u64,
    /// Max entries for each version cache.
    pub version_max_capacity: u64,
}

impl Default for PolicyContractCacheConfig {
    fn default() -> Self {
        Self {
            version_ttl_secs: 600,
            version_max_capacity: 512,
        }
    }
}

/// Shared cache for on-chain policy contract reads: `(chain_id, policy_address)` -> version
/// string, TTL-based. Services share this via `Arc<PolicyContractCache>`.
pub struct PolicyContractCache {
    policy_version: Cache<(u64, Address), String>,
}

impl std::fmt::Debug for PolicyContractCache {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PolicyContractCache")
            .field("policy_version_entry_count", &self.policy_version.entry_count())
            .finish()
    }
}

impl Default for PolicyContractCache {
    fn default() -> Self {
        Self::new(PolicyContractCacheConfig::default())
    }
}

impl PolicyContractCache {
    /// Create a new cache with the given configuration.
    pub fn new(config: PolicyContractCacheConfig) -> Self {
        Self {
            policy_version: Cache::builder()
                .time_to_live(Duration::from_secs(config.version_ttl_secs))
                .max_capacity(config.version_max_capacity)
                .build(),
        }
    }

    // -- Policy version cache (TTL-based) --

    /// Look up a cached policy implementation version.
    pub async fn get_policy_version(&self, chain_id: u64, policy_address: Address) -> Option<String> {
        self.policy_version.get(&(chain_id, policy_address)).await
    }

    /// Cache a policy implementation version.
    pub async fn insert_policy_version(&self, chain_id: u64, policy_address: Address, version: String) {
        self.policy_version.insert((chain_id, policy_address), version).await;
    }

    /// Get or populate a policy version, coalescing concurrent lookups.
    pub async fn get_or_try_insert_policy_version<F, E>(
        &self,
        chain_id: u64,
        policy_address: Address,
        init: F,
    ) -> Result<String, std::sync::Arc<E>>
    where
        F: std::future::Future<Output = Result<String, E>>,
        E: Send + Sync + 'static,
    {
        self.policy_version.try_get_with((chain_id, policy_address), init).await
    }

    // -- Invalidation --

    /// Remove a policy version entry (e.g., on proxy upgrade).
    pub async fn invalidate_policy_version(&self, chain_id: u64, policy_address: Address) {
        self.policy_version.invalidate(&(chain_id, policy_address)).await;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy::primitives::Address;
    use std::time::Duration;

    fn test_config() -> PolicyContractCacheConfig {
        PolicyContractCacheConfig {
            version_ttl_secs: 1,
            version_max_capacity: 10,
        }
    }

    #[tokio::test]
    async fn test_version_cache_miss_then_hit() {
        let cache = PolicyContractCache::new(test_config());
        let chain_id = 1u64;
        let addr = Address::repeat_byte(0x01);

        assert!(cache.get_policy_version(chain_id, addr).await.is_none());

        cache.insert_policy_version(chain_id, addr, "0.3.0".to_string()).await;

        let cached = cache.get_policy_version(chain_id, addr).await.unwrap();
        assert_eq!(cached, "0.3.0");
    }

    #[tokio::test]
    async fn test_version_ttl_expiry() {
        let cache = PolicyContractCache::new(test_config());
        let chain_id = 1u64;
        let addr = Address::repeat_byte(0x01);

        cache.insert_policy_version(chain_id, addr, "0.3.0".to_string()).await;
        assert!(cache.get_policy_version(chain_id, addr).await.is_some());

        tokio::time::sleep(Duration::from_millis(1100)).await;

        assert!(cache.get_policy_version(chain_id, addr).await.is_none());
    }

    #[tokio::test]
    async fn test_default_config() {
        let config = PolicyContractCacheConfig::default();
        assert_eq!(config.version_ttl_secs, 600);
        assert_eq!(config.version_max_capacity, 512);
    }
}