Files
aitbc/cli/aitbc_cli/commands/blockchain.py
oib a302da73a9 refactor: migrate blockchain CLI commands to use centralized config and update port assignments
- Replace load_multichain_config() with ctx.obj['config'] in all blockchain commands
- Update blockchain RPC port from 8003 to 8006 throughout CLI
- Add blockchain_rpc_url and wallet_url fields to Config class with environment variable support
- Update node status command to use new port logic (8006 for primary, 8026 for dev)
- Update installation docs to reflect new blockchain RPC port (8006)
- Update
2026-03-06 10:25:57 +01:00

463 lines
16 KiB
Python

"""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}")