"""Blockchain commands for AITBC CLI""" import click import httpx def _get_node_endpoint(ctx): try: config = ctx.obj['config'] # Use the new blockchain_rpc_url from config return config.blockchain_rpc_url except: return "http://127.0.0.1:8006" # Use new blockchain RPC port from typing import Optional, List from ..utils import output, error @click.group() @click.pass_context def blockchain(ctx): """Query blockchain information and status""" # Set role for blockchain commands ctx.ensure_object(dict) ctx.parent.detected_role = 'blockchain' @blockchain.command() @click.option("--limit", type=int, default=10, help="Number of blocks to show") @click.option("--from-height", type=int, help="Start from this block height") @click.pass_context def blocks(ctx, limit: int, from_height: Optional[int]): """List recent blocks""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port # Get blocks from the local blockchain node with httpx.Client() as client: if from_height: # Get blocks range response = client.get( f"{node_url}/rpc/blocks-range", params={"from_height": from_height, "limit": limit}, timeout=5 ) else: # Get recent blocks starting from head response = client.get( f"{node_url}/rpc/blocks-range", params={"limit": limit}, timeout=5 ) if response.status_code == 200: blocks_data = response.json() output(blocks_data, ctx.obj['output_format']) else: # Fallback to getting head block if range not available head_response = client.get(f"{node_url}/rpc/head", timeout=5) if head_response.status_code == 200: head_data = head_response.json() output({ "blocks": [head_data], "message": f"Showing head block only (height {head_data.get('height', 'unknown')})" }, ctx.obj['output_format']) else: error(f"Failed to get blocks: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.argument("block_hash") @click.pass_context def block(ctx, block_hash: str): """Get details of a specific block""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port # Try to get block from local blockchain node with httpx.Client() as client: # First try to get block by hash response = client.get( f"{node_url}/rpc/blocks/by_hash/{block_hash}", timeout=5 ) if response.status_code == 200: block_data = response.json() output(block_data, ctx.obj['output_format']) else: # If by_hash not available, try to get by height (if hash looks like a number) try: height = int(block_hash) response = client.get(f"{node_url}/rpc/blocks/{height}", timeout=5) if response.status_code == 200: block_data = response.json() output(block_data, ctx.obj['output_format']) else: error(f"Block not found: {response.status_code}") except ValueError: # Not a number, try to find block by scanning recent blocks head_response = client.get(f"{node_url}/rpc/head", timeout=5) if head_response.status_code == 200: head_data = head_response.json() current_height = head_data.get('height', 0) # Search recent blocks (last 10) for h in range(max(0, current_height - 10), current_height + 1): block_response = client.get(f"{node_url}/rpc/blocks/{h}", timeout=5) if block_response.status_code == 200: block_data = block_response.json() if block_data.get('hash') == block_hash: output(block_data, ctx.obj['output_format']) return error(f"Block not found: {response.status_code}") else: error(f"Failed to get head block: {head_response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.argument("tx_hash") @click.pass_context def transaction(ctx, tx_hash: str): """Get transaction details""" config = ctx.obj['config'] try: with httpx.Client() as client: response = client.get( f"{config.coordinator_url}/explorer/transactions/{tx_hash}", headers={"X-Api-Key": config.api_key or ""} ) if response.status_code == 200: tx_data = response.json() output(tx_data, ctx.obj['output_format']) else: error(f"Transaction not found: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option("--node", type=int, default=1, help="Node number (1, 2, or 3)") @click.pass_context def status(ctx, node: int): """Get blockchain node status""" config = ctx.obj['config'] # Map node to RPC URL using new port logic node_urls = { 1: "http://localhost:8006", # Primary Blockchain RPC 2: "http://localhost:8026", # Development Blockchain RPC 3: "http://aitbc.keisanki.net/rpc" } rpc_url = node_urls.get(node) if not rpc_url: error(f"Invalid node number: {node}") return try: with httpx.Client() as client: # Use health endpoint that exists health_url = rpc_url + "/health" response = client.get( health_url, timeout=5 ) if response.status_code == 200: status_data = response.json() output({ "node": node, "rpc_url": rpc_url, "status": status_data }, ctx.obj['output_format']) else: error(f"Node {node} not responding: {response.status_code}") except Exception as e: error(f"Failed to connect to node {node}: {e}") @blockchain.command() @click.pass_context def sync_status(ctx): """Get blockchain synchronization status""" config = ctx.obj['config'] try: with httpx.Client() as client: response = client.get( f"{config.coordinator_url}/v1/sync-status", headers={"X-Api-Key": config.api_key or ""} ) if response.status_code == 200: sync_data = response.json() output(sync_data, ctx.obj['output_format']) else: error(f"Failed to get sync status: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.pass_context def peers(ctx): """List connected peers""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port # Try to get peers from the local blockchain node with httpx.Client() as client: # First try the RPC endpoint for peers response = client.get( f"{node_url}/rpc/peers", timeout=5 ) if response.status_code == 200: peers_data = response.json() output(peers_data, ctx.obj['output_format']) else: # If no peers endpoint, return meaningful message output({ "peers": [], "message": "No P2P peers available - node running in RPC-only mode", "node_url": node_url }, ctx.obj['output_format']) except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.pass_context def info(ctx): """Get blockchain information""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port with httpx.Client() as client: # Get head block for basic info response = client.get( f"{node_url}/rpc/head", timeout=5 ) if response.status_code == 200: head_data = response.json() # Create basic info from head block info_data = { "chain_id": "ait-devnet", "height": head_data.get("height"), "latest_block": head_data.get("hash"), "timestamp": head_data.get("timestamp"), "transactions_in_block": head_data.get("tx_count", 0), "status": "active" } output(info_data, ctx.obj['output_format']) else: error(f"Failed to get blockchain info: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.pass_context def supply(ctx): """Get token supply information""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port with httpx.Client() as client: response = client.get( f"{node_url}/rpc/supply", timeout=5 ) if response.status_code == 200: supply_data = response.json() output(supply_data, ctx.obj['output_format']) else: error(f"Failed to get supply info: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.pass_context def validators(ctx): """List blockchain validators""" try: config = ctx.obj['config'] node_url = config.blockchain_rpc_url # Use new blockchain RPC port with httpx.Client() as client: response = client.get( f"{node_url}/rpc/validators", timeout=5 ) if response.status_code == 200: validators_data = response.json() output(validators_data, ctx.obj['output_format']) else: error(f"Failed to get validators: {response.status_code}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--chain-id', required=True, help='Chain ID') @click.pass_context def genesis(ctx, chain_id): """Get the genesis block of a chain""" config = ctx.obj['config'] try: import httpx with httpx.Client() as client: # We assume node 1 is running on port 8082, but let's just hit the first configured node response = client.get( f"{_get_node_endpoint(ctx)}/rpc/blocks/0?chain_id={chain_id}", timeout=5 ) if response.status_code == 200: output(response.json(), ctx.obj['output_format']) else: error(f"Failed to get genesis block: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--chain-id', required=True, help='Chain ID') @click.pass_context def transactions(ctx, chain_id): """Get latest transactions on a chain""" config = ctx.obj['config'] try: import httpx with httpx.Client() as client: response = client.get( f"{_get_node_endpoint(ctx)}/rpc/transactions?chain_id={chain_id}", timeout=5 ) if response.status_code == 200: output(response.json(), ctx.obj['output_format']) else: error(f"Failed to get transactions: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--chain-id', required=True, help='Chain ID') @click.pass_context def head(ctx, chain_id): """Get the head block of a chain""" config = ctx.obj['config'] try: import httpx with httpx.Client() as client: response = client.get( f"{_get_node_endpoint(ctx)}/rpc/head?chain_id={chain_id}", timeout=5 ) if response.status_code == 200: output(response.json(), ctx.obj['output_format']) else: error(f"Failed to get head block: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--chain-id', required=True, help='Chain ID') @click.option('--from', 'from_addr', required=True, help='Sender address') @click.option('--to', required=True, help='Recipient address') @click.option('--data', required=True, help='Transaction data payload') @click.option('--nonce', type=int, default=0, help='Nonce') @click.pass_context def send(ctx, chain_id, from_addr, to, data, nonce): """Send a transaction to a chain""" config = ctx.obj['config'] try: import httpx with httpx.Client() as client: tx_payload = { "type": "TRANSFER", "chain_id": chain_id, "from_address": from_addr, "to_address": to, "value": 0, "gas_limit": 100000, "gas_price": 1, "nonce": nonce, "data": data, "signature": "mock_signature" } response = client.post( f"{_get_node_endpoint(ctx)}/rpc/sendTx", json=tx_payload, timeout=5 ) if response.status_code in (200, 201): output(response.json(), ctx.obj['output_format']) else: error(f"Failed to send transaction: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--address', required=True, help='Wallet address') @click.pass_context def balance(ctx, address): """Get the balance of an address across all chains""" config = ctx.obj['config'] try: import httpx # Balance is typically served by the coordinator API or blockchain node directly # The node has /rpc/getBalance/{address} but it expects chain_id param. Let's just query devnet for now. with httpx.Client() as client: response = client.get( f"{_get_node_endpoint(ctx)}/rpc/getBalance/{address}?chain_id=ait-devnet", timeout=5 ) if response.status_code == 200: output(response.json(), ctx.obj['output_format']) else: error(f"Failed to get balance: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}") @blockchain.command() @click.option('--address', required=True, help='Wallet address') @click.option('--amount', type=int, default=1000, help='Amount to mint') @click.pass_context def faucet(ctx, address, amount): """Mint devnet funds to an address""" config = ctx.obj['config'] try: import httpx with httpx.Client() as client: response = client.post( f"{_get_node_endpoint(ctx)}/rpc/admin/mintFaucet", json={"address": address, "amount": amount, "chain_id": "ait-devnet"}, timeout=5 ) if response.status_code in (200, 201): output(response.json(), ctx.obj['output_format']) else: error(f"Failed to use faucet: {response.status_code} - {response.text}") except Exception as e: error(f"Network error: {e}")