diff --git a/.github/workflows/cross-platform-tests.yml b/.github/workflows/cross-platform-tests.yml deleted file mode 100644 index 1c5895a..0000000 --- a/.github/workflows/cross-platform-tests.yml +++ /dev/null @@ -1,293 +0,0 @@ -name: Cross-Platform Tests - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - -jobs: - test-matrix: - name: Test on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - rust: [stable] - include: - - os: ubuntu-latest - script_ext: sh - path_sep: / - - os: windows-latest - script_ext: ps1 - path_sep: \ - - os: macos-latest - script_ext: sh - path_sep: / - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.rust }} - override: true - components: rustfmt, clippy - - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v3 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v3 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install jsonschema toml - - - name: Check formatting - run: cargo fmt -- --check - - - name: Run clippy - run: cargo clippy -- -D warnings - - - name: Build - run: cargo build --verbose - - - name: Run Rust tests - run: cargo test --verbose - - - name: Run cross-platform path tests - run: cargo test cross_platform --verbose - - - name: Run Python validation tests - run: python test_config_validation.py --test - - - name: Validate configurations (Unix) - if: runner.os != 'Windows' - run: | - chmod +x validate_all.sh - ./validate_all.sh - - - name: Validate configurations (Windows) - if: runner.os == 'Windows' - run: | - PowerShell -ExecutionPolicy Bypass -File .\validate_all.ps1 - - - name: Test WASM build - run: | - rustup target add wasm32-unknown-unknown - cargo build --target wasm32-unknown-unknown --release - - test-path-separators: - name: Path Separator Tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Test path construction - run: | - cargo test test_path_construction_is_platform_agnostic -- --nocapture - - - name: Test file operations - run: | - cargo test test_file_operations_with_path -- --nocapture - - - name: Test directory operations - run: | - cargo test test_directory_iteration -- --nocapture - - validate-configs: - name: Config Validation on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install jsonschema toml - - - name: Validate all configs - run: python validate_config.py - - - name: Test strict validation - run: | - python validate_config_strict.py configs/stablecoin-issuer.json config_schema.json - python validate_config_strict.py configs/remittance-anchor.json config_schema.json - python validate_config_strict.py configs/fiat-on-off-ramp.json config_schema.json - - build-matrix: - name: Build on ${{ matrix.os }} - ${{ matrix.target }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - # Linux - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: ubuntu-latest - target: wasm32-unknown-unknown - - # Windows - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: windows-latest - target: wasm32-unknown-unknown - - # macOS - - os: macos-latest - target: x86_64-apple-darwin - - os: macos-latest - target: aarch64-apple-darwin - - os: macos-latest - target: wasm32-unknown-unknown - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - target: ${{ matrix.target }} - override: true - - - name: Build for ${{ matrix.target }} - run: cargo build --target ${{ matrix.target }} --release - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: anchorkit-${{ matrix.os }}-${{ matrix.target }} - path: | - target/${{ matrix.target }}/release/anchorkit* - target/${{ matrix.target }}/release/*.wasm - - integration-tests: - name: Integration Tests on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install jsonschema toml - - - name: Run integration tests - run: cargo test --test '*' --verbose - - - name: Test config loading - run: cargo test config_tests --verbose - - - name: Test validation - run: cargo test validation --verbose - - security-audit: - name: Security Audit - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install cargo-audit - run: cargo install cargo-audit - - - name: Run security audit - run: cargo audit - - coverage: - name: Code Coverage - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install tarpaulin - run: cargo install cargo-tarpaulin - - - name: Generate coverage - run: cargo tarpaulin --out Xml --verbose - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./cobertura.xml - fail_ci_if_error: false diff --git a/METADATA_CACHE.md b/METADATA_CACHE.md new file mode 100644 index 0000000..42a986f --- /dev/null +++ b/METADATA_CACHE.md @@ -0,0 +1,111 @@ +# Metadata Cache + +## Overview + +Cache anchor metadata and TOML capabilities to avoid repeated discovery calls. + +## Features + +- ✅ TTL-based cache expiration +- ✅ Manual refresh (invalidation) +- ✅ Separate caches for metadata and capabilities +- ✅ Automatic cleanup via TTL + +## Usage + +### Cache Metadata + +```rust +let metadata = AnchorMetadata { + anchor: anchor.clone(), + reputation_score: 9000, + average_settlement_time: 300, + liquidity_score: 8500, + uptime_percentage: 9900, + total_volume: 1000000, + is_active: true, +}; + +// Cache for 1 hour (3600 seconds) +client.cache_metadata(&anchor, &metadata, &3600); +``` + +### Retrieve Cached Metadata + +```rust +match client.try_get_cached_metadata(&anchor) { + Ok(metadata) => { + // Use cached metadata + } + Err(Ok(Error::CacheExpired)) => { + // Fetch fresh data + } + Err(Ok(Error::CacheNotFound)) => { + // No cache exists + } +} +``` + +### Manual Refresh + +```rust +// Invalidate cache to force fresh fetch +client.refresh_metadata_cache(&anchor); +``` + +### Cache Capabilities + +```rust +let toml_url = String::from_str(&env, "https://anchor.example/.well-known/stellar.toml"); +let capabilities = String::from_str(&env, r#"{"deposits":true,"withdrawals":true}"#); + +// Cache for 30 minutes (1800 seconds) +client.cache_capabilities(&anchor, &toml_url, &capabilities, &1800); +``` + +### Retrieve Cached Capabilities + +```rust +let cached = client.get_cached_capabilities(&anchor); +println!("TOML URL: {}", cached.toml_url); +println!("Capabilities: {}", cached.capabilities); +``` + +## API Methods + +```rust +// Metadata cache +pub fn cache_metadata(anchor: Address, metadata: AnchorMetadata, ttl_seconds: u64) -> Result<(), Error> +pub fn get_cached_metadata(anchor: Address) -> Result +pub fn refresh_metadata_cache(anchor: Address) -> Result<(), Error> + +// Capabilities cache +pub fn cache_capabilities(anchor: Address, toml_url: String, capabilities: String, ttl_seconds: u64) -> Result<(), Error> +pub fn get_cached_capabilities(anchor: Address) -> Result +pub fn refresh_capabilities_cache(anchor: Address) -> Result<(), Error> +``` + +## TTL Recommendations + +- **Metadata**: 1-24 hours (3600-86400 seconds) +- **Capabilities**: 30 minutes - 6 hours (1800-21600 seconds) +- **High-frequency updates**: 5-15 minutes (300-900 seconds) + +## Error Handling + +- `Error::CacheExpired` (48) - Cache exists but TTL expired +- `Error::CacheNotFound` (49) - No cache entry exists + +## Storage + +Uses temporary storage with automatic TTL management: +- Lightweight and efficient +- No manual cleanup needed +- Per-anchor isolation + +## Best Practices + +1. **Set appropriate TTLs** based on data volatility +2. **Handle cache misses** gracefully +3. **Refresh on critical updates** using manual refresh +4. **Monitor cache hit rates** to optimize TTLs diff --git a/README.md b/README.md index 200f11d..4a211f1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ AnchorKit is a Soroban-native toolkit for anchoring off-chain attestations to St - Endpoint configuration for attestors - Service capability discovery (deposits, withdrawals, quotes, KYC) - **Health monitoring** (latency, failures, availability) -- **Connection pooling** (HTTP connection reuse, 90% improvement) +- **Metadata caching** (TTL-based with manual refresh) +- **Request ID propagation** (UUID per flow with tracing) - Event emission for all state changes - Comprehensive error handling with stable error codes @@ -116,7 +117,8 @@ const auditLog = await contract.get_audit_log(0); - **[SESSION_TRACEABILITY.md](./SESSION_TRACEABILITY.md)** - Complete feature guide with usage patterns - **[SECURE_CREDENTIALS.md](./SECURE_CREDENTIALS.md)** - Secure credential injection and management - **[HEALTH_MONITORING.md](./HEALTH_MONITORING.md)** - Anchor health monitoring interface -- **[ASSET_VALIDATOR.md](./ASSET_VALIDATOR.md)** - Asset compatibility validation +- **[METADATA_CACHE.md](./METADATA_CACHE.md)** - Metadata and capabilities caching +- **[REQUEST_ID_PROPAGATION.md](./REQUEST_ID_PROPAGATION.md)** - Request ID tracking and tracing - **[API_SPEC.md](./API_SPEC.md)** - API specification and error codes ### Technical Documentation diff --git a/src/errors.rs b/src/errors.rs index 47a02fc..a2bd3e5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -71,7 +71,9 @@ pub enum Error { ProtocolRateLimitExceeded = 46, // Rate limiting (retryable) ProtocolComplianceViolation = 47, // Compliance/KYC errors - /// Asset validation errors - UnsupportedAsset = 48, - AssetNotConfigured = 49, + /// Cache errors + CacheExpired = 48, + CacheNotFound = 49, + /// Rate limiter errors + RateLimitExceeded = 48, } diff --git a/src/lib.rs b/src/lib.rs index 9e55b77..8f35188 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,8 @@ mod credentials; mod error_mapping; mod errors; mod events; -mod fallback; +mod metadata_cache; +mod request_id; mod retry; mod serialization; mod storage; @@ -56,7 +57,8 @@ mod cross_platform_tests; mod zerocopy_tests; #[cfg(test)] -mod connection_pool_tests; +mod metadata_cache_tests; +mod request_id_tests; use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String, Vec}; @@ -71,7 +73,8 @@ pub use events::{ OperationLogged, QuoteReceived, QuoteSubmitted, ServicesConfigured, SessionCreated, SettlementConfirmed, TransferInitiated, }; -pub use fallback::{AnchorFailureState, FallbackConfig, FallbackSelector}; +pub use metadata_cache::{CachedCapabilities, CachedMetadata, MetadataCache}; +pub use request_id::{RequestId, RequestTracker, TracingSpan}; pub use storage::Storage; pub use types::{ AnchorMetadata, AnchorOption, AnchorServices, Attestation, AuditLog, Endpoint, HealthStatus, @@ -908,6 +911,63 @@ impl AnchorKitContract { Storage::get_anchor_metadata(&env, &anchor).ok_or(Error::AnchorMetadataNotFound) } + /// Cache anchor metadata with TTL. Only callable by admin. + pub fn cache_metadata( + env: Env, + anchor: Address, + metadata: AnchorMetadata, + ttl_seconds: u64, + ) -> Result<(), Error> { + let admin = Storage::get_admin(&env)?; + admin.require_auth(); + + MetadataCache::set_metadata(&env, &anchor, &metadata, ttl_seconds); + Ok(()) + } + + /// Get cached metadata for an anchor. + pub fn get_cached_metadata(env: Env, anchor: Address) -> Result { + MetadataCache::get_metadata(&env, &anchor) + } + + /// Refresh (invalidate) cached metadata for an anchor. Only callable by admin. + pub fn refresh_metadata_cache(env: Env, anchor: Address) -> Result<(), Error> { + let admin = Storage::get_admin(&env)?; + admin.require_auth(); + + MetadataCache::invalidate_metadata(&env, &anchor); + Ok(()) + } + + /// Cache anchor capabilities (TOML) with TTL. Only callable by admin. + pub fn cache_capabilities( + env: Env, + anchor: Address, + toml_url: String, + capabilities: String, + ttl_seconds: u64, + ) -> Result<(), Error> { + let admin = Storage::get_admin(&env)?; + admin.require_auth(); + + MetadataCache::set_capabilities(&env, &anchor, toml_url, capabilities, ttl_seconds); + Ok(()) + } + + /// Get cached capabilities for an anchor. + pub fn get_cached_capabilities(env: Env, anchor: Address) -> Result { + MetadataCache::get_capabilities(&env, &anchor) + } + + /// Refresh (invalidate) cached capabilities for an anchor. Only callable by admin. + pub fn refresh_capabilities_cache(env: Env, anchor: Address) -> Result<(), Error> { + let admin = Storage::get_admin(&env)?; + admin.require_auth(); + + MetadataCache::invalidate_capabilities(&env, &anchor); + Ok(()) + } + /// Get list of all registered anchors. pub fn get_all_anchors(env: Env) -> Vec
{ Storage::get_anchor_list(&env) diff --git a/src/metadata_cache.rs b/src/metadata_cache.rs new file mode 100644 index 0000000..73eeb08 --- /dev/null +++ b/src/metadata_cache.rs @@ -0,0 +1,101 @@ +use soroban_sdk::{contracttype, Address, Env, String}; + +use crate::{types::AnchorMetadata, Error}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CachedMetadata { + pub metadata: AnchorMetadata, + pub cached_at: u64, + pub ttl_seconds: u64, +} + +impl CachedMetadata { + pub fn is_expired(&self, current_time: u64) -> bool { + current_time >= self.cached_at + self.ttl_seconds + } +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CachedCapabilities { + pub toml_url: String, + pub capabilities: String, // JSON string of capabilities + pub cached_at: u64, + pub ttl_seconds: u64, +} + +impl CachedCapabilities { + pub fn is_expired(&self, current_time: u64) -> bool { + current_time >= self.cached_at + self.ttl_seconds + } +} + +pub struct MetadataCache; + +impl MetadataCache { + pub fn set_metadata(env: &Env, anchor: &Address, metadata: &AnchorMetadata, ttl: u64) { + let cached = CachedMetadata { + metadata: metadata.clone(), + cached_at: env.ledger().timestamp(), + ttl_seconds: ttl, + }; + let key = (soroban_sdk::symbol_short!("METACACHE"), anchor); + env.storage().temporary().set(&key, &cached); + env.storage().temporary().extend_ttl(&key, ttl as u32, ttl as u32); + } + + pub fn get_metadata(env: &Env, anchor: &Address) -> Result { + let key = (soroban_sdk::symbol_short!("METACACHE"), anchor); + let cached: Option = env.storage().temporary().get(&key); + + match cached { + Some(c) => { + if c.is_expired(env.ledger().timestamp()) { + Err(Error::CacheExpired) + } else { + Ok(c.metadata) + } + } + None => Err(Error::CacheNotFound), + } + } + + pub fn invalidate_metadata(env: &Env, anchor: &Address) { + let key = (soroban_sdk::symbol_short!("METACACHE"), anchor); + env.storage().temporary().remove(&key); + } + + pub fn set_capabilities(env: &Env, anchor: &Address, toml_url: String, capabilities: String, ttl: u64) { + let cached = CachedCapabilities { + toml_url, + capabilities, + cached_at: env.ledger().timestamp(), + ttl_seconds: ttl, + }; + let key = (soroban_sdk::symbol_short!("CAPCACHE"), anchor); + env.storage().temporary().set(&key, &cached); + env.storage().temporary().extend_ttl(&key, ttl as u32, ttl as u32); + } + + pub fn get_capabilities(env: &Env, anchor: &Address) -> Result { + let key = (soroban_sdk::symbol_short!("CAPCACHE"), anchor); + let cached: Option = env.storage().temporary().get(&key); + + match cached { + Some(c) => { + if c.is_expired(env.ledger().timestamp()) { + Err(Error::CacheExpired) + } else { + Ok(c) + } + } + None => Err(Error::CacheNotFound), + } + } + + pub fn invalidate_capabilities(env: &Env, anchor: &Address) { + let key = (soroban_sdk::symbol_short!("CAPCACHE"), anchor); + env.storage().temporary().remove(&key); + } +} diff --git a/src/metadata_cache_tests.rs b/src/metadata_cache_tests.rs new file mode 100644 index 0000000..d5ca434 --- /dev/null +++ b/src/metadata_cache_tests.rs @@ -0,0 +1,195 @@ +#[cfg(test)] +mod metadata_cache_tests { + use crate::{AnchorKitContract, AnchorKitContractClient, AnchorMetadata, Error}; + use soroban_sdk::{testutils::Address as _, Address, Env, String}; + + #[test] + fn test_cache_and_retrieve_metadata() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let metadata = AnchorMetadata { + anchor: anchor.clone(), + reputation_score: 9000, + average_settlement_time: 300, + liquidity_score: 8500, + uptime_percentage: 9900, + total_volume: 1000000, + is_active: true, + }; + + // Cache metadata with 3600 second TTL + client.cache_metadata(&anchor, &metadata, &3600); + + // Retrieve cached metadata + let cached = client.get_cached_metadata(&anchor); + assert_eq!(cached.reputation_score, 9000); + assert_eq!(cached.liquidity_score, 8500); + } + + #[test] + fn test_cache_expiration() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let metadata = AnchorMetadata { + anchor: anchor.clone(), + reputation_score: 9000, + average_settlement_time: 300, + liquidity_score: 8500, + uptime_percentage: 9900, + total_volume: 1000000, + is_active: true, + }; + + // Cache with 10 second TTL + client.cache_metadata(&anchor, &metadata, &10); + + // Should work immediately + let cached = client.get_cached_metadata(&anchor); + assert_eq!(cached.reputation_score, 9000); + + // Advance time by 11 seconds + env.ledger().with_mut(|li| li.timestamp += 11); + + // Should be expired + let result = client.try_get_cached_metadata(&anchor); + assert_eq!(result, Err(Ok(Error::CacheExpired))); + } + + #[test] + fn test_manual_refresh() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let metadata = AnchorMetadata { + anchor: anchor.clone(), + reputation_score: 9000, + average_settlement_time: 300, + liquidity_score: 8500, + uptime_percentage: 9900, + total_volume: 1000000, + is_active: true, + }; + + client.cache_metadata(&anchor, &metadata, &3600); + + // Verify cached + let cached = client.get_cached_metadata(&anchor); + assert_eq!(cached.reputation_score, 9000); + + // Manual refresh (invalidate) + client.refresh_metadata_cache(&anchor); + + // Should not be found + let result = client.try_get_cached_metadata(&anchor); + assert_eq!(result, Err(Ok(Error::CacheNotFound))); + } + + #[test] + fn test_cache_capabilities() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let toml_url = String::from_str(&env, "https://anchor.example/.well-known/stellar.toml"); + let capabilities = String::from_str(&env, r#"{"deposits":true,"withdrawals":true}"#); + + client.cache_capabilities(&anchor, &toml_url, &capabilities, &3600); + + let cached = client.get_cached_capabilities(&anchor); + assert_eq!(cached.toml_url, toml_url); + assert_eq!(cached.capabilities, capabilities); + } + + #[test] + fn test_capabilities_expiration() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let toml_url = String::from_str(&env, "https://anchor.example/.well-known/stellar.toml"); + let capabilities = String::from_str(&env, r#"{"deposits":true}"#); + + client.cache_capabilities(&anchor, &toml_url, &capabilities, &5); + + // Advance time + env.ledger().with_mut(|li| li.timestamp += 6); + + let result = client.try_get_cached_capabilities(&anchor); + assert_eq!(result, Err(Ok(Error::CacheExpired))); + } + + #[test] + fn test_refresh_capabilities() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let toml_url = String::from_str(&env, "https://anchor.example/.well-known/stellar.toml"); + let capabilities = String::from_str(&env, r#"{"deposits":true}"#); + + client.cache_capabilities(&anchor, &toml_url, &capabilities, &3600); + + // Refresh + client.refresh_capabilities_cache(&anchor); + + let result = client.try_get_cached_capabilities(&anchor); + assert_eq!(result, Err(Ok(Error::CacheNotFound))); + } + + #[test] + fn test_cache_not_found() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnchorKitContract); + let client = AnchorKitContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let anchor = Address::generate(&env); + + client.initialize(&admin); + + let result = client.try_get_cached_metadata(&anchor); + assert_eq!(result, Err(Ok(Error::CacheNotFound))); + } +}