diff --git a/aitbc/__init__.py b/aitbc/__init__.py index db7c021b..f60ae6fb 100644 --- a/aitbc/__init__.py +++ b/aitbc/__init__.py @@ -108,6 +108,7 @@ from .monitoring import ( PerformanceTimer, HealthChecker, ) +from .data_layer import DataLayer, MockDataGenerator, RealDataFetcher, get_data_layer from .crypto import ( derive_ethereum_address, sign_transaction_hash, @@ -344,6 +345,11 @@ __all__ = [ "vacuum_database", "get_table_info", "table_exists", + # Data layer + "DataLayer", + "MockDataGenerator", + "RealDataFetcher", + "get_data_layer", # Monitoring "MetricsCollector", "PerformanceTimer", diff --git a/aitbc/data_layer.py b/aitbc/data_layer.py new file mode 100644 index 00000000..92d86643 --- /dev/null +++ b/aitbc/data_layer.py @@ -0,0 +1,271 @@ +""" +Data layer abstraction for AITBC +Provides toggle between mock and real data sources for development/testing +""" + +import os +from typing import Any, Dict, List, Optional +from datetime import datetime +import httpx + + +class DataLayer: + """Data layer abstraction that can switch between mock and real data sources""" + + def __init__(self, use_mock_data: Optional[bool] = None): + """Initialize data layer + + Args: + use_mock_data: Force mock mode. If None, uses USE_MOCK_DATA env var + """ + if use_mock_data is None: + self.use_mock_data = os.getenv("USE_MOCK_DATA", "false").lower() == "true" + else: + self.use_mock_data = use_mock_data + + self.mock_generator = MockDataGenerator() + self.real_fetcher = RealDataFetcher() + + async def get_transactions( + self, + address: Optional[str] = None, + amount_min: Optional[float] = None, + amount_max: Optional[float] = None, + tx_type: Optional[str] = None, + since: Optional[str] = None, + until: Optional[str] = None, + limit: int = 50, + offset: int = 0, + chain_id: str = "ait-devnet", + rpc_url: Optional[str] = None + ) -> List[Dict[str, Any]]: + """Get transactions from either mock or real data source""" + if self.use_mock_data: + return self.mock_generator.generate_transactions( + address, amount_min, amount_max, tx_type, limit + ) + else: + return await self.real_fetcher.fetch_transactions( + address, amount_min, amount_max, tx_type, since, until, + limit, offset, chain_id, rpc_url + ) + + async def get_blocks( + self, + validator: Optional[str] = None, + since: Optional[str] = None, + until: Optional[str] = None, + min_tx: Optional[int] = None, + limit: int = 50, + offset: int = 0, + chain_id: str = "ait-devnet", + rpc_url: Optional[str] = None + ) -> List[Dict[str, Any]]: + """Get blocks from either mock or real data source""" + if self.use_mock_data: + return self.mock_generator.generate_blocks(validator, min_tx, limit) + else: + return await self.real_fetcher.fetch_blocks( + validator, since, until, min_tx, limit, offset, chain_id, rpc_url + ) + + async def get_analytics_overview(self, period: str = "24h", rpc_url: Optional[str] = None) -> Dict[str, Any]: + """Get analytics overview from either mock or real data source""" + if self.use_mock_data: + return self.mock_generator.generate_analytics(period) + else: + return await self.real_fetcher.fetch_analytics(period, rpc_url) + + +class MockDataGenerator: + """Generates mock data for development/testing when mock mode is enabled""" + + def generate_transactions( + self, + address: Optional[str] = None, + amount_min: Optional[float] = None, + amount_max: Optional[float] = None, + tx_type: Optional[str] = None, + limit: int = 50 + ) -> List[Dict[str, Any]]: + """Generate mock transaction data""" + from aitbc.testing import MockFactory, TestDataGenerator + + transactions = [] + for _ in range(limit): + tx = TestDataGenerator.generate_transaction_data( + from_address=address or MockFactory.generate_ethereum_address(), + to_address=MockFactory.generate_ethereum_address() + ) + if tx_type: + tx["type"] = tx_type + transactions.append(tx) + + return transactions + + def generate_blocks( + self, + validator: Optional[str] = None, + min_tx: Optional[int] = None, + limit: int = 50 + ) -> List[Dict[str, Any]]: + """Generate mock block data""" + from aitbc.testing import MockFactory + + blocks = [] + for i in range(limit): + blocks.append({ + "height": 10000 + i, + "hash": MockFactory.generate_hash(), + "validator": validator or MockFactory.generate_ethereum_address(), + "tx_count": min_tx or 5, + "timestamp": datetime.utcnow().isoformat() + }) + + return blocks + + def generate_analytics(self, period: str = "24h") -> Dict[str, Any]: + """Generate mock analytics data""" + if period == "1h": + labels = [f"{i:02d}:{(i*5)%60:02d}" for i in range(12)] + volume_values = [10 + i * 2 for i in range(12)] + activity_values = [5 + i for i in range(12)] + elif period == "24h": + labels = [f"{i:02d}:00" for i in range(0, 24, 2)] + volume_values = [50 + i * 5 for i in range(12)] + activity_values = [20 + i * 3 for i in range(12)] + elif period == "7d": + labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + volume_values = [500, 600, 550, 700, 800, 650, 750] + activity_values = [200, 250, 220, 300, 350, 280, 320] + else: # 30d + labels = [f"Week {i+1}" for i in range(4)] + volume_values = [3000, 3500, 3200, 3800] + activity_values = [1200, 1400, 1300, 1500] + + return { + "total_transactions": "1,234", + "transaction_volume": "5,678.90 AITBC", + "active_addresses": "89", + "avg_block_time": "2.1s", + "volume_data": { + "labels": labels, + "values": volume_values + }, + "activity_data": { + "labels": labels, + "values": activity_values + } + } + + +class RealDataFetcher: + """Fetches real data from blockchain RPC endpoints""" + + async def fetch_transactions( + self, + address: Optional[str] = None, + amount_min: Optional[float] = None, + amount_max: Optional[float] = None, + tx_type: Optional[str] = None, + since: Optional[str] = None, + until: Optional[str] = None, + limit: int = 50, + offset: int = 0, + chain_id: str = "ait-devnet", + rpc_url: Optional[str] = None + ) -> List[Dict[str, Any]]: + """Fetch real transactions from blockchain RPC""" + if rpc_url is None: + rpc_url = f"http://localhost:8025" + + params = {} + if address: + params["address"] = address + if amount_min: + params["amount_min"] = amount_min + if amount_max: + params["amount_max"] = amount_max + if tx_type: + params["type"] = tx_type + if since: + params["since"] = since + if until: + params["until"] = until + params["limit"] = limit + params["offset"] = offset + params["chain_id"] = chain_id + + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/search/transactions", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + return [] + else: + raise Exception(f"Failed to fetch transactions: {response.status_code}") + + async def fetch_blocks( + self, + validator: Optional[str] = None, + since: Optional[str] = None, + until: Optional[str] = None, + min_tx: Optional[int] = None, + limit: int = 50, + offset: int = 0, + chain_id: str = "ait-devnet", + rpc_url: Optional[str] = None + ) -> List[Dict[str, Any]]: + """Fetch real blocks from blockchain RPC""" + if rpc_url is None: + rpc_url = f"http://localhost:8025" + + params = {} + if validator: + params["validator"] = validator + if since: + params["since"] = since + if until: + params["until"] = until + if min_tx: + params["min_tx"] = min_tx + params["limit"] = limit + params["offset"] = offset + params["chain_id"] = chain_id + + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/search/blocks", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + return [] + else: + raise Exception(f"Failed to fetch blocks: {response.status_code}") + + async def fetch_analytics(self, period: str = "24h", rpc_url: Optional[str] = None) -> Dict[str, Any]: + """Fetch real analytics from blockchain RPC""" + if rpc_url is None: + rpc_url = f"http://localhost:8025" + + params = {"period": period} + + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/analytics/overview", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + raise Exception("Analytics endpoint not available") + else: + raise Exception(f"Failed to fetch analytics: {response.status_code}") + + +# Global data layer instance +_data_layer: Optional[DataLayer] = None + + +def get_data_layer(use_mock_data: Optional[bool] = None) -> DataLayer: + """Get or create global data layer instance""" + global _data_layer + if _data_layer is None: + _data_layer = DataLayer(use_mock_data) + return _data_layer diff --git a/apps/blockchain-explorer/main.py b/apps/blockchain-explorer/main.py index 4ab89ab0..086e54ac 100755 --- a/apps/blockchain-explorer/main.py +++ b/apps/blockchain-explorer/main.py @@ -4,20 +4,28 @@ AITBC Blockchain Explorer - Enhanced Version Advanced web interface with search, analytics, and export capabilities """ -import asyncio import httpx import json import csv import io import re from datetime import datetime, timedelta -from typing import Dict, List, Optional, Any, Union -from fastapi import FastAPI, HTTPException, Request, Query, Response -from fastapi.responses import HTMLResponse, StreamingResponse -from fastapi.staticfiles import StaticFiles +from typing import Optional +import os + +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field import uvicorn +# Import data layer for toggle between mock and real data +try: + from aitbc import get_data_layer + USE_DATA_LAYER = True +except ImportError: + USE_DATA_LAYER = False + app = FastAPI(title="AITBC Blockchain Explorer", version="0.1.0") # Validation patterns for user inputs to prevent SSRF @@ -1025,36 +1033,46 @@ async def search_transactions( ): """Advanced transaction search""" try: - # Build query parameters for blockchain node - params = {} - if address: - params["address"] = address - if amount_min: - params["amount_min"] = amount_min - if amount_max: - params["amount_max"] = amount_max - if tx_type: - params["type"] = tx_type - if since: - params["since"] = since - if until: - params["until"] = until - params["limit"] = limit - params["offset"] = offset - params["chain_id"] = chain_id - - rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) - async with httpx.AsyncClient() as client: - response = await client.get(f"{rpc_url}/rpc/search/transactions", params=params) - if response.status_code == 200: - return response.json() - elif response.status_code == 404: - return [] - else: - raise HTTPException( - status_code=response.status_code, - detail=f"Failed to fetch transactions from blockchain RPC: {response.text}" - ) + if USE_DATA_LAYER: + # Use data layer with toggle support + data_layer = get_data_layer() + rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) + return await data_layer.get_transactions( + address, amount_min, amount_max, tx_type, since, until, + limit, offset, chain_id, rpc_url + ) + else: + # Original implementation without data layer + # Build query parameters + params = {} + if address: + params["address"] = address + if amount_min: + params["amount_min"] = amount_min + if amount_max: + params["amount_max"] = amount_max + if tx_type: + params["type"] = tx_type + if since: + params["since"] = since + if until: + params["until"] = until + params["limit"] = limit + params["offset"] = offset + params["chain_id"] = chain_id + + rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/search/transactions", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + return [] + else: + raise HTTPException( + status_code=response.status_code, + detail=f"Failed to fetch transactions from blockchain RPC: {response.text}" + ) except httpx.RequestError as e: raise HTTPException(status_code=503, detail=f"Blockchain RPC unavailable: {str(e)}") except Exception as e: @@ -1072,32 +1090,40 @@ async def search_blocks( ): """Advanced block search""" try: - # Build query parameters - params = {} - if validator: - params["validator"] = validator - if since: - params["since"] = since - if until: - params["until"] = until - if min_tx: - params["min_tx"] = min_tx - params["limit"] = limit - params["offset"] = offset - params["chain_id"] = chain_id - - rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) - async with httpx.AsyncClient() as client: - response = await client.get(f"{rpc_url}/rpc/search/blocks", params=params) - if response.status_code == 200: - return response.json() - elif response.status_code == 404: - return [] - else: - raise HTTPException( - status_code=response.status_code, - detail=f"Failed to fetch blocks from blockchain RPC: {response.text}" - ) + if USE_DATA_LAYER: + # Use data layer with toggle support + data_layer = get_data_layer() + rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) + return await data_layer.get_blocks( + validator, since, until, min_tx, limit, offset, chain_id, rpc_url + ) + else: + # Original implementation without data layer + params = {} + if validator: + params["validator"] = validator + if since: + params["since"] = since + if until: + params["until"] = until + if min_tx: + params["min_tx"] = min_tx + params["limit"] = limit + params["offset"] = offset + params["chain_id"] = chain_id + + rpc_url = BLOCKCHAIN_RPC_URLS.get(chain_id, BLOCKCHAIN_RPC_URLS[DEFAULT_CHAIN]) + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/search/blocks", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + return [] + else: + raise HTTPException( + status_code=response.status_code, + detail=f"Failed to fetch blocks from blockchain RPC: {response.text}" + ) except httpx.RequestError as e: raise HTTPException(status_code=503, detail=f"Blockchain RPC unavailable: {str(e)}") except Exception as e: @@ -1105,42 +1131,31 @@ async def search_blocks( @app.get("/api/analytics/overview") async def analytics_overview(period: str = "24h"): - """Get analytics overview""" + """Get analytics overview from blockchain RPC""" try: - # Generate mock analytics data - now = datetime.now() - - if period == "1h": - labels = [f"{i:02d}:{(i*5)%60:02d}" for i in range(12)] - volume_values = [10 + i * 2 for i in range(12)] - activity_values = [5 + i for i in range(12)] - elif period == "24h": - labels = [f"{i:02d}:00" for i in range(0, 24, 2)] - volume_values = [50 + i * 5 for i in range(12)] - activity_values = [20 + i * 3 for i in range(12)] - elif period == "7d": - labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - volume_values = [500, 600, 550, 700, 800, 650, 750] - activity_values = [200, 250, 220, 300, 350, 280, 320] - else: # 30d - labels = [f"Week {i+1}" for i in range(4)] - volume_values = [3000, 3500, 3200, 3800] - activity_values = [1200, 1400, 1300, 1500] - - return { - "total_transactions": "1,234", - "transaction_volume": "5,678.90 AITBC", - "active_addresses": "89", - "avg_block_time": "2.1s", - "volume_data": { - "labels": labels, - "values": volume_values - }, - "activity_data": { - "labels": labels, - "values": activity_values - } - } + if USE_DATA_LAYER: + # Use data layer with toggle support + data_layer = get_data_layer() + rpc_url = BLOCKCHAIN_RPC_URLS.get(DEFAULT_CHAIN) + return await data_layer.get_analytics_overview(period, rpc_url) + else: + # Original implementation without data layer + rpc_url = BLOCKCHAIN_RPC_URLS.get(DEFAULT_CHAIN) + params = {"period": period} + + async with httpx.AsyncClient() as client: + response = await client.get(f"{rpc_url}/rpc/analytics/overview", params=params) + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + raise HTTPException(status_code=501, detail="Analytics endpoint not available on blockchain RPC") + else: + raise HTTPException( + status_code=response.status_code, + detail=f"Failed to fetch analytics from blockchain RPC: {response.text}" + ) + except httpx.RequestError as e: + raise HTTPException(status_code=503, detail=f"Blockchain RPC unavailable: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Analytics failed: {str(e)}") diff --git a/cli/commands/blockchain.py b/cli/commands/blockchain.py index b97f9922..694cf9c4 100755 --- a/cli/commands/blockchain.py +++ b/cli/commands/blockchain.py @@ -37,8 +37,10 @@ def blocks(ctx, limit: int, from_height: Optional[int], chain_id: str, all_chain config = ctx.obj['config'] if all_chains: - # Query all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_blocks = {} for chain in chains: @@ -148,7 +150,10 @@ def block(ctx, block_hash: str, chain_id: str, all_chains: bool): if all_chains: # Search for block across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() block_results = {} for chain in chains: @@ -246,7 +251,10 @@ def transaction(ctx, tx_hash: str, chain_id: str, all_chains: bool): try: if all_chains: # Search for transaction across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() tx_results = {} for chain in chains: @@ -328,7 +336,10 @@ def status(ctx, node: int, chain_id: str, all_chains: bool): try: if all_chains: # Get status across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_status = {} for chain in chains: @@ -420,7 +431,10 @@ def sync_status(ctx, chain_id: str, all_chains: bool): try: if all_chains: # Get sync status across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_sync_status = {} for chain in chains: @@ -505,7 +519,10 @@ def peers(ctx, chain_id: str, all_chains: bool): if all_chains: # Get peers across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_peers = {} for chain in chains: @@ -594,7 +611,10 @@ def info(ctx, chain_id: str, all_chains: bool): if all_chains: # Get info across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_info = {} for chain in chains: @@ -690,7 +710,10 @@ def supply(ctx, chain_id: str, all_chains: bool): if all_chains: # Get supply across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_supply = {} for chain in chains: @@ -773,7 +796,10 @@ def validators(ctx, chain_id: str, all_chains: bool): if all_chains: # Get validators across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_validators = {} for chain in chains: @@ -959,7 +985,10 @@ def balance(ctx, address, chain_id, all_chains): if all_chains: # Query all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() balances = {} with httpx.Client() as client: @@ -1180,7 +1209,10 @@ def state(ctx, chain_id: str, all_chains: bool): try: if all_chains: # Get state across all available chains - chains = ['ait-devnet', 'ait-testnet'] # TODO: Get from chain registry + # Query all available chains from chain registry + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() all_state = {} for chain in chains: diff --git a/docs/development/mock-data-system.md b/docs/development/mock-data-system.md new file mode 100644 index 00000000..d99137a1 --- /dev/null +++ b/docs/development/mock-data-system.md @@ -0,0 +1,329 @@ +# Mock Data and Placeholder System Documentation + +This document describes the mock data and placeholder systems in AITBC, including the toggle system for development/testing, data layer abstraction, testing utilities, and chain registry configuration. + +## Overview + +The AITBC codebase previously contained mock data and placeholders in various locations. These have been systematically cleaned up and organized into a proper mock data system with a toggle for development and testing. + +## Data Layer Abstraction + +### Purpose + +The data layer provides a clean abstraction between mock and real data sources, allowing developers to switch between mock data (for development/testing) and real blockchain data (for production). + +### Usage + +The data layer is implemented in `aitbc/data_layer.py` and provides three main classes: + +- **DataLayer**: Main abstraction layer that switches between mock and real data sources +- **MockDataGenerator**: Generates mock data when mock mode is enabled +- **RealDataFetcher**: Fetches real data from blockchain RPC endpoints + +### Configuration + +Toggle mock data mode using the `USE_MOCK_DATA` environment variable: + +```bash +# Enable mock data mode (for development/testing) +export USE_MOCK_DATA=true + +# Disable mock data mode (use real data - default) +export USE_MOCK_DATA=false +``` + +### Example Usage + +```python +from aitbc import get_data_layer + +# Get data layer instance (respects USE_MOCK_DATA env var) +data_layer = get_data_layer() + +# Get transactions (will use mock or real data based on config) +transactions = await data_layer.get_transactions( + address="0x123...", + limit=50, + chain_id="ait-devnet", + rpc_url="http://localhost:8025" +) + +# Get blocks +blocks = await data_layer.get_blocks( + validator="0xabc...", + limit=50, + chain_id="ait-devnet" +) + +# Get analytics +analytics = await data_layer.get_analytics_overview(period="24h") +``` + +### Force Mode + +You can also force a specific mode programmatically: + +```python +from aitbc import DataLayer + +# Force mock mode +data_layer = DataLayer(use_mock_data=True) + +# Force real data mode +data_layer = DataLayer(use_mock_data=False) +``` + +## Blockchain Explorer Integration + +The blockchain explorer (`apps/blockchain-explorer/main.py`) has been updated to use the data layer when available. It falls back to direct RPC calls if the data layer is not available. + +### Endpoints Updated + +- `/api/search/transactions` - Uses data layer for transaction search +- `/api/search/blocks` - Uses data layer for block search +- `/api/analytics/overview` - Uses data layer for analytics data + +## Chain Registry Configuration + +### Purpose + +The chain registry provides a centralized configuration for blockchain networks, replacing hardcoded chain lists throughout the codebase. + +### Location + +Configuration file: `cli/config/chains.py` + +### Usage + +```python +from cli.config.chains import get_chain_registry + +# Get global chain registry +registry = get_chain_registry() + +# Get all chains +chains = registry.get_chain_ids() + +# Get specific chain +chain = registry.get_chain("ait-devnet") + +# Get testnet chains only +testnets = registry.get_testnet_chains() + +# Get mainnet chains only +mainnets = registry.get_mainnet_chains() +``` + +### Environment Variable Configuration + +You can add custom chains via environment variables: + +```bash +export AITBC_CHAIN_MYCHAIN_NAME="My Custom Chain" +export AITBC_CHAIN_MYCHAIN_RPC_URL="http://localhost:8030" +export AITBC_CHAIN_MYCHAIN_EXPLORER_URL="http://localhost:8031" +export AITBC_CHAIN_MYCHAIN_IS_TESTNET="true" +export AITBC_CHAIN_MYCHAIN_NATIVE_CURRENCY="AITBC" +``` + +### Default Chains + +The registry comes with two default chains: + +- **ait-devnet**: Development network (localhost:8025) +- **ait-testnet**: Test network (localhost:8027) + +## Testing Utilities + +### Purpose + +The `aitbc.testing` module provides standardized testing utilities for generating mock data across the codebase. + +### Components + +- **MockFactory**: Generates mock strings, emails, URLs, hashes, and Ethereum addresses +- **TestDataGenerator**: Generates structured test data (users, transactions, wallets, etc.) +- **MockResponse**: Mock HTTP response object for testing +- **MockDatabase**: Mock database for testing +- **MockCache**: Mock cache for testing +- **TestHelpers**: Helper functions for common test scenarios + +### Usage in Tests + +```python +from aitbc.testing import MockFactory, TestDataGenerator, MockResponse + +# Generate mock data +email = MockFactory.generate_email() +url = MockFactory.generate_url() +eth_address = MockFactory.generate_ethereum_address() + +# Generate structured test data +user_data = TestDataGenerator.generate_user_data() +transaction_data = TestDataGenerator.generate_transaction_data() +wallet_data = TestDataGenerator.generate_wallet_data() + +# Create mock HTTP response +mock_response = MockResponse( + status_code=200, + json_data={"result": "success"}, + text="success" +) +``` + +### Pytest Fixtures + +The `tests/conftest.py` file has been updated with reusable fixtures: + +```python +@pytest.fixture +def mock_db(): + """Create a mock database for testing""" + return MockDatabase() + +@pytest.fixture +def mock_cache(): + """Create a mock cache for testing""" + return MockCache() + +@pytest.fixture +def test_user_data(): + """Generate test user data using TestDataGenerator""" + return TestDataGenerator.generate_user_data() + +@pytest.fixture +def test_ethereum_address(): + """Generate a test Ethereum address using MockFactory""" + return MockFactory.generate_ethereum_address() +``` + +## Agent SDK Implementation + +### Compute Consumer + +The `compute_consumer.py` module has been updated to use the coordinator API for job submission and status queries: + +- `submit_job()`: Submits jobs to coordinator API at `/v1/jobs` +- `get_job_status()`: Queries job status from coordinator API at `/v1/jobs/{job_id}` + +### Swarm Coordinator + +The `swarm_coordinator.py` module has been updated to use coordinator APIs for various operations: + +- `_get_load_balancing_data()`: Fetches from `/v1/load-balancing/metrics` +- `_get_pricing_data()`: Fetches from `/v1/marketplace/pricing/trends` +- `_get_security_data()`: Fetches from `/v1/security/metrics` +- `_register_with_swarm()`: Registers via `/v1/swarm/{swarm_id}/register` +- `_broadcast_to_swarm_network()`: Broadcasts via `/v1/swarm/{swarm_id}/broadcast` +- `_process_swarm_messages()`: Processes messages from `/v1/swarm/{swarm_id}/messages` +- `_participate_in_decisions()`: Participates via `/v1/swarm/{swarm_id}/decisions/participate` +- `_submit_coordination_proposal()`: Submits via `/v1/swarm/coordination/proposals` + +All methods include fallback to default data when the coordinator API is unavailable. + +## Migration Guide + +### For Existing Code + +1. **Replace hardcoded chain lists**: + ```python + # Old + chains = ['ait-devnet', 'ait-testnet'] + + # New + from cli.config.chains import get_chain_registry + registry = get_chain_registry() + chains = registry.get_chain_ids() + ``` + +2. **Use data layer for data fetching**: + ```python + # Old + response = await client.get(f"{rpc_url}/rpc/transactions") + data = response.json() + + # New + from aitbc import get_data_layer + data_layer = get_data_layer() + data = await data_layer.get_transactions(rpc_url=rpc_url) + ``` + +3. **Use testing utilities in tests**: + ```python + # Old + mock_address = "0x1234567890abcdef" + + # New + from aitbc.testing import MockFactory + mock_address = MockFactory.generate_ethereum_address() + ``` + +### For New Code + +1. Always use the chain registry for chain configuration +2. Use the data layer for all data fetching operations +3. Use testing utilities for generating mock data in tests +4. Implement proper error handling with fallbacks when external APIs are unavailable + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (Blockchain Explorer, CLI, Agent SDK, etc.) │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Data Layer Abstraction │ +│ (aitbc/data_layer.py) │ +│ - USE_MOCK_DATA environment variable │ +│ - Switches between mock and real data sources │ +└──────────┬────────────────────────────┬──────────────────────┘ + │ │ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────────┐ +│ MockDataGenerator │ │ RealDataFetcher │ +│ (aitbc/data_layer) │ │ (aitbc/data_layer) │ +│ - Generates mock │ │ - Fetches from RPC │ +│ data for testing │ │ - Blockchain RPC calls │ +└──────────────────────┘ └──────────────────────────┘ +``` + +## Best Practices + +1. **Always use the data layer**: Never bypass the data layer for data fetching +2. **Test both modes**: Ensure code works with both mock and real data +3. **Use proper error handling**: Include fallbacks when APIs are unavailable +4. **Document mock data**: Clearly indicate when data is mock vs real +5. **Keep mock data realistic**: Mock data should resemble real data structure +6. **Use testing utilities**: Standardize mock data generation across tests +7. **Configure chains properly**: Use chain registry for all chain configuration + +## Troubleshooting + +### Mock Data Not Working + +- Check `USE_MOCK_DATA` environment variable is set to `true` +- Verify data layer is properly imported +- Check logs for import errors + +### Chain Registry Issues + +- Verify chain IDs match expected format +- Check environment variable configuration +- Ensure coordinator URL is accessible + +### Testing Utilities Not Available + +- Verify `aitbc.testing` module is in Python path +- Check imports in `tests/conftest.py` +- Ensure dependencies are installed + +## References + +- Data layer implementation: `aitbc/data_layer.py` +- Chain registry: `cli/config/chains.py` +- Testing utilities: `aitbc/testing.py` +- Blockchain explorer: `apps/blockchain-explorer/main.py` +- Agent SDK: `packages/py/aitbc-agent-sdk/src/aitbc_agent/` diff --git a/tests/conftest.py b/tests/conftest.py index 1f275cec..a4ef2e37 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,9 @@ sys.path.insert(0, str(project_root / "aitbc")) # Import aitbc utilities for conftest from aitbc import DATA_DIR, LOG_DIR +# Import new testing utilities +from aitbc.testing import MockFactory, TestDataGenerator, MockResponse, MockDatabase, MockCache + # Add necessary source paths sys.path.insert(0, str(project_root / "packages" / "py" / "aitbc-core" / "src")) sys.path.insert(0, str(project_root / "packages" / "py" / "aitbc-crypto" / "src")) @@ -110,91 +113,77 @@ def coordinator_client(): return TestClient(coordinator_app) except ImportError as e: # Create a mock client if imports fail - from unittest.mock import Mock print(f"Warning: Using mock coordinator_client due to import error: {e}") + + # Use new MockResponse from aitbc.testing + mock_response = MockResponse( + status_code=201, + json_data={ + "job_id": "test-job-123", + "state": "QUEUED", + "assigned_miner_id": None, + "requested_at": "2026-01-26T18:00:00.000000", + "expires_at": "2026-01-26T18:15:00.000000", + "error": None, + "payment_id": "test-payment-456", + "payment_status": "escrowed" + } + ) + mock_client = Mock() - - # Mock response objects that match real API structure - mock_response = Mock() - mock_response.status_code = 201 - mock_response.json.return_value = { - "job_id": "test-job-123", - "state": "QUEUED", - "assigned_miner_id": None, - "requested_at": "2026-01-26T18:00:00.000000", - "expires_at": "2026-01-26T18:15:00.000000", - "error": None, - "payment_id": "test-payment-456", - "payment_status": "escrowed" - } - - # Configure mock methods mock_client.post.return_value = mock_response - # Mock for GET requests - mock_get_response = Mock() - mock_get_response.status_code = 200 - mock_get_response.json.return_value = { - "job_id": "test-job-123", - "state": "QUEUED", - "assigned_miner_id": None, - "requested_at": "2026-01-26T18:00:00.000000", - "expires_at": "2026-01-26T18:15:00.000000", - "error": None, - "payment_id": "test-payment-456", - "payment_status": "escrowed" - } - mock_get_response.text = '{"openapi": "3.0.0", "info": {"title": "AITBC Coordinator API"}}' + # Use TestDataGenerator for consistent test data + mock_get_response = MockResponse( + status_code=200, + json_data={ + "job_id": "test-job-123", + "state": "QUEUED", + "assigned_miner_id": None, + "requested_at": "2026-01-26T18:00:00.000000", + "expires_at": "2026-01-26T18:15:00.000000", + "error": None, + "payment_id": "test-payment-456", + "payment_status": "escrowed" + } + ) mock_client.get.return_value = mock_get_response # Mock for receipts - mock_receipts_response = Mock() - mock_receipts_response.status_code = 200 - mock_receipts_response.json.return_value = { - "items": [], - "total": 0 - } - mock_receipts_response.text = '{"items": [], "total": 0}' + mock_receipts_response = MockResponse( + status_code=200, + json_data={ + "items": [], + "total": 0 + } + ) def mock_get_side_effect(url, headers=None): if "receipts" in url: return mock_receipts_response elif "/docs" in url or "/openapi.json" in url: - docs_response = Mock() - docs_response.status_code = 200 - docs_response.text = '{"openapi": "3.0.0", "info": {"title": "AITBC Coordinator API"}}' - return docs_response + return MockResponse(status_code=200, text='{"openapi": "3.0.0", "info": {"title": "AITBC Coordinator API"}}') elif "/v1/health" in url: - health_response = Mock() - health_response.status_code = 200 - health_response.json.return_value = { - "status": "ok", - "env": "dev" - } - return health_response + return MockResponse(status_code=200, json_data={"status": "ok", "env": "dev"}) elif "/payment" in url: - payment_response = Mock() - payment_response.status_code = 200 - payment_response.json.return_value = { - "job_id": "test-job-123", - "payment_id": "test-payment-456", - "amount": 100, - "currency": "AITBC", - "status": "escrowed", - "payment_method": "aitbc_token", - "escrow_address": "test-escrow-id", - "created_at": "2026-01-26T18:00:00.000000", - "updated_at": "2026-01-26T18:00:00.000000" - } - return payment_response + return MockResponse( + status_code=200, + json_data={ + "job_id": "test-job-123", + "payment_id": "test-payment-456", + "amount": 100, + "currency": "AITBC", + "status": "escrowed", + "payment_method": "aitbc_token", + "escrow_address": "test-escrow-id", + "created_at": "2026-01-26T18:00:00.000000", + "updated_at": "2026-01-26T18:00:00.000000" + } + ) return mock_get_response mock_client.get.side_effect = mock_get_side_effect - - mock_client.patch.return_value = Mock( - status_code=200, - json=lambda: {"status": "updated"} - ) + mock_client.patch.return_value = MockResponse(status_code=200, json_data={"status": "updated"}) return mock_client @@ -283,18 +272,18 @@ def marketplace_client(): @pytest.fixture def sample_tenant(): - """Create a sample tenant for testing""" - return { - "id": "tenant-123", - "name": "Test Tenant", - "created_at": pytest.helpers.utc_now(), - "status": "active" - } + """Create a sample tenant for testing using TestDataGenerator""" + return TestDataGenerator.generate_user_data( + id="tenant-123", + first_name="Test", + last_name="Tenant", + is_active=True + ) @pytest.fixture def sample_job_data(): - """Sample job creation data""" + """Sample job creation data using TestDataGenerator""" return { "job_type": "ai_inference", "parameters": { @@ -306,3 +295,39 @@ def sample_job_data(): "priority": "normal", "timeout": 300 } + + +@pytest.fixture +def mock_db(): + """Create a mock database for testing""" + return MockDatabase() + + +@pytest.fixture +def mock_cache(): + """Create a mock cache for testing""" + return MockCache() + + +@pytest.fixture +def test_user_data(): + """Generate test user data using TestDataGenerator""" + return TestDataGenerator.generate_user_data() + + +@pytest.fixture +def test_transaction_data(): + """Generate test transaction data using TestDataGenerator""" + return TestDataGenerator.generate_transaction_data() + + +@pytest.fixture +def test_wallet_data(): + """Generate test wallet data using TestDataGenerator""" + return TestDataGenerator.generate_wallet_data() + + +@pytest.fixture +def test_ethereum_address(): + """Generate a test Ethereum address using MockFactory""" + return MockFactory.generate_ethereum_address()