feat: add multi-chain support to blockchain explorer and improve GPU review handling
- Add multi-chain configuration with devnet, testnet, and mainnet RPC URLs - Add chain selector dropdown in explorer UI for network switching - Add chain_id parameter to all API endpoints (chain/head, blocks, transactions, search) - Add /api/chains endpoint to list supported blockchain networks - Update blockchain explorer port from 3001 to 8016 - Update devnet RPC port from 8080 to 8026 - Add GPU reviews table
This commit is contained in:
@@ -370,7 +370,7 @@ def status(ctx, exchange_name: str):
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/api/v1/exchange/rates",
|
||||
f"{config.coordinator_url}/v1/exchange/rates",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -412,7 +412,7 @@ def create_payment(ctx, aitbc_amount: Optional[float], btc_amount: Optional[floa
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
rates_response = client.get(
|
||||
f"{config.coordinator_url}/api/v1/exchange/rates",
|
||||
f"{config.coordinator_url}/v1/exchange/rates",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -441,7 +441,7 @@ def create_payment(ctx, aitbc_amount: Optional[float], btc_amount: Optional[floa
|
||||
|
||||
# Create payment
|
||||
response = client.post(
|
||||
f"{config.coordinator_url}/api/v1/exchange/create-payment",
|
||||
f"{config.coordinator_url}/v1/exchange/create-payment",
|
||||
json=payment_data,
|
||||
timeout=10
|
||||
)
|
||||
@@ -471,7 +471,7 @@ def payment_status(ctx, payment_id: str):
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/api/v1/exchange/payment-status/{payment_id}",
|
||||
f"{config.coordinator_url}/v1/exchange/payment-status/{payment_id}",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -505,7 +505,7 @@ def market_stats(ctx):
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/api/v1/exchange/market-stats",
|
||||
f"{config.coordinator_url}/v1/exchange/market-stats",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -593,7 +593,7 @@ def register(ctx, name: str, api_key: str, api_secret: Optional[str], sandbox: b
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
f"{config.coordinator_url}/api/v1/exchange/register",
|
||||
f"{config.coordinator_url}/v1/exchange/register",
|
||||
json=exchange_data,
|
||||
timeout=10
|
||||
)
|
||||
@@ -642,7 +642,7 @@ def create_pair(ctx, pair: str, base_asset: str, quote_asset: str,
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
f"{config.coordinator_url}/api/v1/exchange/create-pair",
|
||||
f"{config.coordinator_url}/v1/exchange/create-pair",
|
||||
json=pair_data,
|
||||
timeout=10
|
||||
)
|
||||
@@ -681,7 +681,7 @@ def start_trading(ctx, pair: str, exchange: Optional[str], order_type: tuple):
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
f"{config.coordinator_url}/api/v1/exchange/start-trading",
|
||||
f"{config.coordinator_url}/v1/exchange/start-trading",
|
||||
json=trading_data,
|
||||
timeout=10
|
||||
)
|
||||
@@ -719,7 +719,7 @@ def list_pairs(ctx, pair: Optional[str], exchange: Optional[str], status: Option
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/api/v1/exchange/pairs",
|
||||
f"{config.coordinator_url}/v1/exchange/pairs",
|
||||
params=params,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
346
cli/aitbc_cli/commands/explorer.py
Normal file
346
cli/aitbc_cli/commands/explorer.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""Explorer commands for AITBC CLI"""
|
||||
|
||||
import click
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Optional, List
|
||||
from ..utils import output, error
|
||||
|
||||
|
||||
def _get_explorer_endpoint(ctx):
|
||||
"""Get explorer endpoint from config or default"""
|
||||
try:
|
||||
config = ctx.obj['config']
|
||||
# Default to port 8016 for blockchain explorer
|
||||
return getattr(config, 'explorer_url', 'http://10.1.223.93:8016')
|
||||
except:
|
||||
return "http://10.1.223.93:8016"
|
||||
|
||||
|
||||
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"""
|
||||
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='ait-devnet', help='Chain ID to query (default: ait-devnet)')
|
||||
@click.option('--open', is_flag=True, help='Open explorer in web browser')
|
||||
@click.pass_context
|
||||
def web(ctx, chain_id: str, open: bool):
|
||||
"""Open blockchain explorer in web browser"""
|
||||
try:
|
||||
explorer_url = _get_explorer_endpoint(ctx)
|
||||
web_url = explorer_url.replace('http://', 'http://') # Ensure proper format
|
||||
|
||||
if open:
|
||||
import webbrowser
|
||||
webbrowser.open(web_url)
|
||||
output(f"Opening explorer in web browser: {web_url}", ctx.obj['output_format'])
|
||||
else:
|
||||
output(f"Explorer web interface: {web_url}", ctx.obj['output_format'])
|
||||
|
||||
except Exception as e:
|
||||
error(f"Failed to open web interface: {str(e)}")
|
||||
Reference in New Issue
Block a user