Files
aitbc/cli/commands/explorer.py
aitbc 8bb2dcf558
Some checks failed
CLI Tests / test-cli (push) Failing after 17s
Cross-Node Transaction Testing / transaction-test (push) Successful in 9s
Deploy to Testnet / deploy-testnet (push) Successful in 1m18s
Multi-Node Stress Testing / stress-test (push) Successful in 3s
Node Failover Simulation / failover-test (push) Successful in 2s
Security Scanning / security-scan (push) Successful in 17s
Standardize config initialization across all CLI command groups
- Add config initialization to all command group decorators
- Import get_config and CLIConfig from aitbc_cli.config in all command modules
- Set default output_format to 'table' in context object
- Add console import to utils imports where needed
- Remove unused imports (json, time, asyncio, base64, mimetypes, pathlib)
- Reorder imports to group utils imports together
- Update marketplace_advanced group name from 'advanced' to 'marketplace
2026-05-08 11:58:32 +02:00

352 lines
12 KiB
Python
Executable File

"""Explorer commands for AITBC CLI"""
import os
import click
import subprocess
import json
from typing import Optional, List
from utils import output, error
from aitbc_cli.config import get_config, CLIConfig
def _get_explorer_endpoint(ctx):
"""Get explorer endpoint from config or default"""
try:
config = ctx.obj['config']
# Default to port 8004 for blockchain explorer
return getattr(config, 'explorer_url', os.getenv('AITBC_EXPLORER_URL', 'http://127.0.0.1:8004'))
except:
return os.getenv('AITBC_EXPLORER_URL', 'http://127.0.0.1:8004')
def _curl_request(url: str, params: dict = None):
"""Make curl request instead of httpx to avoid connection issues"""
cmd = ['curl', '-s', url]
if params:
param_str = '&'.join([f"{k}={v}" for k, v in params.items()])
cmd.append(f"{url}?{param_str}")
else:
cmd.append(url)
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return result.stdout
else:
return None
except Exception:
return None
@click.group()
@click.pass_context
def explorer(ctx):
"""Blockchain explorer operations and queries"""
# Initialize context object with config
if ctx.obj is None:
ctx.obj = {}
if 'config' not in ctx.obj:
ctx.obj['config'] = get_config()
if 'output_format' not in ctx.obj:
ctx.obj['output_format'] = 'table'
ctx.ensure_object(dict)
ctx.parent.detected_role = 'explorer'
@explorer.command()
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def status(ctx, chain_id: str):
"""Get explorer and chain status"""
try:
explorer_url = _get_explorer_endpoint(ctx)
# Get explorer health
response_text = _curl_request(f"{explorer_url}/health")
if response_text:
try:
health = json.loads(response_text)
output({
"explorer_status": health.get("status", "unknown"),
"node_status": health.get("node_status", "unknown"),
"version": health.get("version", "unknown"),
"features": health.get("features", [])
}, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to get explorer status: {str(e)}")
@explorer.command()
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def chains(ctx, chain_id: str):
"""List all supported chains"""
try:
explorer_url = _get_explorer_endpoint(ctx)
response_text = _curl_request(f"{explorer_url}/api/chains")
if response_text:
try:
chains_data = json.loads(response_text)
output(chains_data, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to list chains: {str(e)}")
@explorer.command()
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def head(ctx, chain_id: str):
"""Get current chain head information"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {"chain_id": chain_id}
response_text = _curl_request(f"{explorer_url}/api/chain/head", params)
if response_text:
try:
head_data = json.loads(response_text)
output(head_data, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to get chain head: {str(e)}")
@explorer.command()
@click.argument('height', type=int)
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def block(ctx, height: int, chain_id: str):
"""Get block information by height"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {"chain_id": chain_id}
response_text = _curl_request(f"{explorer_url}/api/blocks/{height}", params)
if response_text:
try:
block_data = json.loads(response_text)
output(block_data, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to get block {height}: {str(e)}")
@explorer.command()
@click.argument('tx_hash')
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def transaction(ctx, tx_hash: str, chain_id: str):
"""Get transaction information by hash"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {"chain_id": chain_id}
response_text = _curl_request(f"{explorer_url}/api/transactions/{tx_hash}", params)
if response_text:
try:
tx_data = json.loads(response_text)
output(tx_data, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to get transaction {tx_hash}: {str(e)}")
@explorer.command()
@click.option('--address', help='Filter by address')
@click.option('--amount-min', type=float, help='Minimum amount')
@click.option('--amount-max', type=float, help='Maximum amount')
@click.option('--type', 'tx_type', help='Transaction type')
@click.option('--since', help='Start date (ISO format)')
@click.option('--until', help='End date (ISO format)')
@click.option('--limit', type=int, default=50, help='Number of results (default: 50)')
@click.option('--offset', type=int, default=0, help='Offset for pagination (default: 0)')
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def search_transactions(ctx, address: Optional[str], amount_min: Optional[float],
amount_max: Optional[float], tx_type: Optional[str],
since: Optional[str], until: Optional[str],
limit: int, offset: int, chain_id: str):
"""Search transactions with filters"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {
"limit": limit,
"offset": offset,
"chain_id": chain_id
}
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["tx_type"] = tx_type
if since:
params["since"] = since
if until:
params["until"] = until
response_text = _curl_request(f"{explorer_url}/api/search/transactions", params)
if response_text:
try:
results = json.loads(response_text)
output(results, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to search transactions: {str(e)}")
@explorer.command()
@click.option('--validator', help='Filter by validator address')
@click.option('--since', help='Start date (ISO format)')
@click.option('--until', help='End date (ISO format)')
@click.option('--min-tx', type=int, help='Minimum transaction count')
@click.option('--limit', type=int, default=50, help='Number of results (default: 50)')
@click.option('--offset', type=int, default=0, help='Offset for pagination (default: 0)')
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def search_blocks(ctx, validator: Optional[str], since: Optional[str],
until: Optional[str], min_tx: Optional[int],
limit: int, offset: int, chain_id: str):
"""Search blocks with filters"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {
"limit": limit,
"offset": offset,
"chain_id": chain_id
}
if validator:
params["validator"] = validator
if since:
params["since"] = since
if until:
params["until"] = until
if min_tx:
params["min_tx"] = min_tx
response_text = _curl_request(f"{explorer_url}/api/search/blocks", params)
if response_text:
try:
results = json.loads(response_text)
output(results, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to search blocks: {str(e)}")
@explorer.command()
@click.option('--period', default='24h', help='Analytics period (1h, 24h, 7d, 30d)')
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def analytics(ctx, period: str, chain_id: str):
"""Get blockchain analytics overview"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {
"period": period,
"chain_id": chain_id
}
response_text = _curl_request(f"{explorer_url}/api/analytics/overview", params)
if response_text:
try:
analytics_data = json.loads(response_text)
output(analytics_data, ctx.obj['output_format'])
except json.JSONDecodeError:
error("Invalid response from explorer")
else:
error("Failed to connect to explorer")
except Exception as e:
error(f"Failed to get analytics: {str(e)}")
@explorer.command()
@click.option('--format', 'export_format', type=click.Choice(['csv', 'json']), default='csv', help='Export format')
@click.option('--type', 'export_type', type=click.Choice(['transactions', 'blocks']), default='transactions', help='Data type to export')
@click.option('--chain-id', default='ait-devnet', help='Chain ID to query (default: ait-devnet)')
@click.pass_context
def export(ctx, export_format: str, export_type: str, chain_id: str):
"""Export blockchain data"""
try:
explorer_url = _get_explorer_endpoint(ctx)
params = {
"format": export_format,
"type": export_type,
"chain_id": chain_id
}
if export_type == 'transactions':
response_text = _curl_request(f"{explorer_url}/api/export/search", params)
else:
response_text = _curl_request(f"{explorer_url}/api/export/blocks", params)
if response_text:
# Save to file
filename = f"explorer_export_{export_type}_{chain_id}.{export_format}"
with open(filename, 'w') as f:
f.write(response_text)
output(f"Data exported to {filename}", ctx.obj['output_format'])
else:
error("Failed to export data")
except Exception as e:
error(f"Failed to export data: {str(e)}")
@explorer.command()
@click.option('--chain-id', default='main', help='Chain ID to explore')
@click.pass_context
def web(ctx, chain_id: str):
"""Get blockchain explorer web URL"""
try:
explorer_url = _get_explorer_endpoint(ctx)
web_url = explorer_url.replace('http://', 'http://') # Ensure proper format
output(f"Explorer web interface: {web_url}", ctx.obj['output_format'])
output("Use the URL above to access the explorer in your browser", ctx.obj['output_format'])
except Exception as e:
error(f"Failed to get explorer URL: {e}", ctx.obj['output_format'])