Files
aitbc/cli/aitbc_cli/commands/exchange_island.py
aitbc 27a078a279
Some checks failed
CLI Tests / test-cli (push) Failing after 3s
Cross-Node Transaction Testing / transaction-test (push) Successful in 3s
Deploy to Testnet / deploy-testnet (push) Successful in 1m18s
Multi-Node Stress Testing / stress-test (push) Successful in 2s
Security Scanning / security-scan (push) Successful in 31s
fix: CLI bugs - island list, output signature, GPU credentials, mining subcommand
- S05 Islands: edge.py list_islands now has name='list' for 'aitbc edge island list'
- S06 Market: output() now handles structured data (dict/list) by JSON-encoding
- S09 GPU: added safe_load_credentials() to gpu_marketplace.py and exchange_island.py
  with graceful FileNotFoundError handling and helpful error message
- S13 Mining: added 'list' subcommand to mining.py for listing active miners
2026-05-19 19:38:06 +02:00

506 lines
19 KiB
Python

"""
Exchange Island CLI Commands
Commands for trading AIT coin against BTC and ETH on the island exchange
"""
import click
import json
import hashlib
import socket
import os
from datetime import datetime
from decimal import Decimal
from typing import Optional
from ..utils import output, error, success, info, warning
from ..utils.island_credentials import (
load_island_credentials, get_rpc_endpoint, get_chain_id,
get_island_id, get_island_name
)
# Import shared modules
from aitbc import get_logger, AITBCHTTPClient, NetworkError
# Initialize logger
logger = get_logger(__name__)
def safe_load_credentials():
"""Load island credentials with graceful error handling"""
try:
return load_island_credentials()
except FileNotFoundError as e:
error(f"Island credentials not found: {e}")
error("Run 'aitbc node island join' to join an island first")
return None
# Supported trading pairs
SUPPORTED_PAIRS = ['AIT/BTC', 'AIT/ETH']
@click.group()
def exchange_island():
"""Exchange commands for trading AIT against BTC and ETH on the island"""
pass
@exchange_island.command()
@click.argument('ait_amount', type=float)
@click.argument('quote_currency', type=click.Choice(['BTC', 'ETH']))
@click.option('--max-price', type=float, help='Maximum price to pay per AIT')
@click.pass_context
def buy(ctx, ait_amount: float, quote_currency: str, max_price: Optional[float]):
"""Buy AIT with BTC or ETH"""
try:
if ait_amount <= 0:
error("AIT amount must be greater than 0")
raise click.Abort()
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get user node ID
hostname = socket.gethostname()
local_address = socket.gethostbyname(hostname)
p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001)
# Get public key for node ID generation
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
public_key_pem = None
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
if public_key_pem:
content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}"
user_id = hashlib.sha256(content.encode()).hexdigest()
else:
error("No public key found in keystore")
raise click.Abort()
else:
error(f"Keystore not found at {keystore_path}")
raise click.Abort()
pair = f"AIT/{quote_currency}"
# Generate order ID
order_id = f"exchange_buy_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{user_id}{ait_amount}{quote_currency}'.encode()).hexdigest()[:8]}"
# Create buy order transaction
buy_order_data = {
'type': 'exchange',
'action': 'buy',
'order_id': order_id,
'user_id': user_id,
'pair': pair,
'side': 'buy',
'amount': float(ait_amount),
'max_price': float(max_price) if max_price else None,
'status': 'open',
'island_id': island_id,
'chain_id': chain_id,
'created_at': datetime.now().isoformat()
}
# Submit transaction to blockchain
try:
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
result = http_client.post("/transaction", json=buy_order_data)
success(f"Buy order created successfully!")
success(f"Order ID: {order_id}")
success(f"Buying {ait_amount} AIT with {quote_currency}")
if max_price:
success(f"Max price: {max_price:.8f} {quote_currency}/AIT")
order_info = {
"Order ID": order_id,
"Pair": pair,
"Side": "BUY",
"Amount": f"{ait_amount} AIT",
"Max Price": f"{max_price:.8f} {quote_currency}/AIT" if max_price else "Market",
"Status": "open",
"User": user_id[:16] + "...",
"Island": island_id[:16] + "..."
}
output(order_info, ctx.obj.get('output_format', 'table'))
except NetworkError as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error creating buy order: {str(e)}")
raise click.Abort()
@exchange_island.command()
@click.argument('ait_amount', type=float)
@click.argument('quote_currency', type=click.Choice(['BTC', 'ETH']))
@click.option('--min-price', type=float, help='Minimum price to accept per AIT')
@click.pass_context
def sell(ctx, ait_amount: float, quote_currency: str, min_price: Optional[float]):
"""Sell AIT for BTC or ETH"""
try:
if ait_amount <= 0:
error("AIT amount must be greater than 0")
raise click.Abort()
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get user node ID
hostname = socket.gethostname()
local_address = socket.gethostbyname(hostname)
p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001)
# Get public key for node ID generation
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
public_key_pem = None
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
if public_key_pem:
content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}"
user_id = hashlib.sha256(content.encode()).hexdigest()
else:
error("No public key found in keystore")
raise click.Abort()
else:
error(f"Keystore not found at {keystore_path}")
raise click.Abort()
pair = f"AIT/{quote_currency}"
# Generate order ID
order_id = f"exchange_sell_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{user_id}{ait_amount}{quote_currency}'.encode()).hexdigest()[:8]}"
# Create sell order transaction
sell_order_data = {
'type': 'exchange',
'action': 'sell',
'order_id': order_id,
'user_id': user_id,
'pair': pair,
'side': 'sell',
'amount': float(ait_amount),
'min_price': float(min_price) if min_price else None,
'status': 'open',
'island_id': island_id,
'chain_id': chain_id,
'created_at': datetime.now().isoformat()
}
# Submit transaction to blockchain
try:
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
result = http_client.post("/transaction", json=sell_order_data)
success(f"Sell order created successfully!")
success(f"Order ID: {order_id}")
success(f"Selling {ait_amount} AIT for {quote_currency}")
if min_price:
success(f"Min price: {min_price:.8f} {quote_currency}/AIT")
order_info = {
"Order ID": order_id,
"Pair": pair,
"Side": "SELL",
"Amount": f"{ait_amount} AIT",
"Min Price": f"{min_price:.8f} {quote_currency}/AIT" if min_price else "Market",
"Status": "open",
"User": user_id[:16] + "...",
"Island": island_id[:16] + "..."
}
output(order_info, ctx.obj.get('output_format', 'table'))
except NetworkError as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error creating sell order: {str(e)}")
raise click.Abort()
@exchange_island.command()
@click.argument('pair', type=click.Choice(SUPPORTED_PAIRS))
@click.option('--limit', type=int, default=20, help='Order book depth')
@click.pass_context
def orderbook(ctx, pair: str, limit: int):
"""View the order book for a trading pair"""
try:
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders
try:
params = {
'transaction_type': 'exchange',
'island_id': island_id,
'pair': pair,
'status': 'open',
'limit': limit * 2 # Get both buys and sells
}
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
transactions = http_client.get("/transactions", params=params)
# Separate buy and sell orders
buy_orders = []
sell_orders = []
for order in transactions:
if order.get('side') == 'buy':
buy_orders.append(order)
elif order.get('side') == 'sell':
sell_orders.append(order)
# Sort buy orders by price descending (highest first)
buy_orders.sort(key=lambda x: x.get('max_price', 0), reverse=True)
# Sort sell orders by price ascending (lowest first)
sell_orders.sort(key=lambda x: x.get('min_price', float('inf')))
if not buy_orders and not sell_orders:
info(f"No open orders for {pair}")
return
# Display sell orders (asks)
if sell_orders:
asks_data = []
for order in sell_orders[:limit]:
asks_data.append({
"Price": f"{order.get('min_price', 0):.8f}",
"Amount": f"{order.get('amount', 0):.4f} AIT",
"Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
"User": order.get('user_id', '')[:16] + "...",
"Order": order.get('order_id', '')[:16] + "..."
})
output(asks_data, ctx.obj.get('output_format', 'table'), title=f"Sell Orders (Asks) - {pair}")
# Display buy orders (bids)
if buy_orders:
bids_data = []
for order in buy_orders[:limit]:
bids_data.append({
"Price": f"{order.get('max_price', 0):.8f}",
"Amount": f"{order.get('amount', 0):.4f} AIT",
"Total": f"{order.get('max_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
"User": order.get('user_id', '')[:16] + "...",
"Order": order.get('order_id', '')[:16] + "..."
})
output(bids_data, ctx.obj.get('output_format', 'table'), title=f"Buy Orders (Bids) - {pair}")
# Calculate spread if both exist
if sell_orders and buy_orders:
best_ask = sell_orders[0].get('min_price', 0)
best_bid = buy_orders[0].get('max_price', 0)
spread = best_ask - best_bid
if best_bid > 0:
spread_pct = (spread / best_bid) * 100
info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)")
info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT")
info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT")
except NetworkError as e:
error(f"Network error fetching order book: {e}")
raise click.Abort()
except Exception as e:
error(f"Error fetching order book: {str(e)}")
raise click.Abort()
@exchange_island.command()
@click.pass_context
def rates(ctx):
"""View current exchange rates for AIT/BTC and AIT/ETH"""
try:
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders to calculate rates
try:
rates_data = []
for pair in SUPPORTED_PAIRS:
params = {
'transaction_type': 'exchange',
'island_id': island_id,
'pair': pair,
'status': 'open',
'limit': 100
}
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
orders = http_client.get("/transactions", params=params)
# Calculate rates from order book
buy_orders = [o for o in orders if o.get('side') == 'buy']
sell_orders = [o for o in orders if o.get('side') == 'sell']
# Get best bid and ask
best_bid = max([o.get('max_price', 0) for o in buy_orders]) if buy_orders else 0
best_ask = min([o.get('min_price', float('inf')) for o in sell_orders]) if sell_orders else 0
# Calculate mid price
mid_price = (best_bid + best_ask) / 2 if best_bid > 0 and best_ask < float('inf') else 0
rates_data.append({
"Pair": pair,
"Best Bid": f"{best_bid:.8f}" if best_bid > 0 else "N/A",
"Best Ask": f"{best_ask:.8f}" if best_ask < float('inf') else "N/A",
"Mid Price": f"{mid_price:.8f}" if mid_price > 0 else "N/A",
"Buy Orders": len(buy_orders),
"Sell Orders": len(sell_orders)
})
output(rates_data, ctx.obj.get('output_format', 'table'), title="Exchange Rates")
except Exception as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error viewing exchange rates: {str(e)}")
raise click.Abort()
@exchange_island.command()
@click.option('--user', help='Filter by user ID')
@click.option('--status', help='Filter by status (open, filled, partially_filled, cancelled)')
@click.option('--pair', type=click.Choice(SUPPORTED_PAIRS), help='Filter by trading pair')
@click.pass_context
def orders(ctx, user: Optional[str], status: Optional[str], pair: Optional[str]):
"""List exchange orders"""
try:
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders
try:
params = {
'transaction_type': 'exchange',
'island_id': island_id
}
if user:
params['user_id'] = user
if status:
params['status'] = status
if pair:
params['pair'] = pair
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
orders = http_client.get("/transactions", params=params)
if not orders:
info("No exchange orders found")
return
# Format output
orders_data = []
for order in orders:
orders_data.append({
"Order ID": order.get('order_id', '')[:20] + "...",
"Pair": order.get('pair'),
"Side": order.get('side', '').upper(),
"Amount": f"{order.get('amount', 0):.4f} AIT",
"Price": f"{order.get('max_price', order.get('min_price', 0)):.8f}" if order.get('max_price') or order.get('min_price') else "Market",
"Status": order.get('status'),
"User": order.get('user_id', '')[:16] + "...",
"Created": order.get('created_at', '')[:19]
})
output(orders_data, ctx.obj.get('output_format', 'table'), title=f"Exchange Orders ({island_id[:16]}...)")
except NetworkError as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error listing orders: {str(e)}")
raise click.Abort()
@exchange_island.command()
@click.argument('order_id')
@click.pass_context
def cancel(ctx, order_id: str):
"""Cancel an exchange order"""
try:
# Load island credentials
credentials = safe_load_credentials()
if not credentials:
return
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get local node ID
hostname = socket.gethostname()
local_address = socket.gethostbyname(hostname)
p2p_port = credentials.get('credentials', {}).get('p2p_port', 8001)
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
public_key_pem = None
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
if public_key_pem:
content = f"{hostname}:{local_address}:{p2p_port}:{public_key_pem}"
local_node_id = hashlib.sha256(content.encode()).hexdigest()
# Create cancel transaction
cancel_data = {
'type': 'exchange',
'action': 'cancel',
'order_id': order_id,
'user_id': local_node_id,
'status': 'cancelled',
'cancelled_at': datetime.now().isoformat(),
'island_id': island_id,
'chain_id': chain_id
}
# Submit transaction to blockchain
try:
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
result = http_client.post("/transaction", json=cancel_data)
success(f"Order {order_id} cancelled successfully!")
except NetworkError as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error cancelling order: {str(e)}")
raise click.Abort()